diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index 6b011aa..2ce6c2f 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -58,6 +58,7 @@ type ClientUpCmdOpts struct { ExitNodeTakesPrecedence bool DisableRelay bool SubnetRouter bool + ExitNode string GatewaySiteIDs []int GatewaySiteResourceID int @@ -197,8 +198,11 @@ logs, and removes the service again when you press Ctrl+C.`, cmd.Flags().BoolVar(&opts.ExitNodeTakesPrecedence, "exit-node-takes-precedence", false, "Do not add routes or resolve aliases for individual resources, so all traffic is sent through the exit node instead of directly to resources (default false)") cmd.Flags().BoolVar(&opts.DisableRelay, "disable-relay", false, "Disable relay connections (default false)") cmd.Flags().BoolVar(&opts.SubnetRouter, "subnet-router", false, "Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false)") + cmd.Flags().StringVar(&opts.ExitNode, "exit-node", "", "Exit node `NICE-ID` to route all traffic through for this connection, overriding the exit node saved by 'pangolin select exit-node'. Requires being logged in") cmd.Flags().IntSliceVar(&opts.GatewaySiteIDs, "exit-node-site-ids", nil, "Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any). Requires --exit-node-resource-id") cmd.Flags().IntVar(&opts.GatewaySiteResourceID, "exit-node-resource-id", 0, "ID of the exit node resource the --exit-node-site-ids belong to, so changes to that resource are applied while connected") + cmd.MarkFlagsMutuallyExclusive("exit-node", "exit-node-site-ids") + cmd.MarkFlagsMutuallyExclusive("exit-node", "exit-node-resource-id") cmd.Flags().BoolVar(&opts.Attached, "attach", false, "Run in attached (foreground) mode, (default: detached (background) mode)") cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Disable TUI and run silently when detached") @@ -443,7 +447,18 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) savedResourceID = activeAccount.ExitNodeResourceID } } - opts.GatewaySiteResourceID, opts.GatewaySiteIDs = resolveSavedExitNode(savedResourceID, orgID, list) + if opts.ExitNode != "" { + // Overrides the saved exit node for this run only; the saved one + // is left as is. + var err error + opts.GatewaySiteResourceID, opts.GatewaySiteIDs, err = resolveExitNodeByNiceID(opts.ExitNode, orgID, list) + if err != nil { + logger.Error("Error: %v", err) + return err + } + } else { + opts.GatewaySiteResourceID, opts.GatewaySiteIDs = resolveSavedExitNode(savedResourceID, orgID, list) + } } else if len(opts.GatewaySiteIDs) > 0 && opts.GatewaySiteResourceID <= 0 { err := fmt.Errorf("--exit-node-site-ids requires --exit-node-resource-id") logger.Error("%v", err) diff --git a/cmd/up/client/exitnode.go b/cmd/up/client/exitnode.go index 3d94f9c..c1f3f32 100644 --- a/cmd/up/client/exitnode.go +++ b/cmd/up/client/exitnode.go @@ -1,10 +1,42 @@ package client import ( + "errors" + "fmt" + "github.com/fosrl/cli/internal/api" "github.com/fosrl/cli/internal/logger" ) +// resolveExitNodeByNiceID returns the resource ID and current site IDs of the +// exit node with the given niceId, as passed to --exit-node. Unlike a saved +// exit node, this one was asked for explicitly, so failing to resolve it is an +// error rather than a reason to connect without one. +// +// list is nil when there's no user session to query the server with. +func resolveExitNodeByNiceID(niceID string, orgID string, list func(orgID string) ([]api.SiteResource, error)) (int, []int, error) { + if list == nil || orgID == "" { + return 0, nil, errors.New("--exit-node needs a logged-in session to look up; pass --exit-node-site-ids and --exit-node-resource-id instead") + } + + gateways, err := list(orgID) + if err != nil { + return 0, nil, fmt.Errorf("failed to look up exit node '%s': %w", niceID, err) + } + + for _, g := range gateways { + if g.NiceID != niceID { + continue + } + if !g.Enabled || len(g.SiteIDs) == 0 { + return 0, nil, fmt.Errorf("exit node '%s' is disabled or has no sites", niceID) + } + return g.SiteResourceID, g.SiteIDs, nil + } + + return 0, nil, fmt.Errorf("exit node '%s' not found", niceID) +} + // resolveSavedExitNode returns the resource ID and current site IDs of the exit // node saved by `pangolin select exit-node` on the active account, or 0/nil if // none should be applied. Only the resource ID is saved (scoped to the diff --git a/cmd/up/client/exitnode_test.go b/cmd/up/client/exitnode_test.go index 85200cb..440eef1 100644 --- a/cmd/up/client/exitnode_test.go +++ b/cmd/up/client/exitnode_test.go @@ -8,6 +8,43 @@ import ( "github.com/fosrl/cli/internal/api" ) +func TestResolveExitNodeByNiceID(t *testing.T) { + lister := func(gws ...api.SiteResource) func(string) ([]api.SiteResource, error) { + return func(string) ([]api.SiteResource, error) { return gws, nil } + } + + tests := []struct { + name string + niceID string + org string + list func(string) ([]api.SiteResource, error) + want []int + wantID int + wantErr bool + }{ + {"matches by niceId", "gw-a", "org1", + lister(api.SiteResource{SiteResourceID: 11, NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}, api.SiteResource{SiteResourceID: 12, NiceID: "gw-a", Enabled: true, SiteIDs: []int{2, 3}}), []int{2, 3}, 12, false}, + {"not found", "gw-a", "org1", lister(api.SiteResource{SiteResourceID: 11, NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}), nil, 0, true}, + {"numeric id is not a niceId", "12", "org1", lister(api.SiteResource{SiteResourceID: 12, NiceID: "gw-a", Enabled: true, SiteIDs: []int{1, 2}}), nil, 0, true}, + {"resource disabled", "gw-a", "org1", lister(api.SiteResource{SiteResourceID: 12, NiceID: "gw-a", Enabled: false, SiteIDs: []int{1, 2}}), nil, 0, true}, + {"resource has no sites", "gw-a", "org1", lister(api.SiteResource{SiteResourceID: 12, NiceID: "gw-a", Enabled: true}), nil, 0, true}, + {"lookup fails", "gw-a", "org1", func(string) ([]api.SiteResource, error) { return nil, errors.New("boom") }, nil, 0, true}, + {"no session to look it up with", "gw-a", "org1", nil, nil, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotID, got, err := resolveExitNodeByNiceID(tt.niceID, tt.org, tt.list) + if (err != nil) != tt.wantErr { + t.Fatalf("got err %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) || gotID != tt.wantID { + t.Fatalf("got %d %v, want %d %v", gotID, got, tt.wantID, tt.want) + } + }) + } +} + func TestResolveSavedExitNode(t *testing.T) { const savedResourceID = 12 lister := func(gws ...api.SiteResource) func(string) ([]api.SiteResource, error) { diff --git a/docs/pangolin_up.md b/docs/pangolin_up.md index 63e52c2..77c33e5 100644 --- a/docs/pangolin_up.md +++ b/docs/pangolin_up.md @@ -19,6 +19,7 @@ pangolin up [flags] --attach Run in attached (foreground) mode, (default: detached (background) mode) --disable-relay Disable relay connections (default false) --endpoint string Client endpoint (required if not logged in) + --exit-node NICE-ID Exit node NICE-ID to route all traffic through for this connection, overriding the exit node saved by 'pangolin select exit-node'. Requires being logged in --exit-node-resource-id int ID of the exit node resource the --exit-node-site-ids belong to, so changes to that resource are applied while connected --exit-node-site-ids ints Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any). Requires --exit-node-resource-id --exit-node-takes-precedence Do not add routes or resolve aliases for individual resources, so all traffic is sent through the exit node instead of directly to resources (default false) diff --git a/docs/pangolin_up_client.md b/docs/pangolin_up_client.md index 646632f..425b0d9 100644 --- a/docs/pangolin_up_client.md +++ b/docs/pangolin_up_client.md @@ -23,6 +23,7 @@ pangolin up client [flags] --attach Run in attached (foreground) mode, (default: detached (background) mode) --disable-relay Disable relay connections (default false) --endpoint string Client endpoint (required if not logged in) + --exit-node NICE-ID Exit node NICE-ID to route all traffic through for this connection, overriding the exit node saved by 'pangolin select exit-node'. Requires being logged in --exit-node-resource-id int ID of the exit node resource the --exit-node-site-ids belong to, so changes to that resource are applied while connected --exit-node-site-ids ints Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any). Requires --exit-node-resource-id --exit-node-takes-precedence Do not add routes or resolve aliases for individual resources, so all traffic is sent through the exit node instead of directly to resources (default false) diff --git a/go.mod b/go.mod index e2d5072..2bfa41a 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/creack/pty v1.1.24 - github.com/fosrl/newt v1.18.0 - github.com/fosrl/olm v1.10.0 + github.com/fosrl/newt v1.18.1 + github.com/fosrl/olm v1.10.1 github.com/mattn/go-isatty v0.0.24 github.com/pelletier/go-toml/v2 v2.4.3 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c @@ -49,7 +49,7 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect - github.com/gaissmai/bart v0.29.0 // indirect + github.com/gaissmai/bart v0.30.0 // indirect github.com/go-crypt/crypt v0.14.15 // indirect github.com/go-crypt/x v0.4.17 // indirect github.com/go-logr/logr v1.4.4 // indirect @@ -68,7 +68,7 @@ require ( github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect github.com/mdlayher/socket v0.5.1 // indirect - github.com/miekg/dns v1.1.70 // indirect + github.com/miekg/dns v1.1.73 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/moby/api v1.56.0 // indirect @@ -109,19 +109,17 @@ require ( go.opentelemetry.io/otel/trace v1.46.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect - golang.org/x/mod v0.41.0 // indirect golang.org/x/net v0.59.0 // indirect golang.org/x/sync v0.23.0 // indirect golang.org/x/text v0.42.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.49.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect - golang.zx2c4.com/wireguard/windows v1.0.1 // indirect + golang.zx2c4.com/wireguard/windows v1.1.1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect - google.golang.org/grpc v1.83.2 // indirect + google.golang.org/grpc v1.84.0 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect diff --git a/go.sum b/go.sum index e85b157..f513030 100644 --- a/go.sum +++ b/go.sum @@ -74,16 +74,16 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/fosrl/newt v1.18.0 h1:fFBksIV6BoI+BGmv6fwo87tXBk1WlxbVoruvupeD6hk= -github.com/fosrl/newt v1.18.0/go.mod h1:CwcuQtifgDQeSWSEB3yfqOgheFv9yltVBYQNgDB/DoM= -github.com/fosrl/olm v1.10.0 h1:2YS6Kirab1yg+MZSM8y9/Ja5IpwQBFY6d7ZMsPuYGM0= -github.com/fosrl/olm v1.10.0/go.mod h1:c/ECpzDjwnkcLrXxNpFDpFViE7+NC8yRuuLCOhGj7OA= +github.com/fosrl/newt v1.18.1 h1:gKWBDKSqQLSgD6z0jPzZB86U19a2xNfOTV0S1KdYSx0= +github.com/fosrl/newt v1.18.1/go.mod h1:gTr7YDmLqjVrqZJ3SQU0MR9wKfqbUZOJF8hSFIi/M+I= +github.com/fosrl/olm v1.10.1 h1:9q6sMuWpT8MzTj7UX4q5VYzPc8CJmW+9eBqZVIUkNZU= +github.com/fosrl/olm v1.10.1/go.mod h1:WqvwXlFtE5WjDFZj35VTdMYIhIRh4Nb65NDSNpmYE1U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/gaissmai/bart v0.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI= -github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/gaissmai/bart v0.30.0 h1:K1oTSQ6Nf76bhrgxeTqzZa4hE0M9x4xDfL6n6UG67Sc= +github.com/gaissmai/bart v0.30.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= github.com/go-crypt/crypt v0.14.15 h1:q1i5OMpL05r935IxWmXgpDAVF0nvi4SMoHhGXLBQUEQ= github.com/go-crypt/crypt v0.14.15/go.mod h1:0n/to1VqIZPENj2yEUa/sLLYYnmupma6cp+QMX4zfF0= github.com/go-crypt/x v0.4.17 h1:SK642R97T/DyDRvm8FGu5Cr2z3rH5/mV+Axc7ve2PL0= @@ -133,8 +133,8 @@ github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0 github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= -github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -239,8 +239,6 @@ golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= @@ -257,24 +255,22 @@ golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= -golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8= -golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= +golang.zx2c4.com/wireguard/windows v1.1.1 h1:8/H97U1v1PNDNcBsMZgU3KFuND9MQdTsU2NOwmCXArE= +golang.zx2c4.com/wireguard/windows v1.1.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=