Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cmd/up/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type ClientUpCmdOpts struct {
ExitNodeTakesPrecedence bool
DisableRelay bool
SubnetRouter bool
ExitNode string
GatewaySiteIDs []int

GatewaySiteResourceID int
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions cmd/up/client/exitnode.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
37 changes: 37 additions & 0 deletions cmd/up/client/exitnode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions docs/pangolin_up.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/pangolin_up_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 6 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 12 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down
Loading