mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-21 16:01:28 +02:00
Merge branch 'main' into net-1411-macos-menu-bar-welcome
This commit is contained in:
@@ -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"}
|
||||
]
|
||||
}
|
||||
|
||||
1325
client/ui/i18n/locales/ja/common.json
Normal file
1325
client/ui/i18n/locales/ja/common.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
|
||||
@@ -8,7 +8,3 @@ type Auth struct {
|
||||
func (a *Auth) Validate(any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auth) ValidateHelloMsgType(any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user