Work around Sequoia's "Local Network" permission with a helper process (#302)

* Work around Sequoia's "Local Network" permission with a helper process

* README.md: macOS 15 (Sequoia) warning

* Make "orchard dev" unix-specific too, otherwise Release fails

* Fix typo in "localNetworkHerlper"

* Slightly improve the macOS 15 (Sequoia) note

* orchard worker run: better documentation for --user

* Make sure privilege dropping is the first step we do in runWorker()
This commit is contained in:
Nikolay Edigaryev 2025-04-10 16:01:19 +02:00 committed by GitHub
parent fa38fe72ed
commit abcfee677d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 201 additions and 32 deletions

View File

@ -1,5 +1,19 @@
# Orchard
> [!IMPORTANT]
>
> **macOS 15 (Sequoia)**
>
> The [newly introduced "Local Network" permission](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy) in macOS Sequoia requires accepting a GUI pop-up on each host machine that runs the Orchard Worker.
>
> To work around this, upgrade your workers to Orchard 0.32.0 or newer and invoke the `orchard worker run` as `root` with an additional `--user` command-line argument, which takes a name of your regular, non-privileged user on the host machine.
>
> This will cause the Orchard Worker to start a small `orchard localnetworkhelper` process in the background and then drop the privileges to the specified user.
>
>The helper process is privileged and needed to establish network connections on behalf of the Orchard Worker without triggering a GUI pop-up.
>
>This approach is more secure than simply running `orchard worker run` as `root`, because only a small part of Orchard Worker runs privileged and the only functionality that this part has is establishing new connections.
<img src="https://github.com/cirruslabs/orchard/raw/main/docs/OrchardSocial.png"/>
Orchard is an orchestration system for [Tart](https://github.com/cirruslabs/tart). Create a cluster of bare-metal Apple Silicon machines and manage dozens of VMs with ease!

13
go.mod
View File

@ -1,11 +1,13 @@
module github.com/cirruslabs/orchard
go 1.23.3
toolchain go1.24.1
go 1.24
toolchain go1.24.2
require (
github.com/avast/retry-go v3.0.0+incompatible
github.com/avast/retry-go/v4 v4.6.1
github.com/cirruslabs/chacha v0.10.0
github.com/coder/websocket v1.8.13
github.com/deckarep/golang-set/v2 v2.8.0
github.com/dgraph-io/badger/v3 v3.2103.5
@ -37,7 +39,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.35.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.36.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
golang.org/x/net v0.38.0
golang.org/x/term v0.30.0
google.golang.org/grpc v1.71.1
@ -62,7 +64,7 @@ require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
@ -107,10 +109,11 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/puzpuzpuz/xsync/v4 v4.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

14
go.sum
View File

@ -50,6 +50,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cirruslabs/chacha v0.10.0 h1:Ny/U7sVFkB3DAZM0PAvbkEX4/IN0bUI97XlQGx4kzzA=
github.com/cirruslabs/chacha v0.10.0/go.mod h1:qbGp1psGAO8KfN4ZkXSHiTWkyZh9az5DUmLyP1ihe3g=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
@ -66,8 +68,9 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
@ -240,8 +243,9 @@ github.com/penglongli/gin-metrics v0.1.13 h1:a1wyrXcbUVxL5w4c2TSv+9kyQA9qM1o23h0
github.com/penglongli/gin-metrics v0.1.13/go.mod h1:VEmSyx/9TwUG50IsPCgjMKOUuGO74V2lmkLZ6x1Dlko=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -260,6 +264,8 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
github.com/puzpuzpuz/xsync/v4 v4.0.0 h1:F1za+MBXzDQtQq+OVgFsojSX4w66rsNDmQNebPFAncA=
github.com/puzpuzpuz/xsync/v4 v4.0.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -354,8 +360,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@ -1,3 +1,5 @@
//go:build unix
package dev
import (

View File

@ -0,0 +1,9 @@
//go:build !unix
package dev
import "github.com/spf13/cobra"
func NewCommand() *cobra.Command {
return nil
}

View File

@ -0,0 +1,31 @@
//go:build unix
package localnetworkhelper
import (
"github.com/cirruslabs/chacha/pkg/localnetworkhelper"
"github.com/spf13/cobra"
)
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: localnetworkhelper.CommandName,
Short: "Run the macOS \"Local Network\" permission helper process",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
// In localnetworkhelper.New(), a macOS "Local Network"
// permission helper process is spawned and receives
// its socketpair(2) end via ExtraFiles field of Golang's
// exec.Cmd[1].
//
// This socketpair(2) end becomes FD number 3 here,
// according to the ExtraFiles documentation[1]:
//
// >If non-nil, entry i becomes file descriptor 3+i.
//
// [1]: https://pkg.go.dev/os/exec#Cmd
return localnetworkhelper.Serve(3)
},
}
return cmd
}

View File

@ -0,0 +1,11 @@
//go:build !unix
package localnetworkhelper
import (
"github.com/spf13/cobra"
)
func NewCommand() *cobra.Command {
return nil
}

View File

@ -8,6 +8,7 @@ import (
"github.com/cirruslabs/orchard/internal/command/dev"
"github.com/cirruslabs/orchard/internal/command/get"
"github.com/cirruslabs/orchard/internal/command/list"
"github.com/cirruslabs/orchard/internal/command/localnetworkhelper"
"github.com/cirruslabs/orchard/internal/command/logs"
"github.com/cirruslabs/orchard/internal/command/pause"
"github.com/cirruslabs/orchard/internal/command/portforward"
@ -37,6 +38,10 @@ func NewRootCmd() *cobra.Command {
},
}
if localNetworkHelperCommand := localnetworkhelper.NewCommand(); localNetworkHelperCommand != nil {
command.AddCommand(localNetworkHelperCommand)
}
addGroupedCommands(command, "Working With Resources:",
create.NewCommand(),
deletepkg.NewCommand(),
@ -51,12 +56,20 @@ func NewRootCmd() *cobra.Command {
vnc.NewCommand(),
)
addGroupedCommands(command, "Administrative Tasks:",
administrativeCommands := []*cobra.Command{
context.NewCommand(),
controller.NewCommand(),
worker.NewCommand(),
dev.NewCommand(),
)
}
if devCommand := dev.NewCommand(); devCommand != nil {
administrativeCommands = append(administrativeCommands, devCommand)
}
if workerCommand := worker.NewCommand(); workerCommand != nil {
administrativeCommands = append(administrativeCommands, workerCommand)
}
addGroupedCommands(command, "Administrative Tasks:", administrativeCommands...)
return command
}

View File

@ -1,8 +1,12 @@
//go:build unix
package worker
import (
"errors"
"fmt"
"github.com/cirruslabs/chacha/pkg/localnetworkhelper"
"github.com/cirruslabs/chacha/pkg/privdrop"
"github.com/cirruslabs/orchard/internal/bootstraptoken"
"github.com/cirruslabs/orchard/internal/netconstants"
"github.com/cirruslabs/orchard/internal/worker"
@ -32,6 +36,7 @@ var labels map[string]string
var noPKI bool
var defaultCPU uint64
var defaultMemory uint64
var username string
var debug bool
func newRunCommand() *cobra.Command {
@ -62,17 +67,46 @@ func newRunCommand() *cobra.Command {
"that do not explicitly specify a value")
cmd.PersistentFlags().Uint64Var(&defaultMemory, "default-memory", 8*1024, "megabytes of memory "+
"to use for VMs that do not explicitly specify a value")
cmd.Flags().StringVar(&username, "user", "", "username to drop privileges to "+
"(\"Local Network\" permission workaround: requires starting \"orchard worker run\" as \"root\", "+
"the privileges will be then dropped to the specified user after starting the \"orchard localnetworkhelper\" "+
"helper process)")
cmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
return cmd
}
func runWorker(cmd *cobra.Command, args []string) (err error) {
var clientOpts []client.Option
workerOpts := []worker.Option{
worker.WithName(name),
worker.WithLabels(labels),
worker.WithDefaultCPUAndMemory(defaultCPU, defaultMemory),
}
// Run the macOS "Local Network" permission helper
// when privilege dropping is requested
if username != "" {
localNetworkHelper, err := localnetworkhelper.New(cmd.Context())
if err != nil {
return err
}
clientOpts = append(clientOpts, client.WithDialContext(localNetworkHelper.PrivilegedDialContext))
workerOpts = append(workerOpts, worker.WithLocalNetworkHelper(localNetworkHelper))
if err := privdrop.Drop(username); err != nil {
return err
}
}
// Parse controller URL
controllerURL, err := netconstants.NormalizeAddress(args[0])
if err != nil {
return err
}
clientOpts = append(clientOpts, client.WithAddress(controllerURL.String()))
// Parse bootstrap token
bootstrapTokenRaw, err := readBootstrapToken()
@ -86,17 +120,15 @@ func runWorker(cmd *cobra.Command, args []string) (err error) {
if err != nil {
return err
}
clientOpts = append(clientOpts, client.WithCredentials(bootstrapToken.ServiceAccountName(),
bootstrapToken.ServiceAccountToken()))
// Convert resources
resources, err := v1.NewResourcesFromStringToString(stringToStringResources)
if err != nil {
return fmt.Errorf("%w: %v", ErrRunFailed, err)
}
clientOpts := []client.Option{
client.WithAddress(controllerURL.String()),
client.WithCredentials(bootstrapToken.ServiceAccountName(), bootstrapToken.ServiceAccountToken()),
}
workerOpts = append(workerOpts, worker.WithResources(resources))
if trustedCertificate := bootstrapToken.Certificate(); trustedCertificate != nil {
clientOpts = append(clientOpts, client.WithTrustedCertificate(trustedCertificate))
@ -120,14 +152,11 @@ func runWorker(cmd *cobra.Command, args []string) (err error) {
err = syncErr
}
}()
workerOpts = append(workerOpts, worker.WithLogger(logger))
workerInstance, err := worker.New(
controllerClient,
worker.WithName(name),
worker.WithResources(resources),
worker.WithLabels(labels),
worker.WithDefaultCPUAndMemory(defaultCPU, defaultMemory),
worker.WithLogger(logger),
workerOpts...,
)
if err != nil {
return err

View File

@ -1,3 +1,5 @@
//go:build unix
package worker
import (

View File

@ -0,0 +1,9 @@
//go:build !unix
package worker
import "github.com/spf13/cobra"
func NewCommand() *cobra.Command {
return nil
}

View File

@ -1,6 +1,7 @@
package worker
import (
"github.com/cirruslabs/chacha/pkg/localnetworkhelper"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"go.uber.org/zap"
)
@ -32,6 +33,12 @@ func WithDefaultCPUAndMemory(defaultCPU uint64, defaultMemory uint64) Option {
}
}
func WithLocalNetworkHelper(localNetworkHelper *localnetworkhelper.LocalNetworkHelper) Option {
return func(worker *Worker) {
worker.localNetworkHelper = localNetworkHelper
}
}
func WithLogger(logger *zap.Logger) Option {
return func(worker *Worker) {
worker.logger = logger.Sugar()

View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"github.com/avast/retry-go"
"github.com/cirruslabs/chacha/pkg/localnetworkhelper"
"github.com/cirruslabs/orchard/internal/worker/ondiskname"
"github.com/cirruslabs/orchard/internal/worker/tart"
"github.com/cirruslabs/orchard/pkg/client"
@ -51,12 +52,15 @@ type VM struct {
cancel context.CancelFunc
wg *sync.WaitGroup
localNetworkHelper *localnetworkhelper.LocalNetworkHelper
}
func NewVM(
vmResource v1.VM,
eventStreamer *client.EventStreamer,
vmPullTimeHistogram metric.Float64Histogram,
localNetworkHelper *localnetworkhelper.LocalNetworkHelper,
logger *zap.SugaredLogger,
) *VM {
vmContext, vmContextCancel := context.WithCancel(context.Background())
@ -74,6 +78,8 @@ func NewVM(
cancel: vmContextCancel,
wg: &sync.WaitGroup{},
localNetworkHelper: localNetworkHelper,
}
vm.wg.Add(1)
@ -429,11 +435,18 @@ func (vm *VM) shell(
addr := ip + ":22"
dialer := net.Dialer{
Timeout: 5 * time.Second,
}
dialCtx, dialCtxCancel := context.WithTimeout(ctx, 5*time.Second)
defer dialCtxCancel()
netConn, err := dialer.DialContext(ctx, "tcp", addr)
var netConn net.Conn
if vm.localNetworkHelper != nil {
netConn, err = vm.localNetworkHelper.PrivilegedDialContext(dialCtx, "tcp", addr)
} else {
dialer := net.Dialer{}
netConn, err = dialer.DialContext(dialCtx, "tcp", addr)
}
if err != nil {
return fmt.Errorf("failed to dial %s: %w", addr, err)
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"github.com/avast/retry-go/v4"
"github.com/cirruslabs/chacha/pkg/localnetworkhelper"
"github.com/cirruslabs/orchard/internal/opentelemetry"
"github.com/cirruslabs/orchard/internal/worker/dhcpleasetime"
"github.com/cirruslabs/orchard/internal/worker/iokitregistry"
@ -40,6 +41,8 @@ type Worker struct {
vmPullTimeHistogram metric.Float64Histogram
localNetworkHelper *localnetworkhelper.LocalNetworkHelper
logger *zap.SugaredLogger
}
@ -409,7 +412,8 @@ func (worker *Worker) deleteVM(vm *vmmanager.VM) error {
func (worker *Worker) createVM(odn ondiskname.OnDiskName, vmResource v1.VM) {
eventStreamer := worker.client.VMs().StreamEvents(vmResource.Name)
vm := vmmanager.NewVM(vmResource, eventStreamer, worker.vmPullTimeHistogram, worker.logger)
vm := vmmanager.NewVM(vmResource, eventStreamer, worker.vmPullTimeHistogram,
worker.localNetworkHelper, worker.logger)
worker.vmm.Put(odn, vm)
}

View File

@ -40,6 +40,8 @@ type Client struct {
serviceAccountName string
serviceAccountToken string
dialContext func(ctx context.Context, network, addr string) (net.Conn, error)
}
type Config struct {
@ -77,15 +79,21 @@ func New(opts ...Option) (*Client, error) {
}
// Instantiate the HTTP client
transport := &http.Transport{
TLSClientConfig: client.tlsConfig,
}
if client.dialContext != nil {
transport.DialContext = client.dialContext
}
client.httpClient = &http.Client{
// The default is zero, which means no timeout, which means that
// the requests may hang indefinitely. See [1] for more details.
//
// [1]: https://github.com/cirruslabs/orchard/issues/152#issuecomment-1927091747
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: client.tlsConfig,
},
Timeout: 30 * time.Second,
Transport: transport,
}
url, err := url.Parse(client.address)

View File

@ -1,7 +1,9 @@
package client
import (
"context"
"crypto/x509"
"net"
)
type Option func(*Client)
@ -24,3 +26,9 @@ func WithCredentials(serviceAccountName string, serviceAccountToken string) Opti
client.serviceAccountToken = serviceAccountToken
}
}
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Option {
return func(client *Client) {
client.dialContext = dialContext
}
}