Merge branch 'main' into net-1411-macos-menu-bar-welcome

This commit is contained in:
Eduard Gert
2026-07-20 14:53:50 +02:00
committed by GitHub
23 changed files with 1604 additions and 296 deletions

View File

@@ -8,6 +8,7 @@
{"code": "fr", "displayName": "Français", "englishName": "French"},
{"code": "it", "displayName": "Italiano", "englishName": "Italian"},
{"code": "pt", "displayName": "Português", "englishName": "Portuguese"},
{"code": "zh-CN", "displayName": "简体中文", "englishName": "Simplified Chinese"}
{"code": "zh-CN", "displayName": "简体中文", "englishName": "Simplified Chinese"},
{"code": "ja", "displayName": "日本語", "englishName": "Japanese"}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -226,7 +226,7 @@ func (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool
}
hashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))
authenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)
authenticator := auth.NewTimedHMACValidator(hashedSecret[:])
relayCfg := relayServer.Config{
Meter: s.metricsServer.Meter,

View File

@@ -197,6 +197,12 @@ type JSONMetadataInjection struct {
// enforces a 128-char limit per value; oversized values are
// truncated rather than failing the request. 0 disables the cap.
MaxValueLength int
// Sanitize, when true, replaces characters outside the destination's
// accepted set with '_' before emitting each value. AWS Bedrock's
// X-Amzn-Bedrock-Request-Metadata restricts values to a limited character
// class, so unsanitized group display names (e.g. containing spaces) would
// make Bedrock reject the request with 400.
Sanitize bool
}
// providers is the canonical list of supported Agent Network providers.
@@ -329,6 +335,18 @@ var providers = []Provider{
{ID: "amazon.nova-lite", Label: "Amazon Nova Lite (Bedrock)", InputPer1k: 0.00006, OutputPer1k: 0.00024, ContextWindow: 300000},
{ID: "amazon.nova-micro", Label: "Amazon Nova Micro (Bedrock)", InputPer1k: 0.000035, OutputPer1k: 0.00014, ContextWindow: 128000},
},
// Bedrock accepts a cost-allocation metadata header; stamp the caller's
// user + authorizing group so spend can be attributed in AWS Cost
// Management. Sanitized because Bedrock restricts the value character set.
IdentityInjection: &IdentityInjection{
JSONMetadata: &JSONMetadataInjection{
Header: "X-Amzn-Bedrock-Request-Metadata",
UserKey: "user",
GroupsKey: "group",
MaxValueLength: 256,
Sanitize: true,
},
},
},
{
ID: "vertex_ai_api",

View File

@@ -540,6 +540,7 @@ type identityInjectJSONMetadata struct {
UserKey string `json:"user_key,omitempty"`
GroupsKey string `json:"groups_key,omitempty"`
MaxValueLength int `json:"max_value_length,omitempty"`
Sanitize bool `json:"sanitize,omitempty"`
}
// buildIdentityInjectConfigJSON walks the enabled providers and emits
@@ -583,9 +584,11 @@ func buildIdentityInjectConfigJSON(providers []*types.Provider, groupIndex map[s
func buildIdentityInjectRule(p *types.Provider, entry catalog.Provider) (identityInjectProvider, bool) {
rule := identityInjectProvider{ProviderID: p.ID}
// Identity-stamping shape (one of HeaderPair / JSONMetadata). Skip the
// shape silently when the catalog entry doesn't declare one — extras
// can still apply, see below.
if entry.IdentityInjection != nil {
// shape silently when the catalog entry doesn't declare one, or when the
// operator disabled metadata for this provider — extras can still apply,
// see below. MetadataDisabled suppresses only the identity dimensions
// (user + authorizing group), not the catalog's routing ExtraHeaders.
if !p.MetadataDisabled && entry.IdentityInjection != nil {
switch {
case entry.IdentityInjection.HeaderPair != nil:
rule.HeaderPair = buildIdentityHeaderPair(p, entry.IdentityInjection.HeaderPair)
@@ -651,6 +654,7 @@ func buildIdentityJSONMetadata(p *types.Provider, jm *catalog.JSONMetadataInject
UserKey: userKey,
GroupsKey: groupsKey,
MaxValueLength: jm.MaxValueLength,
Sanitize: jm.Sanitize,
}
}

View File

@@ -698,6 +698,94 @@ func TestSynthesizeServices_IdentityInject_Portkey_NotCustomizable(t *testing.T)
"same fixed-schema guarantee for the groups dimension")
}
// TestSynthesizeServices_IdentityInject_Bedrock pins Bedrock's cost-allocation
// metadata: a JSONMetadata shape emitting X-Amzn-Bedrock-Request-Metadata with
// the reserved user/group keys, sanitized to Bedrock's accepted charset.
func TestSynthesizeServices_IdentityInject_Bedrock(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStore := store.NewMockStore(ctrl)
br := newSynthTestProvider()
br.ID = "prov-bedrock"
br.ProviderID = "bedrock_api"
br.UpstreamURL = "https://bedrock-runtime.us-east-1.amazonaws.com"
br.APIKey = "bedrock-bearer"
br.CreatedAt = time.Date(2026, 4, 2, 0, 0, 0, 0, time.UTC)
policy := newSynthTestPolicy(br.ID, "grp-eng", "")
policy.ID = "pol-bedrock"
expectSynthBaseInputs(mockStore, ctx, newSynthTestSettings(),
[]*types.Provider{br},
[]*types.Policy{policy},
[]*types.Guardrail{})
services, err := SynthesizeServices(ctx, mockStore, testAccountID)
require.NoError(t, err)
require.Len(t, services, 1)
var injectCfg identityInjectConfig
for _, m := range services[0].Targets[0].Options.Middlewares {
if m.ID == middlewareIDLLMIdentityInject {
require.NoError(t, json.Unmarshal(m.ConfigJSON, &injectCfg))
break
}
}
require.Len(t, injectCfg.Providers, 1)
entry := injectCfg.Providers[0]
require.NotNil(t, entry.JSONMetadata, "Bedrock uses the JSONMetadata shape for cost-allocation metadata")
assert.Nil(t, entry.HeaderPair, "shapes are mutually exclusive")
assert.Equal(t, "X-Amzn-Bedrock-Request-Metadata", entry.JSONMetadata.Header,
"the caller identity lands in Bedrock's cost-allocation metadata header")
assert.Equal(t, "user", entry.JSONMetadata.UserKey)
assert.Equal(t, "group", entry.JSONMetadata.GroupsKey)
assert.True(t, entry.JSONMetadata.Sanitize,
"Bedrock restricts the metadata value charset, so values must be sanitized")
}
// TestSynthesizeServices_MetadataDisabled_SuppressesInjection verifies the
// per-provider opt-out: a provider with MetadataDisabled set emits no
// identity-inject entry (Bedrock has no catalog ExtraHeaders, so the whole
// entry is dropped).
func TestSynthesizeServices_MetadataDisabled_SuppressesInjection(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStore := store.NewMockStore(ctrl)
br := newSynthTestProvider()
br.ID = "prov-bedrock"
br.ProviderID = "bedrock_api"
br.UpstreamURL = "https://bedrock-runtime.us-east-1.amazonaws.com"
br.APIKey = "bedrock-bearer"
br.MetadataDisabled = true
br.CreatedAt = time.Date(2026, 4, 2, 0, 0, 0, 0, time.UTC)
policy := newSynthTestPolicy(br.ID, "grp-eng", "")
policy.ID = "pol-bedrock"
expectSynthBaseInputs(mockStore, ctx, newSynthTestSettings(),
[]*types.Provider{br},
[]*types.Policy{policy},
[]*types.Guardrail{})
services, err := SynthesizeServices(ctx, mockStore, testAccountID)
require.NoError(t, err)
require.Len(t, services, 1)
var injectCfg identityInjectConfig
for _, m := range services[0].Targets[0].Options.Middlewares {
if m.ID == middlewareIDLLMIdentityInject {
require.NoError(t, json.Unmarshal(m.ConfigJSON, &injectCfg))
break
}
}
assert.Empty(t, injectCfg.Providers,
"metadata_disabled must drop the provider's identity-inject entry")
}
// TestSynthesizeServices_IdentityInject_Vercel pins Vercel AI
// Gateway's wiring: HeaderPair shape with fixed wire names dictated
// by Vercel's Custom Reporting API (ai-reporting-user /

View File

@@ -51,6 +51,12 @@ type Provider struct {
// private or self-signed certificate. The synthesiser propagates it into
// the router route so the proxy dials that provider's upstream insecurely.
SkipTLSVerification bool `gorm:"column:skip_tls_verification"`
// MetadataDisabled suppresses identity metadata injection for this provider.
// Metadata (the caller's user + authorizing group) is injected by default;
// when true the synthesiser omits the provider's identity-inject shape, so no
// user/group headers (e.g. Bedrock's X-Amzn-Bedrock-Request-Metadata) are
// stamped. Catalog ExtraHeaders (routing config) are unaffected.
MetadataDisabled bool `gorm:"column:metadata_disabled"`
// SessionPrivateKey + SessionPublicKey are the ed25519 keypair the
// synthesised reverse-proxy service uses to sign / verify session
// JWTs after a successful OIDC handshake. Generated once on
@@ -137,6 +143,9 @@ func (p *Provider) FromAPIRequest(req *api.AgentNetworkProviderRequest) {
if req.SkipTlsVerification != nil {
p.SkipTLSVerification = *req.SkipTlsVerification
}
if req.MetadataDisabled != nil {
p.MetadataDisabled = *req.MetadataDisabled
}
// Identity-header overrides for catalogs flagged Customizable.
// nil pointer = "field omitted on the wire" → leave the stored
// value untouched (per the openapi description). Empty string is
@@ -170,6 +179,7 @@ func (p *Provider) ToAPIResponse() *api.AgentNetworkProvider {
Models: models,
Enabled: p.Enabled,
SkipTlsVerification: p.SkipTLSVerification,
MetadataDisabled: p.MetadataDisabled,
CreatedAt: &created,
UpdatedAt: &updated,
}

View File

@@ -42,3 +42,38 @@ func TestProvider_SkipTLSVerification_RoundTrip(t *testing.T) {
assert.False(t, p.SkipTLSVerification, "explicit false must clear skip_tls_verification")
assert.False(t, p.ToAPIResponse().SkipTlsVerification, "response must reflect the cleared value")
}
// TestProvider_MetadataDisabled_RoundTrip covers the request→provider→response
// mapping of metadata_disabled, with the same update semantics: nil preserves,
// explicit false clears.
func TestProvider_MetadataDisabled_RoundTrip(t *testing.T) {
enable := true
disable := false
base := func() *api.AgentNetworkProviderRequest {
return &api.AgentNetworkProviderRequest{
ProviderId: "bedrock_api",
Name: "bedrock",
UpstreamUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
}
}
p := NewProvider("acc-1")
req := base()
req.MetadataDisabled = &enable
p.FromAPIRequest(req)
assert.True(t, p.MetadataDisabled, "create with metadata_disabled=true must set the field")
assert.True(t, p.ToAPIResponse().MetadataDisabled, "response must surface metadata_disabled")
// Omitting the field on update leaves the stored value untouched.
p.FromAPIRequest(base())
assert.True(t, p.MetadataDisabled, "omitting metadata_disabled on update must preserve it")
// Explicit false clears it (re-enables metadata).
req = base()
req.MetadataDisabled = &disable
p.FromAPIRequest(req)
assert.False(t, p.MetadataDisabled, "explicit false must clear metadata_disabled")
assert.False(t, p.ToAPIResponse().MetadataDisabled, "response must reflect the cleared value")
}

View File

@@ -64,6 +64,11 @@ type JSONMetadataRule struct {
UserKey string `json:"user_key,omitempty"`
GroupsKey string `json:"groups_key,omitempty"`
MaxValueLength int `json:"max_value_length,omitempty"`
// Sanitize replaces characters outside the destination provider's accepted
// set with '_' before emitting each value. AWS Bedrock's
// X-Amzn-Bedrock-Request-Metadata restricts values to [A-Za-z0-9 +-=._:/@];
// group display names with other characters would otherwise 400.
Sanitize bool `json:"sanitize,omitempty"`
}
// Config is the on-wire configuration accepted by the factory. An

View File

@@ -292,15 +292,21 @@ func applyJSONMetadata(rule *JSONMetadataRule, in *middleware.Input) *middleware
mutations := &middleware.Mutations{}
mutations.HeadersRemove = append(mutations.HeadersRemove, rule.Header)
emit := func(v string) string {
if rule.Sanitize {
v = sanitizeMetadataValue(v)
}
return truncate(v, rule.MaxValueLength)
}
payload := map[string]string{}
if rule.UserKey != "" {
if identity := identityFor(in); identity != "" {
payload[rule.UserKey] = truncate(identity, rule.MaxValueLength)
payload[rule.UserKey] = emit(identity)
}
}
if rule.GroupsKey != "" {
if csv := authorisingTagsCSV(in); csv != "" {
payload[rule.GroupsKey] = truncate(csv, rule.MaxValueLength)
payload[rule.GroupsKey] = emit(csv)
}
}
if len(payload) == 0 {
@@ -359,6 +365,36 @@ func truncate(s string, maxBytes int) string {
return s[:maxBytes]
}
// sanitizeMetadataValue replaces any character outside AWS Bedrock's accepted
// request-metadata class — letters, digits, space, and + - = . _ : / @ — with
// '_'. This keeps values (notably the groups CSV, whose commas are rejected, and
// group display names with arbitrary characters) from making Bedrock reject the
// request with 400. The result stays opaque to the gateway.
func sanitizeMetadataValue(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if metadataCharAllowed(r) {
b.WriteRune(r)
} else {
b.WriteByte('_')
}
}
return b.String()
}
func metadataCharAllowed(r rune) bool {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
return true
}
switch r {
case ' ', '+', '-', '=', '.', '_', ':', '/', '@':
return true
}
return false
}
// tagsIDsFromAuthorising reads llm_router's authorising-groups metadata
// (a CSV of group ids) and returns the parsed slice. Returns nil when
// the key is absent or empty so the caller can fall back to the full

View File

@@ -304,6 +304,46 @@ func TestInject_JSONMetadata_TruncatesValues(t *testing.T) {
"per-value byte length must be capped at MaxValueLength")
}
// TestInject_JSONMetadata_Sanitize pins the AWS-Bedrock sanitization path: when
// Sanitize is set, characters outside Bedrock's accepted metadata class
// (notably the groups CSV comma and arbitrary characters in group display
// names) are replaced with '_' so Bedrock doesn't reject the request. Allowed
// characters (letters, digits, spaces, and @ . _ : / + - =) pass through.
func TestInject_JSONMetadata_Sanitize(t *testing.T) {
rule := ProviderInjection{
ProviderID: portkeyProvider,
JSONMetadata: &JSONMetadataRule{
Header: "X-Amzn-Bedrock-Request-Metadata",
UserKey: "user",
GroupsKey: "group",
MaxValueLength: 256,
Sanitize: true,
},
}
mw := New(Config{Providers: []ProviderInjection{rule}})
in := newInput(portkeyProvider, "alice", []string{"g1", "g2"})
in.UserEmail = "alice@example.com"
// Group display names carry characters Bedrock rejects (comma, '#'); the CSV
// join adds another comma between the two groups.
in.UserGroupNames = []string{"Eng,Team", "Ops#1"}
out, err := mw.Invoke(context.Background(), in)
require.NoError(t, err)
require.NotNil(t, out.Mutations)
require.Len(t, out.Mutations.HeadersAdd, 1)
added := out.Mutations.HeadersAdd[0]
assert.Equal(t, "X-Amzn-Bedrock-Request-Metadata", added.Key,
"the Bedrock cost-allocation header carries the metadata JSON")
var payload map[string]string
require.NoError(t, json.Unmarshal([]byte(added.Value), &payload))
assert.Equal(t, "alice@example.com", payload["user"],
"'@' and '.' are in Bedrock's accepted set and must be preserved")
assert.NotContains(t, payload["group"], ",", "commas must be sanitized — Bedrock rejects them")
assert.NotContains(t, payload["group"], "#", "disallowed characters must be sanitized")
assert.Contains(t, payload["group"], "Eng", "allowed characters must be preserved")
}
// TestInject_JSONMetadata_EmptyIdentity_StripsButDoesNotAdd verifies the
// anti-spoof Remove still fires when there's nothing to stamp.
func TestInject_JSONMetadata_EmptyIdentity_StripsButDoesNotAdd(t *testing.T) {

View File

@@ -173,7 +173,7 @@ func execute(cmd *cobra.Command, args []string) error {
}
hashedSecret := sha256.Sum256([]byte(cobraConfig.AuthSecret))
authenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)
authenticator := auth.NewTimedHMACValidator(hashedSecret[:])
cfg := server.Config{
Meter: metricsServer.Meter,

View File

@@ -5,14 +5,8 @@ import (
"fmt"
"time"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/relay/server/listener"
"github.com/netbirdio/netbird/shared/relay/messages"
//nolint:staticcheck
"github.com/netbirdio/netbird/shared/relay/messages/address"
//nolint:staticcheck
authmsg "github.com/netbirdio/netbird/shared/relay/messages/auth"
)
const (
@@ -23,55 +17,30 @@ const (
type Validator interface {
Validate(any) error
// Deprecated: Use Validate instead.
ValidateHelloMsgType(any) error
}
// preparedMsg contains the marshalled success response messages
// preparedMsg contains the marshalled success response message
type preparedMsg struct {
responseHelloMsg []byte
responseAuthMsg []byte
responseAuthMsg []byte
}
func newPreparedMsg(instanceURL string) (*preparedMsg, error) {
rhm, err := marshalResponseHelloMsg(instanceURL)
if err != nil {
return nil, err
}
ram, err := messages.MarshalAuthResponse(instanceURL)
if err != nil {
return nil, fmt.Errorf("failed to marshal auth response msg: %w", err)
}
return &preparedMsg{
responseHelloMsg: rhm,
responseAuthMsg: ram,
responseAuthMsg: ram,
}, nil
}
func marshalResponseHelloMsg(instanceURL string) ([]byte, error) {
addr := &address.Address{URL: instanceURL}
addrData, err := addr.Marshal()
if err != nil {
return nil, fmt.Errorf("failed to marshal response address: %w", err)
}
//nolint:staticcheck
responseMsg, err := messages.MarshalHelloResponse(addrData)
if err != nil {
return nil, fmt.Errorf("failed to marshal hello response: %w", err)
}
return responseMsg, nil
}
type handshake struct {
conn listener.Conn
validator Validator
preparedMsg *preparedMsg
handshakeMethodAuth bool
peerID *messages.PeerID
peerID *messages.PeerID
}
func (h *handshake) handshakeReceive(ctx context.Context) (*messages.PeerID, error) {
@@ -93,17 +62,11 @@ func (h *handshake) handshakeReceive(ctx context.Context) (*messages.PeerID, err
return nil, fmt.Errorf("determine message type from %s: %w", h.conn.RemoteAddr(), err)
}
var peerID *messages.PeerID
switch msgType {
//nolint:staticcheck
case messages.MsgTypeHello:
peerID, err = h.handleHelloMsg(buf)
case messages.MsgTypeAuth:
h.handshakeMethodAuth = true
peerID, err = h.handleAuthMsg(buf)
default:
if msgType != messages.MsgTypeAuth {
return nil, fmt.Errorf("invalid message type %d from %s", msgType, h.conn.RemoteAddr())
}
peerID, err := h.handleAuthMsg(buf)
if err != nil {
return peerID, err
}
@@ -112,46 +75,17 @@ func (h *handshake) handshakeReceive(ctx context.Context) (*messages.PeerID, err
}
func (h *handshake) handshakeResponse(ctx context.Context) error {
var responseMsg []byte
if h.handshakeMethodAuth {
responseMsg = h.preparedMsg.responseAuthMsg
} else {
responseMsg = h.preparedMsg.responseHelloMsg
}
if _, err := h.conn.Write(ctx, responseMsg); err != nil {
if _, err := h.conn.Write(ctx, h.preparedMsg.responseAuthMsg); err != nil {
return fmt.Errorf("handshake response write to %s (%s): %w", h.peerID, h.conn.RemoteAddr(), err)
}
return nil
}
func (h *handshake) handleHelloMsg(buf []byte) (*messages.PeerID, error) {
//nolint:staticcheck
peerID, authData, err := messages.UnmarshalHelloMsg(buf)
if err != nil {
return nil, fmt.Errorf("unmarshal hello message: %w", err)
}
log.Warnf("peer %s (%s) is using deprecated initial message type", peerID, h.conn.RemoteAddr())
authMsg, err := authmsg.UnmarshalMsg(authData)
if err != nil {
return nil, fmt.Errorf("unmarshal auth message: %w", err)
}
//nolint:staticcheck
if err := h.validator.ValidateHelloMsgType(authMsg.AdditionalData); err != nil {
return nil, fmt.Errorf("validate %s (%s): %w", peerID, h.conn.RemoteAddr(), err)
}
return peerID, nil
}
func (h *handshake) handleAuthMsg(buf []byte) (*messages.PeerID, error) {
rawPeerID, authPayload, err := messages.UnmarshalAuthMsg(buf)
if err != nil {
return nil, fmt.Errorf("unmarshal hello message: %w", err)
return nil, fmt.Errorf("unmarshal auth message: %w", err)
}
if err := h.validator.Validate(authPayload); err != nil {

View File

@@ -5164,6 +5164,10 @@ components:
type: boolean
description: Whether upstream TLS certificate verification is skipped when the proxy dials this provider's URL. Intended for self-hosted / internal gateways behind a private or self-signed certificate.
example: false
metadata_disabled:
type: boolean
description: Whether identity metadata injection is disabled for this provider. When enabled (the default), the proxy stamps the caller's user and authorizing group onto upstream requests as provider-specific metadata (e.g. AWS Bedrock's X-Amzn-Bedrock-Request-Metadata header). Set true to suppress it.
example: false
created_at:
type: string
format: date-time
@@ -5184,6 +5188,7 @@ components:
- models
- enabled
- skip_tls_verification
- metadata_disabled
- created_at
- updated_at
AgentNetworkProviderRequest:
@@ -5240,6 +5245,10 @@ components:
type: boolean
description: Skip upstream TLS certificate verification when the proxy dials this provider's URL. For self-hosted / internal gateways behind a private or self-signed certificate. Defaults to false. When omitted on update, the stored value is left unchanged.
example: false
metadata_disabled:
type: boolean
description: Disable identity metadata injection (the caller's user + authorizing group) for this provider. Defaults to false (metadata is injected). When omitted on update, the stored value is left unchanged.
example: false
required:
- provider_id
- name

View File

@@ -2227,6 +2227,9 @@ type AgentNetworkProvider struct {
// IdentityHeaderUserId Wire header name the proxy stamps with the caller's display identity (user email or peer name) when the catalog entry's HeaderPair is `customizable`. Empty disables stamping for this dimension. Ignored when the catalog entry has a fixed HeaderPair (e.g. LiteLLM, Portkey). Used today by Bifrost: typical values are `x-bf-lh-netbird_user_id` (always-on log metadata) or `x-bf-dim-netbird_user_id` (Prometheus / OTEL — requires the label to be pre-declared in the gateway's `client.prometheus_labels` config).
IdentityHeaderUserId *string `json:"identity_header_user_id,omitempty"`
// MetadataDisabled Whether identity metadata injection is disabled for this provider. When enabled (the default), the proxy stamps the caller's user and authorizing group onto upstream requests as provider-specific metadata (e.g. AWS Bedrock's X-Amzn-Bedrock-Request-Metadata header). Set true to suppress it.
MetadataDisabled bool `json:"metadata_disabled"`
// Models Models exposed through this endpoint, with the operator's per-1k input/output prices. Empty means all catalog models are allowed at catalog prices.
Models []AgentNetworkProviderModel `json:"models"`
@@ -2278,6 +2281,9 @@ type AgentNetworkProviderRequest struct {
// IdentityHeaderUserId Wire header name for the caller's display identity. See AgentNetworkProvider.identity_header_user_id. When omitted on a request, the stored value is left unchanged; pass an empty string explicitly to clear it (which disables stamping for this dimension).
IdentityHeaderUserId *string `json:"identity_header_user_id,omitempty"`
// MetadataDisabled Disable identity metadata injection (the caller's user + authorizing group) for this provider. Defaults to false (metadata is injected). When omitted on update, the stored value is left unchanged.
MetadataDisabled *bool `json:"metadata_disabled,omitempty"`
// Models Models exposed through this endpoint, with the operator's per-1k input/output prices. Empty means all catalog models are allowed at catalog prices.
Models *[]AgentNetworkProviderModel `json:"models,omitempty"`

View File

@@ -8,7 +8,3 @@ type Auth struct {
func (a *Auth) Validate(any) error {
return nil
}
func (a *Auth) ValidateHelloMsgType(any) error {
return nil
}

View File

@@ -1,10 +1,8 @@
package hmac
import (
"bytes"
"crypto/hmac"
"encoding/base64"
"encoding/gob"
"fmt"
"hash"
"strconv"
@@ -18,14 +16,6 @@ type Token struct {
Signature string
}
func unmarshalToken(payload []byte) (Token, error) {
var creds Token
buffer := bytes.NewBuffer(payload)
decoder := gob.NewDecoder(buffer)
err := decoder.Decode(&creds)
return creds, err
}
// TimedHMAC generates a token with TTL and uses a pre-shared secret known to the relay server
type TimedHMAC struct {
secret string

View File

@@ -1,33 +0,0 @@
package hmac
import (
"crypto/sha256"
"fmt"
"time"
log "github.com/sirupsen/logrus"
)
type TimedHMACValidator struct {
*TimedHMAC
}
func NewTimedHMACValidator(secret string, duration time.Duration) *TimedHMACValidator {
ta := NewTimedHMAC(secret, duration)
return &TimedHMACValidator{
ta,
}
}
func (a *TimedHMACValidator) Validate(credentials any) error {
b, ok := credentials.([]byte)
if !ok {
return fmt.Errorf("invalid credentials type")
}
c, err := unmarshalToken(b)
if err != nil {
log.Debugf("failed to unmarshal token: %s", err)
return err
}
return a.TimedHMAC.Validate(sha256.New, c)
}

View File

@@ -1,28 +1,19 @@
package auth
import (
"time"
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
authv2 "github.com/netbirdio/netbird/shared/relay/auth/hmac/v2"
)
type TimedHMACValidator struct {
authenticatorV2 *authv2.Validator
authenticator *auth.TimedHMACValidator
}
func NewTimedHMACValidator(secret []byte, duration time.Duration) *TimedHMACValidator {
func NewTimedHMACValidator(secret []byte) *TimedHMACValidator {
return &TimedHMACValidator{
authenticatorV2: authv2.NewValidator(secret),
authenticator: auth.NewTimedHMACValidator(string(secret), duration),
}
}
func (a *TimedHMACValidator) Validate(credentials any) error {
return a.authenticatorV2.Validate(credentials)
}
func (a *TimedHMACValidator) ValidateHelloMsgType(credentials any) error {
return a.authenticator.Validate(credentials)
}

View File

@@ -1,21 +0,0 @@
// Deprecated: This package is deprecated and will be removed in a future release.
package address
import (
"bytes"
"encoding/gob"
"fmt"
)
type Address struct {
URL string
}
func (addr *Address) Marshal() ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(addr); err != nil {
return nil, fmt.Errorf("encode Address: %w", err)
}
return buf.Bytes(), nil
}

View File

@@ -1,43 +0,0 @@
// Deprecated: This package is deprecated and will be removed in a future release.
package auth
import (
"bytes"
"encoding/gob"
"fmt"
)
type Algorithm int
const (
AlgoUnknown Algorithm = iota
AlgoHMACSHA256
AlgoHMACSHA512
)
func (a Algorithm) String() string {
switch a {
case AlgoHMACSHA256:
return "HMAC-SHA256"
case AlgoHMACSHA512:
return "HMAC-SHA512"
default:
return "Unknown"
}
}
type Msg struct {
AuthAlgorithm Algorithm
AdditionalData []byte
}
func UnmarshalMsg(data []byte) (*Msg, error) {
var msg *Msg
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
if err := dec.Decode(&msg); err != nil {
return nil, fmt.Errorf("decode Msg: %w", err)
}
return msg, nil
}

View File

@@ -14,9 +14,10 @@ const (
CurrentProtocolVersion = 1
MsgTypeUnknown MsgType = 0
// Deprecated: Use MsgTypeAuth instead.
MsgTypeHello = 1
// Deprecated: Use MsgTypeAuthResponse instead.
// MsgTypeHello and MsgTypeHelloResponse are the removed legacy handshake
// message types. They are retained only to reserve wire values 1 and 2 so
// the values are never reused; the server rejects both.
MsgTypeHello = 1
MsgTypeHelloResponse = 2
MsgTypeTransport = 3
MsgTypeClose = 4
@@ -42,10 +43,6 @@ const (
offsetAuthPeerID = sizeOfProtoHeader + sizeOfMagicByte
headerTotalSizeAuth = sizeOfProtoHeader + headerSizeAuth
// hello message
headerSizeHello = sizeOfMagicByte + peerIDSize
headerSizeHelloResp = 0
// transport
headerSizeTransport = peerIDSize
offsetTransportID = sizeOfProtoHeader
@@ -113,7 +110,6 @@ func DetermineClientMessageType(msg []byte) (MsgType, error) {
msgType := MsgType(msg[1])
switch msgType {
case
MsgTypeHello,
MsgTypeAuth,
MsgTypeTransport,
MsgTypeClose,
@@ -135,7 +131,6 @@ func DetermineServerMessageType(msg []byte) (MsgType, error) {
msgType := MsgType(msg[1])
switch msgType {
case
MsgTypeHelloResponse,
MsgTypeAuthResponse,
MsgTypeTransport,
MsgTypeClose,
@@ -148,67 +143,6 @@ func DetermineServerMessageType(msg []byte) (MsgType, error) {
}
}
// Deprecated: Use MarshalAuthMsg instead.
// MarshalHelloMsg initial hello message
// The Hello message is the first message sent by a client after establishing a connection with the Relay server. This
// message is used to authenticate the client with the server. The authentication is done using an HMAC method.
// The protocol does not limit to use HMAC, it can be any other method. If the authentication failed the server will
// close the network connection without any response.
func MarshalHelloMsg(peerID PeerID, additions []byte) ([]byte, error) {
msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, sizeOfProtoHeader+headerSizeHello+len(additions))
msg[0] = byte(CurrentProtocolVersion)
msg[1] = byte(MsgTypeHello)
copy(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader)
msg = append(msg, peerID[:]...)
msg = append(msg, additions...)
return msg, nil
}
// Deprecated: Use UnmarshalAuthMsg instead.
// UnmarshalHelloMsg extracts peerID and the additional data from the hello message. The Additional data is used to
// authenticate the client with the server.
func UnmarshalHelloMsg(msg []byte) (*PeerID, []byte, error) {
if len(msg) < sizeOfProtoHeader+headerSizeHello {
return nil, nil, ErrInvalidMessageLength
}
if !bytes.Equal(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader) {
return nil, nil, errors.New("invalid magic header")
}
peerID := PeerID(msg[sizeOfProtoHeader+sizeOfMagicByte : sizeOfProtoHeader+headerSizeHello])
return &peerID, msg[headerSizeHello:], nil
}
// Deprecated: Use MarshalAuthResponse instead.
// MarshalHelloResponse creates a response message to the hello message.
// In case of success connection the server response with a Hello Response message. This message contains the server's
// instance URL. This URL will be used by choose the common Relay server in case if the peers are in different Relay
// servers.
func MarshalHelloResponse(additionalData []byte) ([]byte, error) {
msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+headerSizeHelloResp+len(additionalData))
msg[0] = byte(CurrentProtocolVersion)
msg[1] = byte(MsgTypeHelloResponse)
msg = append(msg, additionalData...)
return msg, nil
}
// Deprecated: Use UnmarshalAuthResponse instead.
// UnmarshalHelloResponse extracts the additional data from the hello response message.
func UnmarshalHelloResponse(msg []byte) ([]byte, error) {
if len(msg) < sizeOfProtoHeader+headerSizeHelloResp {
return nil, ErrInvalidMessageLength
}
return msg, nil
}
// MarshalAuthMsg initial authentication message
// The Auth message is the first message sent by a client after establishing a connection with the Relay server. This
// message is used to authenticate the client with the server. The authentication is done using an HMAC method.

View File

@@ -4,28 +4,11 @@ import (
"testing"
)
func TestMarshalHelloMsg(t *testing.T) {
peerID := HashID("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+")
msg, err := MarshalHelloMsg(peerID, nil)
if err != nil {
t.Fatalf("error: %v", err)
}
msgType, err := DetermineClientMessageType(msg)
if err != nil {
t.Fatalf("error: %v", err)
}
if msgType != MsgTypeHello {
t.Errorf("expected %d, got %d", MsgTypeHello, msgType)
}
receivedPeerID, _, err := UnmarshalHelloMsg(msg)
if err != nil {
t.Fatalf("error: %v", err)
}
if receivedPeerID.String() != peerID.String() {
t.Errorf("expected %s, got %s", peerID, receivedPeerID)
func TestDetermineClientMessageTypeRejectsHello(t *testing.T) {
// The reserved legacy Hello message (type 1) must be rejected by the server.
msg := []byte{byte(CurrentProtocolVersion), byte(MsgTypeHello)}
if _, err := DetermineClientMessageType(msg); err == nil {
t.Fatalf("expected hello message type to be rejected")
}
}