mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-28 02:21:30 +02:00
Merge remote-tracking branch 'origin/main' into android-airplane
# Conflicts: # client/android/client.go # client/ios/NetBirdSDK/client.go
This commit is contained in:
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5208,10 +5208,6 @@ components:
|
||||
type: string
|
||||
description: Full upstream URL (with scheme) that NetBird forwards traffic to.
|
||||
example: "https://api.openai.com"
|
||||
bootstrap_cluster:
|
||||
type: string
|
||||
description: Proxy cluster used to bootstrap the per-account agent-network endpoint when the first provider is created. Ignored on subsequent creates and on updates because the cluster is pinned on the account-level Settings row.
|
||||
example: "eu.proxy.netbird.io"
|
||||
api_key:
|
||||
type: string
|
||||
description: Upstream provider API key. Sealed at rest on the management server and never returned in responses. Required on create; optional on update (omit to keep the existing key).
|
||||
@@ -6193,20 +6189,20 @@ components:
|
||||
- cache_cost_usd
|
||||
AgentNetworkSettings:
|
||||
type: object
|
||||
description: Per-account Agent Network gateway settings. One row per account; cluster and subdomain are assigned at bootstrap and immutable thereafter. Before bootstrap the account reads as the default values with empty cluster, subdomain and endpoint.
|
||||
description: Per-account Agent Network gateway settings. One row per account; endpoint and proxy_address are assigned at bootstrap (POST) and immutable thereafter. Before bootstrap the account reads as the default values with empty endpoint and proxy_address.
|
||||
properties:
|
||||
cluster:
|
||||
type: string
|
||||
description: Address of the NetBird proxy cluster fronting this account's agent-network endpoint. Empty until the account is bootstrapped.
|
||||
example: "eu.proxy.netbird.io"
|
||||
subdomain:
|
||||
type: string
|
||||
description: Auto-generated DNS-safe label that prefixes the cluster to form the agent-network endpoint. Empty until the account is bootstrapped.
|
||||
example: "violet"
|
||||
endpoint:
|
||||
type: string
|
||||
description: Bare hostname agents call for this account, computed as `<subdomain>.<cluster>`. Empty until the account is bootstrapped.
|
||||
example: "violet.eu.proxy.netbird.io"
|
||||
description: Bare hostname agents call for this account. Empty until the account is bootstrapped.
|
||||
example: "brave-otter.eu.proxy.netbird.io"
|
||||
proxy_address:
|
||||
type: string
|
||||
description: Declared cluster address of the proxy serving this account's gateway. Equal to `endpoint` when a dedicated proxy serves the account; otherwise the endpoint's immediate parent (a shared cluster the endpoint hangs one label beneath). Empty until the account is bootstrapped.
|
||||
example: "eu.proxy.netbird.io"
|
||||
dedicated:
|
||||
type: boolean
|
||||
description: Whether the account's gateway is served by a proxy dedicated to it (endpoint equals proxy_address).
|
||||
example: false
|
||||
enable_log_collection:
|
||||
type: boolean
|
||||
description: Whether per-request access-log entries are collected for this account's agent-network traffic.
|
||||
@@ -6236,19 +6232,51 @@ components:
|
||||
readOnly: true
|
||||
example: "2026-04-26T10:30:00Z"
|
||||
required:
|
||||
- cluster
|
||||
- subdomain
|
||||
- endpoint
|
||||
- proxy_address
|
||||
- dedicated
|
||||
- enable_log_collection
|
||||
- enable_prompt_collection
|
||||
- redact_pii
|
||||
AgentNetworkSettingsCreateRequest:
|
||||
type: object
|
||||
description: Bootstraps the per-account Agent Network settings row, assigning the account's immutable endpoint. Exactly one of `proxy_address` and `endpoint` must be provided. `proxy_address` requests a labeled endpoint — the server allocates a label and the endpoint becomes `<label>.<proxy_address>`, served by whichever proxy declares that parent address. `endpoint` claims the given hostname itself as a self-addressed (dedicated) endpoint, served only by a proxy declaring exactly that address — the claim is legitimate before the proxy exists (address-first). Collection toggles may ride along; omitted toggles take their defaults.
|
||||
properties:
|
||||
proxy_address:
|
||||
type: string
|
||||
description: Cluster address to allocate a labeled endpoint beneath. Mutually exclusive with `endpoint`.
|
||||
example: "eu.proxy.netbird.io"
|
||||
endpoint:
|
||||
type: string
|
||||
description: Hostname to claim as the account's self-addressed (dedicated) endpoint. Mutually exclusive with `proxy_address`. Rejected when another account already holds it.
|
||||
example: "brave-otter.gateway.example.com"
|
||||
enable_log_collection:
|
||||
type: boolean
|
||||
description: Whether per-request access-log entries are collected for this account's agent-network traffic. Defaults to true.
|
||||
example: true
|
||||
enable_prompt_collection:
|
||||
type: boolean
|
||||
description: Master switch for request/response prompt capture. Defaults to false.
|
||||
example: false
|
||||
redact_pii:
|
||||
type: boolean
|
||||
description: Whether captured prompts have PII redacted. Defaults to false.
|
||||
example: false
|
||||
access_log_retention_days:
|
||||
type: integer
|
||||
description: Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely. Defaults to 30.
|
||||
example: 30
|
||||
AgentNetworkSettingsRequest:
|
||||
type: object
|
||||
description: Account-level Agent Network settings update. The request replaces every mutable field. `cluster` additionally bootstraps the per-account settings row when the account does not have one yet; the subdomain is always server-assigned.
|
||||
description: Account-level Agent Network settings update. Every field is required, matching the PUT convention of the other endpoints. The endpoint and proxy address are assigned at bootstrap (POST) and are immutable — the request must carry them unchanged, and a request carrying different values is rejected. To change them, delete the settings (DELETE, guarded) and bootstrap again; re-creating allocates a new endpoint.
|
||||
properties:
|
||||
cluster:
|
||||
endpoint:
|
||||
type: string
|
||||
description: Address of the NetBird proxy cluster fronting this account's agent-network endpoint. When the account has no settings row yet, providing it bootstraps the row (assigning the subdomain that forms the agent endpoint). The cluster is immutable once assigned — later updates must omit it or send the assigned value; any other value is rejected.
|
||||
description: The account's gateway endpoint hostname. Immutable — must match the assigned value; a different value is rejected.
|
||||
example: "brave-otter.eu.proxy.netbird.io"
|
||||
proxy_address:
|
||||
type: string
|
||||
description: Declared cluster address of the proxy serving this account's gateway. Immutable — must match the assigned value; a different value is rejected.
|
||||
example: "eu.proxy.netbird.io"
|
||||
enable_log_collection:
|
||||
type: boolean
|
||||
@@ -6267,9 +6295,12 @@ components:
|
||||
description: Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely.
|
||||
example: 30
|
||||
required:
|
||||
- endpoint
|
||||
- proxy_address
|
||||
- enable_log_collection
|
||||
- enable_prompt_collection
|
||||
- redact_pii
|
||||
- access_log_retention_days
|
||||
AgentNetworkBudgetRule:
|
||||
type: object
|
||||
description: Account-level budget rule. A limit-only rule bound to groups and/or users that applies across all policies as a min-wins ceiling. Empty targets means it applies to every caller.
|
||||
@@ -13694,7 +13725,7 @@ paths:
|
||||
/api/agent-network/settings:
|
||||
get:
|
||||
summary: Retrieve Agent Network settings
|
||||
description: Returns the per-account Agent Network gateway settings (cluster, subdomain, endpoint). Before the account is bootstrapped — on first provider create (`bootstrap_cluster`) or via PUT with `cluster` — the response carries the default values with empty cluster, subdomain and endpoint.
|
||||
description: Returns the per-account Agent Network gateway settings (endpoint, proxy address, collection toggles). Before the account is bootstrapped via POST, the response carries the default values with an empty endpoint and proxy address.
|
||||
tags: [ Agent Network ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
@@ -13712,9 +13743,42 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
summary: Bootstrap Agent Network settings
|
||||
description: Creates the per-account Agent Network settings row and allocates the account's endpoint. Exactly one of `proxy_address` (labeled endpoint under that cluster; the server allocates the label) and `endpoint` (self-addressed dedicated endpoint, claimed verbatim) must be provided. The endpoint and proxy address are immutable once assigned. Returns 409 when the account already has a settings row.
|
||||
tags: [ Agent Network ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
requestBody:
|
||||
required: true
|
||||
description: Settings bootstrap request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentNetworkSettingsCreateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: The freshly bootstrapped Agent Network settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentNetworkSettings'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'409':
|
||||
"$ref": "#/components/responses/conflict"
|
||||
'422':
|
||||
"$ref": "#/components/responses/validation_failed"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
put:
|
||||
summary: Update Agent Network settings
|
||||
description: Updates the account-level Agent Network settings; the request replaces every mutable field (collection toggles and retention). When the account has no settings row yet, providing `cluster` bootstraps it (assigning the subdomain that forms the agent endpoint); without `cluster` the request returns 404. Sending a `cluster` different from the assigned one is rejected (the cluster is immutable once assigned). The subdomain is always server-assigned and immutable.
|
||||
description: Updates the account-level Agent Network settings; the request carries every field, replacing the mutable ones (collection toggles and retention). Returns 404 when the account has no settings row yet — bootstrap it with POST first. The endpoint and proxy address are assigned at bootstrap and immutable; the request must carry them unchanged, and a request carrying different values is rejected.
|
||||
tags: [ Agent Network ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
@@ -13744,6 +13808,27 @@ paths:
|
||||
"$ref": "#/components/responses/validation_failed"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete Agent Network settings
|
||||
description: Deletes the account's Agent Network settings row, releasing the endpoint. Guarded — the delete is refused with 412 while any Agent Network provider exists for the account or while a proxy is actively serving the endpoint. Bootstrapping again after a delete allocates a new endpoint; the released hostname is not reserved.
|
||||
tags: [ Agent Network ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: Settings deleted
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'404':
|
||||
"$ref": "#/components/responses/not_found"
|
||||
'412':
|
||||
description: Delete refused — Agent Network providers still exist for the account, or a proxy is actively serving the endpoint
|
||||
content: { }
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/agent-network/budget-rules:
|
||||
get:
|
||||
summary: List all Agent Network budget rules
|
||||
|
||||
@@ -2329,9 +2329,6 @@ type AgentNetworkProviderRequest struct {
|
||||
// ApiKey Upstream provider API key. Sealed at rest on the management server and never returned in responses. Required on create; optional on update (omit to keep the existing key).
|
||||
ApiKey *string `json:"api_key,omitempty"`
|
||||
|
||||
// BootstrapCluster Proxy cluster used to bootstrap the per-account agent-network endpoint when the first provider is created. Ignored on subsequent creates and on updates because the cluster is pinned on the account-level Settings row.
|
||||
BootstrapCluster *string `json:"bootstrap_cluster,omitempty"`
|
||||
|
||||
// Enabled Whether the provider is enabled. Defaults to true on create.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
|
||||
@@ -2363,43 +2360,61 @@ type AgentNetworkProviderRequest struct {
|
||||
UpstreamUrl string `json:"upstream_url"`
|
||||
}
|
||||
|
||||
// AgentNetworkSettings Per-account Agent Network gateway settings. One row per account; cluster and subdomain are assigned at bootstrap and immutable thereafter. Before bootstrap the account reads as the default values with empty cluster, subdomain and endpoint.
|
||||
// AgentNetworkSettings Per-account Agent Network gateway settings. One row per account; endpoint and proxy_address are assigned at bootstrap (POST) and immutable thereafter. Before bootstrap the account reads as the default values with empty endpoint and proxy_address.
|
||||
type AgentNetworkSettings struct {
|
||||
// AccessLogRetentionDays Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely. Usage records are retained independently.
|
||||
AccessLogRetentionDays *int `json:"access_log_retention_days,omitempty"`
|
||||
|
||||
// Cluster Address of the NetBird proxy cluster fronting this account's agent-network endpoint. Empty until the account is bootstrapped.
|
||||
Cluster string `json:"cluster"`
|
||||
|
||||
// CreatedAt Timestamp when the settings row was created. Absent until the account is bootstrapped.
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
|
||||
// Dedicated Whether the account's gateway is served by a proxy dedicated to it (endpoint equals proxy_address).
|
||||
Dedicated bool `json:"dedicated"`
|
||||
|
||||
// EnableLogCollection Whether per-request access-log entries are collected for this account's agent-network traffic.
|
||||
EnableLogCollection bool `json:"enable_log_collection"`
|
||||
|
||||
// EnablePromptCollection Master switch for request/response prompt capture. Capture runs only when this is on AND a policy guardrail also enables it.
|
||||
EnablePromptCollection bool `json:"enable_prompt_collection"`
|
||||
|
||||
// Endpoint Bare hostname agents call for this account, computed as `<subdomain>.<cluster>`. Empty until the account is bootstrapped.
|
||||
// Endpoint Bare hostname agents call for this account. Empty until the account is bootstrapped.
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// ProxyAddress Declared cluster address of the proxy serving this account's gateway. Equal to `endpoint` when a dedicated proxy serves the account; otherwise the endpoint's immediate parent (a shared cluster the endpoint hangs one label beneath). Empty until the account is bootstrapped.
|
||||
ProxyAddress string `json:"proxy_address"`
|
||||
|
||||
// RedactPii Whether captured prompts have PII redacted. Effective redaction is the OR of this and any policy guardrail's redact setting.
|
||||
RedactPii bool `json:"redact_pii"`
|
||||
|
||||
// Subdomain Auto-generated DNS-safe label that prefixes the cluster to form the agent-network endpoint. Empty until the account is bootstrapped.
|
||||
Subdomain string `json:"subdomain"`
|
||||
|
||||
// UpdatedAt Timestamp when the settings row was last updated. Absent until the account is bootstrapped.
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkSettingsRequest Account-level Agent Network settings update. The request replaces every mutable field. `cluster` additionally bootstraps the per-account settings row when the account does not have one yet; the subdomain is always server-assigned.
|
||||
type AgentNetworkSettingsRequest struct {
|
||||
// AccessLogRetentionDays Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely.
|
||||
// AgentNetworkSettingsCreateRequest Bootstraps the per-account Agent Network settings row, assigning the account's immutable endpoint. Exactly one of `proxy_address` and `endpoint` must be provided. `proxy_address` requests a labeled endpoint — the server allocates a label and the endpoint becomes `<label>.<proxy_address>`, served by whichever proxy declares that parent address. `endpoint` claims the given hostname itself as a self-addressed (dedicated) endpoint, served only by a proxy declaring exactly that address — the claim is legitimate before the proxy exists (address-first). Collection toggles may ride along; omitted toggles take their defaults.
|
||||
type AgentNetworkSettingsCreateRequest struct {
|
||||
// AccessLogRetentionDays Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely. Defaults to 30.
|
||||
AccessLogRetentionDays *int `json:"access_log_retention_days,omitempty"`
|
||||
|
||||
// Cluster Address of the NetBird proxy cluster fronting this account's agent-network endpoint. When the account has no settings row yet, providing it bootstraps the row (assigning the subdomain that forms the agent endpoint). The cluster is immutable once assigned — later updates must omit it or send the assigned value; any other value is rejected.
|
||||
Cluster *string `json:"cluster,omitempty"`
|
||||
// EnableLogCollection Whether per-request access-log entries are collected for this account's agent-network traffic. Defaults to true.
|
||||
EnableLogCollection *bool `json:"enable_log_collection,omitempty"`
|
||||
|
||||
// EnablePromptCollection Master switch for request/response prompt capture. Defaults to false.
|
||||
EnablePromptCollection *bool `json:"enable_prompt_collection,omitempty"`
|
||||
|
||||
// Endpoint Hostname to claim as the account's self-addressed (dedicated) endpoint. Mutually exclusive with `proxy_address`. Rejected when another account already holds it.
|
||||
Endpoint *string `json:"endpoint,omitempty"`
|
||||
|
||||
// ProxyAddress Cluster address to allocate a labeled endpoint beneath. Mutually exclusive with `endpoint`.
|
||||
ProxyAddress *string `json:"proxy_address,omitempty"`
|
||||
|
||||
// RedactPii Whether captured prompts have PII redacted. Defaults to false.
|
||||
RedactPii *bool `json:"redact_pii,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkSettingsRequest Account-level Agent Network settings update. Every field is required, matching the PUT convention of the other endpoints. The endpoint and proxy address are assigned at bootstrap (POST) and are immutable — the request must carry them unchanged, and a request carrying different values is rejected. To change them, delete the settings (DELETE, guarded) and bootstrap again; re-creating allocates a new endpoint.
|
||||
type AgentNetworkSettingsRequest struct {
|
||||
// AccessLogRetentionDays Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely.
|
||||
AccessLogRetentionDays int `json:"access_log_retention_days"`
|
||||
|
||||
// EnableLogCollection Whether per-request access-log entries are collected for this account's agent-network traffic.
|
||||
EnableLogCollection bool `json:"enable_log_collection"`
|
||||
@@ -2407,6 +2422,12 @@ type AgentNetworkSettingsRequest struct {
|
||||
// EnablePromptCollection Master switch for request/response prompt capture.
|
||||
EnablePromptCollection bool `json:"enable_prompt_collection"`
|
||||
|
||||
// Endpoint The account's gateway endpoint hostname. Immutable — must match the assigned value; a different value is rejected.
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// ProxyAddress Declared cluster address of the proxy serving this account's gateway. Immutable — must match the assigned value; a different value is rejected.
|
||||
ProxyAddress string `json:"proxy_address"`
|
||||
|
||||
// RedactPii Whether captured prompts have PII redacted.
|
||||
RedactPii bool `json:"redact_pii"`
|
||||
}
|
||||
@@ -6176,6 +6197,9 @@ type PostApiAgentNetworkProvidersJSONRequestBody = AgentNetworkProviderRequest
|
||||
// PutApiAgentNetworkProvidersProviderIdJSONRequestBody defines body for PutApiAgentNetworkProvidersProviderId for application/json ContentType.
|
||||
type PutApiAgentNetworkProvidersProviderIdJSONRequestBody = AgentNetworkProviderRequest
|
||||
|
||||
// PostApiAgentNetworkSettingsJSONRequestBody defines body for PostApiAgentNetworkSettings for application/json ContentType.
|
||||
type PostApiAgentNetworkSettingsJSONRequestBody = AgentNetworkSettingsCreateRequest
|
||||
|
||||
// PutApiAgentNetworkSettingsJSONRequestBody defines body for PutApiAgentNetworkSettings for application/json ContentType.
|
||||
type PutApiAgentNetworkSettingsJSONRequestBody = AgentNetworkSettingsRequest
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -110,6 +110,10 @@ message BundleParameters {
|
||||
int64 bundle_for_time = 2;
|
||||
int32 log_file_count = 3;
|
||||
bool anonymize = 4;
|
||||
// anonymize_level selects how much the anonymizer redacts: "default"
|
||||
// (or empty) keeps internal IP ranges, "strict" also anonymizes them.
|
||||
// Unknown values are treated as "strict".
|
||||
string anonymize_level = 5;
|
||||
}
|
||||
|
||||
message BundleResult {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/logging"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbnet "github.com/netbirdio/netbird/client/net"
|
||||
@@ -80,28 +79,6 @@ func (d Dialer) Dial(ctx context.Context, address, serverName string) (net.Conn,
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// connectionTracer returns a QUIC tracer that logs the DPLPMTUD result and the
|
||||
// reason a relay connection closed, so the path MTU settled on and teardown
|
||||
// cause are visible in logs. Lines carry the relay address as a structured
|
||||
// field, matching the rest of the relay client logging.
|
||||
func connectionTracer(addr string) func(context.Context, logging.Perspective, quic.ConnectionID) *logging.ConnectionTracer {
|
||||
relayLog := log.WithField("relay", addr)
|
||||
return func(context.Context, logging.Perspective, quic.ConnectionID) *logging.ConnectionTracer {
|
||||
return &logging.ConnectionTracer{
|
||||
UpdatedMTU: func(mtu logging.ByteCount, done bool) {
|
||||
if done {
|
||||
relayLog.Infof("QUIC path MTU settled at %d", mtu)
|
||||
return
|
||||
}
|
||||
relayLog.Debugf("QUIC path MTU probing at %d", mtu)
|
||||
},
|
||||
ClosedConnection: func(err error) {
|
||||
relayLog.Debugf("QUIC connection closed: %v", err)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareURL(address string) (string, error) {
|
||||
var host string
|
||||
var defaultPort string
|
||||
|
||||
145
shared/relay/client/dialer/quic/quic_test.go
Normal file
145
shared/relay/client/dialer/quic/quic_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/quic-go/quic-go/qlog"
|
||||
"github.com/quic-go/quic-go/qlogwriter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
)
|
||||
|
||||
func TestCloseReason(t *testing.T) {
|
||||
transportErr := qlog.TransportErrorCode(0x2) // CONNECTION_REFUSED
|
||||
appErr := qlog.ApplicationErrorCode(42)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event qlog.ConnectionClosed
|
||||
want string
|
||||
}{
|
||||
{
|
||||
// A close carrying nothing but an initiator still reads sensibly.
|
||||
name: "initiator only",
|
||||
event: qlog.ConnectionClosed{Initiator: qlog.InitiatorLocal},
|
||||
want: "closed by local",
|
||||
},
|
||||
{
|
||||
name: "transport error with trigger",
|
||||
event: qlog.ConnectionClosed{
|
||||
Initiator: qlog.InitiatorRemote,
|
||||
ConnectionError: &transportErr,
|
||||
Trigger: qlog.ConnectionCloseTriggerIdleTimeout,
|
||||
},
|
||||
want: "closed by remote, transport error: CONNECTION_REFUSED, trigger: idle_timeout",
|
||||
},
|
||||
{
|
||||
name: "application error with reason",
|
||||
event: qlog.ConnectionClosed{
|
||||
Initiator: qlog.InitiatorLocal,
|
||||
ApplicationError: &appErr,
|
||||
Reason: "bye",
|
||||
},
|
||||
want: "closed by local, application error: 42, reason: bye",
|
||||
},
|
||||
{
|
||||
// Transport and application errors are mutually exclusive in
|
||||
// practice; if both are set the transport code wins.
|
||||
name: "transport error takes precedence over application error",
|
||||
event: qlog.ConnectionClosed{
|
||||
Initiator: qlog.InitiatorLocal,
|
||||
ConnectionError: &transportErr,
|
||||
ApplicationError: &appErr,
|
||||
},
|
||||
want: "closed by local, transport error: CONNECTION_REFUSED",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := closeReason(tt.event); got != tt.want {
|
||||
t.Errorf("closeReason() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogSinkRecordEvent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
event qlogwriter.Event
|
||||
wantLevel log.Level
|
||||
wantMsg string
|
||||
}{
|
||||
{
|
||||
name: "settled MTU is logged at info",
|
||||
event: qlog.MTUUpdated{Value: 1400, Done: true},
|
||||
wantLevel: log.InfoLevel,
|
||||
wantMsg: "QUIC path MTU settled at 1400",
|
||||
},
|
||||
{
|
||||
// Probing fires repeatedly during discovery, so it stays at debug.
|
||||
name: "MTU probe is logged at debug",
|
||||
event: qlog.MTUUpdated{Value: 1300, Done: false},
|
||||
wantLevel: log.DebugLevel,
|
||||
wantMsg: "QUIC path MTU probing at 1300",
|
||||
},
|
||||
{
|
||||
name: "connection closed is logged at debug",
|
||||
event: qlog.ConnectionClosed{Initiator: qlog.InitiatorRemote},
|
||||
wantLevel: log.DebugLevel,
|
||||
wantMsg: "QUIC connection closed: closed by remote",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger, hook := test.NewNullLogger()
|
||||
logger.SetLevel(log.DebugLevel)
|
||||
recorder := logSink{log: logger.WithField("relay", "relay.example.com:443")}
|
||||
|
||||
recorder.RecordEvent(tt.event)
|
||||
|
||||
entries := hook.AllEntries()
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("got %d log entries, want 1", len(entries))
|
||||
}
|
||||
if entries[0].Level != tt.wantLevel {
|
||||
t.Errorf("level = %v, want %v", entries[0].Level, tt.wantLevel)
|
||||
}
|
||||
if entries[0].Message != tt.wantMsg {
|
||||
t.Errorf("message = %q, want %q", entries[0].Message, tt.wantMsg)
|
||||
}
|
||||
if relay := entries[0].Data["relay"]; relay != "relay.example.com:443" {
|
||||
t.Errorf("relay field = %v, want relay.example.com:443", relay)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Events the relay client does not care about must not produce log lines.
|
||||
func TestLogSinkIgnoresUnhandledEvents(t *testing.T) {
|
||||
logger, hook := test.NewNullLogger()
|
||||
logger.SetLevel(log.DebugLevel)
|
||||
recorder := logSink{log: logger.WithField("relay", "relay.example.com:443")}
|
||||
|
||||
recorder.RecordEvent(qlog.PacketLost{})
|
||||
|
||||
if entries := hook.AllEntries(); len(entries) != 0 {
|
||||
t.Errorf("got %d log entries, want 0", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogSinkSupportsSchemas(t *testing.T) {
|
||||
trace := logSink{log: log.WithField("relay", "relay.example.com:443")}
|
||||
|
||||
if !trace.SupportsSchemas(qlog.EventSchema) {
|
||||
t.Errorf("SupportsSchemas(%q) = false, want true", qlog.EventSchema)
|
||||
}
|
||||
if trace.SupportsSchemas("urn:ietf:params:qlog:events:http3-12") {
|
||||
t.Error("SupportsSchemas() = true for an unrelated schema, want false")
|
||||
}
|
||||
if trace.AddProducer() == nil {
|
||||
t.Error("AddProducer() = nil, want a recorder")
|
||||
}
|
||||
}
|
||||
70
shared/relay/client/dialer/quic/tracer.go
Normal file
70
shared/relay/client/dialer/quic/tracer.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/qlog"
|
||||
"github.com/quic-go/quic-go/qlogwriter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// logSink implements both qlogwriter.Trace and qlogwriter.Recorder, forwarding
|
||||
// the few qlog events the relay client cares about to logrus instead of
|
||||
// writing a qlog file. It holds no mutable state and logrus entries are safe
|
||||
// to share, so one value can serve every producer on the connection.
|
||||
type logSink struct {
|
||||
log *log.Entry
|
||||
}
|
||||
|
||||
func (s logSink) AddProducer() qlogwriter.Recorder { return s }
|
||||
|
||||
func (s logSink) SupportsSchemas(schema string) bool { return schema == qlog.EventSchema }
|
||||
|
||||
func (s logSink) RecordEvent(event qlogwriter.Event) {
|
||||
switch e := event.(type) {
|
||||
case qlog.MTUUpdated:
|
||||
if e.Done {
|
||||
s.log.Infof("QUIC path MTU settled at %d", e.Value)
|
||||
return
|
||||
}
|
||||
s.log.Debugf("QUIC path MTU probing at %d", e.Value)
|
||||
case qlog.ConnectionClosed:
|
||||
s.log.Debugf("QUIC connection closed: %s", closeReason(e))
|
||||
}
|
||||
}
|
||||
|
||||
func (s logSink) Close() error { return nil }
|
||||
|
||||
// connectionTracer returns a QUIC tracer that logs the DPLPMTUD result and the
|
||||
// reason a relay connection closed, so the path MTU settled on and teardown
|
||||
// cause are visible in logs. Lines carry the relay address as a structured
|
||||
// field, matching the rest of the relay client logging.
|
||||
func connectionTracer(addr string) func(context.Context, bool, quic.ConnectionID) qlogwriter.Trace {
|
||||
relayLog := log.WithField("relay", addr)
|
||||
return func(context.Context, bool, quic.ConnectionID) qlogwriter.Trace {
|
||||
return logSink{log: relayLog}
|
||||
}
|
||||
}
|
||||
|
||||
// closeReason renders a ConnectionClosed event as a single line. The event
|
||||
// carries the error as separate initiator, code, trigger and reason fields,
|
||||
// any of which may be unset.
|
||||
func closeReason(e qlog.ConnectionClosed) string {
|
||||
parts := []string{fmt.Sprintf("closed by %s", e.Initiator)}
|
||||
switch {
|
||||
case e.ConnectionError != nil:
|
||||
parts = append(parts, fmt.Sprintf("transport error: %s", *e.ConnectionError))
|
||||
case e.ApplicationError != nil:
|
||||
parts = append(parts, fmt.Sprintf("application error: %d", *e.ApplicationError))
|
||||
}
|
||||
if e.Trigger != "" {
|
||||
parts = append(parts, fmt.Sprintf("trigger: %s", e.Trigger))
|
||||
}
|
||||
if e.Reason != "" {
|
||||
parts = append(parts, fmt.Sprintf("reason: %s", e.Reason))
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user