Merge branch 'main' into embedded-vnc

This commit is contained in:
Viktor Liu
2026-08-25 18:26:29 +02:00
439 changed files with 35257 additions and 5725 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"testing"
"time"
"github.com/golang/mock/gomock"
"go.uber.org/mock/gomock"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -591,7 +591,7 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) {
expectedFlowInfo := &mgmtProto.PKCEAuthorizationFlow{
ProviderConfig: &mgmtProto.ProviderConfig{
ClientID: "client",
ClientSecret: "secret",
ClientSecret: "secret", //nolint:staticcheck
},
}
+64 -18
View File
@@ -21,6 +21,8 @@ import (
"google.golang.org/grpc/connectivity"
nbgrpc "github.com/netbirdio/netbird/client/grpc"
"github.com/netbirdio/netbird/client/netstate"
"github.com/netbirdio/netbird/client/netsweep"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/shared/management/domain"
@@ -62,6 +64,13 @@ type GrpcClient struct {
connStateCallbackLock sync.RWMutex
serverURL string
// netState gates the stream retry loop on OS-reported network
// availability; nil (the default) disables gating.
netState *netstate.State
// sweeper cuts the transport connections on network change; nil disables it.
sweeper *netsweep.Sweeper
// syncStreamErr holds the last Sync stream error, or nil while the stream
// is established and healthy. GetServerKey succeeds even when the peer
// cannot sync (e.g. the server returns "settings not found"), so the
@@ -111,16 +120,43 @@ func MaxRecvMsgSize() int {
return size
}
// Option configures optional GrpcClient behavior.
type Option func(*GrpcClient)
// WithNetworkState injects the OS network availability state that gates the
// stream retry loop; without it gating is disabled.
func WithNetworkState(netState *netstate.State) Option {
return func(c *GrpcClient) { c.netState = netState }
}
// WithSweeper injects the network change sweeper.
func WithSweeper(sweeper *netsweep.Sweeper) Option {
return func(c *GrpcClient) { c.sweeper = sweeper }
}
// NewClient creates a new client to Management service
func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*GrpcClient, error) {
var conn *grpc.ClientConn
func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsEnabled bool, opts ...Option) (*GrpcClient, error) {
// Options apply before dialing: the sweeper must wrap the first connection too.
c := &GrpcClient{
key: ourPrivateKey,
ctx: ctx,
connStateCallbackLock: sync.RWMutex{},
serverURL: addr,
}
for _, opt := range opts {
opt(c)
}
var extraOpts []grpc.DialOption
if maxSize := MaxRecvMsgSize(); maxSize > 0 {
extraOpts = append(extraOpts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxSize)))
log.Infof("management gRPC max receive message size set to %d bytes", maxSize)
}
if c.sweeper != nil {
extraOpts = append(extraOpts, nbgrpc.WithSweeper(c.sweeper))
}
var conn *grpc.ClientConn
operation := func() error {
var err error
conn, err = nbgrpc.CreateConnection(ctx, addr, tlsEnabled, wsproxy.ManagementComponent, extraOpts...)
@@ -136,16 +172,9 @@ func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsE
return nil, err
}
realClient := proto.NewManagementServiceClient(conn)
return &GrpcClient{
key: ourPrivateKey,
realClient: realClient,
ctx: ctx,
conn: conn,
connStateCallbackLock: sync.RWMutex{},
serverURL: addr,
}, nil
c.conn = conn
c.realClient = proto.NewManagementServiceClient(conn)
return c, nil
}
// GetServerURL returns the management server URL
@@ -206,16 +235,33 @@ func (c *GrpcClient) withMgmtStream(
ctx context.Context,
handler func(ctx context.Context, serverPubKey wgtypes.Key, backOff backoff.BackOff) error,
) error {
backOff := defaultBackoff(ctx)
backOff := c.sweeper.QuickRetryBackoff(ctx, defaultBackoff(ctx), c.netState)
operation := func() error {
log.Debugf("management connection state %v", c.conn.GetState())
connState := c.conn.GetState()
// suspend reconnect attempts while the OS reports no usable network.
// Wait only errors on a cancelled context, which means shutdown, so
// stop the loop without reporting a failure.
if waited, err := c.netState.Wait(ctx); err != nil {
log.Debugf("management connection context has been canceled while offline, this usually indicates shutdown")
return nil //nolint:nilerr // a cancelled context means shutdown, not a retryable failure
} else if waited {
backOff.Reset()
}
connState := c.conn.GetState()
log.Debugf("management connection state %v", connState)
if connState == connectivity.Shutdown {
return backoff.Permanent(fmt.Errorf("connection to management has been shut down"))
} else if !(connState == connectivity.Ready || connState == connectivity.Idle) {
}
if !(connState == connectivity.Ready || connState == connectivity.Idle) {
// A dial may already be in flight (e.g. the other stream triggered
// it after a network change); wait for it to settle and proceed if
// the channel became usable, instead of burning a backoff round on
// a successful dial. A failed dial errors out as before.
c.conn.WaitForStateChange(ctx, connState)
return fmt.Errorf("connection to management is not ready and in %s state", connState)
connState = c.conn.GetState()
if !(connState == connectivity.Ready || connState == connectivity.Idle) {
return fmt.Errorf("connection to management is not ready and in %s state", connState)
}
}
serverPubKey, err := c.getServerPublicKey()
@@ -227,7 +273,7 @@ func (c *GrpcClient) withMgmtStream(
return handler(ctx, *serverPubKey, backOff)
}
err := backoff.Retry(operation, backOff)
err := nbgrpc.Retry(ctx, operation, backOff, c.netState)
if err != nil {
log.Warnf("exiting the Management service connection retry loop due to the unrecoverable error: %s", err)
}
+52 -16
View File
@@ -57,10 +57,9 @@ func (a *AgentNetworkAPI) GetProvider(ctx context.Context, providerID string) (*
return &ret, err
}
// CreateProvider creates a new Agent Network provider. Set
// request.BootstrapCluster on the account's first provider to bootstrap the
// per-account gateway endpoint (alternatively bootstrap via UpdateSettings
// with a cluster).
// CreateProvider creates a new Agent Network provider. Providers have no
// settings side effects — bootstrap the account's gateway endpoint separately
// via CreateSettings.
func (a *AgentNetworkAPI) CreateProvider(ctx context.Context, request api.PostApiAgentNetworkProvidersJSONRequestBody) (*api.AgentNetworkProvider, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
@@ -329,14 +328,13 @@ func (a *AgentNetworkAPI) DeleteBudgetRule(ctx context.Context, ruleID string) e
return nil
}
// GetSettings gets the account's Agent Network gateway settings (cluster,
// subdomain, endpoint, collection toggles). An account that has not been
// bootstrapped yet — via UpdateSettings with a cluster, or by creating the
// first provider with bootstrap_cluster set — reads as the defaults with an
// empty Cluster, Subdomain and Endpoint. Management servers prior to that
// contract answered 200 with a JSON null body instead; that legacy shape is
// translated to an APIError matchable via IsNotFound rather than fabricating
// defaults the server never stated.
// GetSettings gets the account's Agent Network gateway settings (endpoint,
// proxy address, collection toggles). An account that has not been
// bootstrapped yet — via CreateSettings — reads as the defaults with an empty
// Endpoint and ProxyAddress. Management servers prior to that contract
// answered 200 with a JSON null body instead; that legacy shape is translated
// to an APIError matchable via IsNotFound rather than fabricating defaults
// the server never stated.
func (a *AgentNetworkAPI) GetSettings(ctx context.Context) (*api.AgentNetworkSettings, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/settings", nil, nil)
if err != nil {
@@ -359,11 +357,33 @@ func (a *AgentNetworkAPI) GetSettings(ctx context.Context) (*api.AgentNetworkSet
return &ret, nil
}
// CreateSettings bootstraps the account's Agent Network settings row,
// assigning the immutable endpoint. Exactly one of request.ProxyAddress
// (labeled endpoint beneath that cluster; the server allocates the label) and
// request.Endpoint (self-addressed dedicated endpoint, claimed verbatim) must
// be set. Returns a conflict when the account already has a settings row.
func (a *AgentNetworkAPI) CreateSettings(ctx context.Context, request api.PostApiAgentNetworkSettingsJSONRequestBody) (*api.AgentNetworkSettings, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "POST", "/api/agent-network/settings", bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.AgentNetworkSettings](resp)
return &ret, err
}
// UpdateSettings updates the account's Agent Network settings; the request
// replaces every mutable field (collection toggles and retention). Setting
// request.Cluster bootstraps the settings row when the account does not have
// one yet; on a bootstrapped account it must match the assigned cluster (or
// be nil) and any other value is rejected — the cluster is immutable.
// carries every field, replacing the mutable ones (collection toggles and
// retention). The endpoint and proxy address are assigned at bootstrap
// (CreateSettings) and immutable — the request must echo them unchanged, and
// a request carrying different values is rejected. Returns not-found until
// the account is bootstrapped.
func (a *AgentNetworkAPI) UpdateSettings(ctx context.Context, request api.PutApiAgentNetworkSettingsJSONRequestBody) (*api.AgentNetworkSettings, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
@@ -379,3 +399,19 @@ func (a *AgentNetworkAPI) UpdateSettings(ctx context.Context, request api.PutApi
ret, err := parseResponse[api.AgentNetworkSettings](resp)
return &ret, err
}
// DeleteSettings deletes the account's Agent Network settings row, releasing
// the endpoint. The server refuses (precondition failed) while any provider
// exists for the account or while a proxy is actively serving the endpoint.
// Bootstrapping again afterwards allocates a new endpoint.
func (a *AgentNetworkAPI) DeleteSettings(ctx context.Context) error {
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/agent-network/settings", nil, nil)
if err != nil {
return err
}
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -47,9 +47,9 @@ var (
}
testAgentNetworkSettings = api.AgentNetworkSettings{
Cluster: "eu.proxy.netbird.io",
Subdomain: "violet",
Endpoint: "violet.eu.proxy.netbird.io",
ProxyAddress: "eu.proxy.netbird.io",
Dedicated: false,
EnableLogCollection: true,
AccessLogRetentionDays: ptr(30),
}
@@ -120,18 +120,15 @@ func TestAgentNetwork_CreateProvider_200(t *testing.T) {
var req api.PostApiAgentNetworkProvidersJSONRequestBody
require.NoError(t, json.Unmarshal(reqBytes, &req))
assert.Equal(t, "OpenAI", req.Name)
require.NotNil(t, req.BootstrapCluster)
assert.Equal(t, "eu.proxy.netbird.io", *req.BootstrapCluster)
retBytes, _ := json.Marshal(testAgentNetworkProvider)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AgentNetwork.CreateProvider(context.Background(), api.PostApiAgentNetworkProvidersJSONRequestBody{
ProviderId: "openai_api",
Name: "OpenAI",
UpstreamUrl: "https://api.openai.com",
ApiKey: ptr("sk-test"),
BootstrapCluster: ptr("eu.proxy.netbird.io"),
ProviderId: "openai_api",
Name: "OpenAI",
UpstreamUrl: "https://api.openai.com",
ApiKey: ptr("sk-test"),
})
require.NoError(t, err)
assert.Equal(t, testAgentNetworkProvider, *ret)
@@ -456,6 +453,45 @@ func TestAgentNetwork_GetSettings_LegacyNullBody(t *testing.T) {
})
}
func TestAgentNetwork_CreateSettings_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.PostApiAgentNetworkSettingsJSONRequestBody
require.NoError(t, json.Unmarshal(reqBytes, &req))
require.NotNil(t, req.ProxyAddress, "proxy address must be on the wire")
assert.Equal(t, "eu.proxy.netbird.io", *req.ProxyAddress)
assert.Nil(t, req.Endpoint, "endpoint must stay off the wire for a labeled bootstrap")
retBytes, _ := json.Marshal(testAgentNetworkSettings)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AgentNetwork.CreateSettings(context.Background(), api.PostApiAgentNetworkSettingsJSONRequestBody{
ProxyAddress: ptr("eu.proxy.netbird.io"),
})
require.NoError(t, err)
assert.Equal(t, testAgentNetworkSettings, *ret)
})
}
func TestAgentNetwork_CreateSettings_Conflict(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "agent network settings already bootstrapped for account acct1", Code: 409})
w.WriteHeader(409)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
_, err := c.AgentNetwork.CreateSettings(context.Background(), api.PostApiAgentNetworkSettingsJSONRequestBody{
Endpoint: ptr("gw.example.com"),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "already bootstrapped")
})
}
func TestAgentNetwork_UpdateSettings_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
@@ -464,15 +500,18 @@ func TestAgentNetwork_UpdateSettings_200(t *testing.T) {
require.NoError(t, err)
var req api.PutApiAgentNetworkSettingsJSONRequestBody
require.NoError(t, json.Unmarshal(reqBytes, &req))
require.NotNil(t, req.Cluster, "bootstrap cluster must be on the wire")
assert.Equal(t, "eu.proxy.netbird.io", *req.Cluster)
assert.True(t, req.EnableLogCollection)
assert.Equal(t, "brave-otter.eu.proxy.netbird.io", req.Endpoint,
"the identity echo must be on the wire")
assert.Equal(t, "eu.proxy.netbird.io", req.ProxyAddress,
"the identity echo must be on the wire")
retBytes, _ := json.Marshal(testAgentNetworkSettings)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AgentNetwork.UpdateSettings(context.Background(), api.PutApiAgentNetworkSettingsJSONRequestBody{
Cluster: ptr("eu.proxy.netbird.io"),
Endpoint: "brave-otter.eu.proxy.netbird.io",
ProxyAddress: "eu.proxy.netbird.io",
EnableLogCollection: true,
})
require.NoError(t, err)
@@ -483,15 +522,40 @@ func TestAgentNetwork_UpdateSettings_200(t *testing.T) {
func TestAgentNetwork_UpdateSettings_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "cluster is immutable once assigned (current: eu.proxy.netbird.io)", Code: 422})
w.WriteHeader(422)
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "agent network settings have not been bootstrapped yet; POST /api/agent-network/settings to bootstrap them", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
_, err := c.AgentNetwork.UpdateSettings(context.Background(), api.PutApiAgentNetworkSettingsJSONRequestBody{
Cluster: ptr("us.proxy.netbird.io"),
EnableLogCollection: true,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "immutable")
assert.True(t, rest.IsNotFound(err), "an unbootstrapped account must surface as IsNotFound")
})
}
func TestAgentNetwork_DeleteSettings_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "DELETE", r.Method)
_, err := w.Write([]byte("{}"))
require.NoError(t, err)
})
require.NoError(t, c.AgentNetwork.DeleteSettings(context.Background()))
})
}
func TestAgentNetwork_DeleteSettings_Guarded(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "agent network settings cannot be deleted while 2 provider(s) exist; delete the providers first", Code: 412})
w.WriteHeader(412)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
err := c.AgentNetwork.DeleteSettings(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot be deleted")
})
}