From 9169a36658c5d4a05d90c9ac73a37c3a11fa5a15 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Tue, 4 Aug 2026 02:06:48 +0000 Subject: [PATCH] [management, client] Make the agent-network setup RPC provable at runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Unimplemented answer to GetAgentNetworkSetup can only come from a server binary compiled without the regenerated management proto — the combined and management servers share the one registration path in boot.go. Make that failure mode self-diagnosing: - Log "ManagementService registered on gRPC server (agent-network setup RPC available)" at boot, so server logs prove which build is running. - Have the CLI name the management URL it dialed in every error, and map Unimplemented to an actionable message including the binary check (grep -ac GetAgentNetworkSetup ). - Pin the wire path with a round-trip test: a real gRPC server built from this tree routes the RPC through the NaCl envelope end to end. Verified live: a combined server built from this branch answers an unregistered probe with PermissionDenied "peer is not registered", never Unimplemented. Linear: NET-1399 --- client/cmd/agentnetwork.go | 13 ++- management/internals/server/boot.go | 1 + shared/management/client/agentnetwork_test.go | 89 +++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 shared/management/client/agentnetwork_test.go diff --git a/client/cmd/agentnetwork.go b/client/cmd/agentnetwork.go index 96bd2fc0b..0995e541b 100644 --- a/client/cmd/agentnetwork.go +++ b/client/cmd/agentnetwork.go @@ -104,10 +104,17 @@ func fetchAgentNetworkSetup(ctx context.Context) (*mgmProto.AgentNetworkSetupRes setup, err := mgmClient.GetAgentNetworkSetup(mgmCtx) if err != nil { - if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied { - return nil, fmt.Errorf("this peer is not registered with the management service — run 'netbird up' first") + if s, ok := status.FromError(err); ok { + switch s.Code() { + case codes.PermissionDenied: + return nil, fmt.Errorf("this peer is not registered with the management service at %s — run 'netbird up' first", config.ManagementURL.String()) + case codes.Unimplemented: + return nil, fmt.Errorf("the management server at %s does not implement the agent-network setup RPC — the process answering runs a build without it.\n"+ + "Verify the running binary contains the RPC: grep -ac GetAgentNetworkSetup (0 = built without it),\n"+ + "and that this URL actually reaches the server you rebuilt", config.ManagementURL.String()) + } } - return nil, fmt.Errorf("get agent network setup: %v", err) + return nil, fmt.Errorf("get agent network setup from %s: %v", config.ManagementURL.String(), err) } return setup, nil } diff --git a/management/internals/server/boot.go b/management/internals/server/boot.go index c671f0b67..67afb5aa1 100644 --- a/management/internals/server/boot.go +++ b/management/internals/server/boot.go @@ -216,6 +216,7 @@ func (s *BaseServer) GRPCServer() *grpc.Server { serviceMgr.StartExposeReaper(context.Background()) } mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv) + log.Info("ManagementService registered on gRPC server (agent-network setup RPC available)") mgmtProto.RegisterProxyServiceServer(gRPCAPIHandler, s.ReverseProxyGRPCServer()) log.Info("ProxyService registered on gRPC server") diff --git a/shared/management/client/agentnetwork_test.go b/shared/management/client/agentnetwork_test.go new file mode 100644 index 000000000..6e0f619d9 --- /dev/null +++ b/shared/management/client/agentnetwork_test.go @@ -0,0 +1,89 @@ +package client + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "google.golang.org/grpc" + + "github.com/netbirdio/netbird/encryption" + mgmtProto "github.com/netbirdio/netbird/shared/management/proto" +) + +// agentNetworkSetupServer is a minimal ManagementService that answers +// GetServerKey and GetAgentNetworkSetup with the real NaCl-box envelope, +// so the test pins the full wire path: gRPC method routing, request +// encryption, and response decryption. +type agentNetworkSetupServer struct { + mgmtProto.UnimplementedManagementServiceServer + key wgtypes.Key +} + +func (s *agentNetworkSetupServer) GetServerKey(_ context.Context, _ *mgmtProto.Empty) (*mgmtProto.ServerKeyResponse, error) { + return &mgmtProto.ServerKeyResponse{Key: s.key.PublicKey().String()}, nil +} + +func (s *agentNetworkSetupServer) GetAgentNetworkSetup(_ context.Context, msg *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) { + peerKey, err := wgtypes.ParseKey(msg.WgPubKey) + if err != nil { + return nil, err + } + req := &mgmtProto.AgentNetworkSetupRequest{} + if err := encryption.DecryptMessage(peerKey, s.key, msg.Body, req); err != nil { + return nil, err + } + resp := &mgmtProto.AgentNetworkSetupResponse{ + Configured: true, + Endpoint: "https://violet.eu.proxy.example.com", + Providers: []*mgmtProto.AgentNetworkProviderInfo{ + { + Name: "Anthropic prod", + CatalogId: "anthropic_api", + ApiFlavor: "anthropic", + Models: []string{"claude-sonnet-4-5"}, + }, + }, + } + body, err := encryption.EncryptMessage(peerKey, s.key, resp) + if err != nil { + return nil, err + } + return &mgmtProto.EncryptedMessage{WgPubKey: s.key.PublicKey().String(), Body: body}, nil +} + +// TestGetAgentNetworkSetup_RoundTrip proves the RPC is routable on any +// server built from this tree and that the encrypt→invoke→decrypt path +// the CLI uses round-trips. A server answering this call with +// codes.Unimplemented is by definition running a binary compiled without +// the regenerated management proto. +func TestGetAgentNetworkSetup_RoundTrip(t *testing.T) { + serverKey, err := wgtypes.GeneratePrivateKey() + require.NoError(t, err) + + lis, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + srv := grpc.NewServer() + mgmtProto.RegisterManagementServiceServer(srv, &agentNetworkSetupServer{key: serverKey}) + go func() { _ = srv.Serve(lis) }() + t.Cleanup(srv.Stop) + + clientKey, err := wgtypes.GeneratePrivateKey() + require.NoError(t, err) + + client, err := NewClient(context.Background(), lis.Addr().String(), clientKey, false) + require.NoError(t, err) + t.Cleanup(func() { _ = client.Close() }) + + setup, err := client.GetAgentNetworkSetup(context.Background()) + require.NoError(t, err) + assert.True(t, setup.Configured) + assert.Equal(t, "https://violet.eu.proxy.example.com", setup.Endpoint) + require.Len(t, setup.Providers, 1) + assert.Equal(t, "anthropic_api", setup.Providers[0].CatalogId) + assert.Equal(t, []string{"claude-sonnet-4-5"}, setup.Providers[0].Models) +}