Compare commits

..

8 Commits

Author SHA1 Message Date
mlsmaycon
f28ff2a1f1 Merge branch 'main' into 0.74.5-branch-sync
# Conflicts:
#	client/internal/peer/conn.go
#	client/internal/peer/conn_test.go
#	client/internal/peer/wg_watcher.go
#	client/internal/peer/wg_watcher_test.go
#	go.mod
#	go.sum
2026-07-14 20:18:01 +02:00
Maycon Santos
f0eed7564f [management] Remove proxy peer stale deduplication logic (#6768)
## Describe your changes

Removing a leftover from an initial implementation. We ended up
resolving it on the client with status checks on the DNS response

## Issue ticket number and link

## Stack

- \#6726 <!-- branch-stack -->
  - \#6768 :point\_left:

### Checklist

- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation

Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)

Paste the PR link from <https://github.com/netbirdio/docs> here:

<https://github.com/netbirdio/docs/pull/>\_\_

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added support for Bedrock-native request routing in agent network
scenarios.
- Added guardrail management capabilities for creating and removing
model allowlists.

- **Bug Fixes**
- Model allowlists now reject requests when the model is missing or
blank.
- Improved Rosenpass and WireGuard recovery after repeated handshake
failures.
- Improved relay connection handling so status and cleanup operations
remain responsive during stalled connections.
- Updated private service DNS zones to avoid unintended search-domain
behavior.

- **Tests**
- Expanded coverage for model allowlists, handshake recovery, relay
concurrency, and Bedrock routing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 20:13:00 +02:00
Maycon Santos
277d8e4c53 [proxy] enforce model allowlist for URL-routed providers (Bedrock/Vertex) (#6764)
## Describe your changes

The Agent Network policy Guardrail "Model Allowlist" was not enforced
for providers whose model travels in the URL/path rather than the JSON
body — most visibly AWS Bedrock (reported in netbirdio/netbird#6751),
and the same class applies to Google Vertex.

Root cause: the `llm_guardrail` allowlist check **failed open**.
`evaluateAllowlist` returned allow whenever the request model was absent
from the metadata bag (`middleware.go`, `if !modelPresent { return nil
}`). The model is stamped upstream by `llm_request_parser`; for
body-routed providers (OpenAI/Anthropic) it comes from the JSON body,
but for path-routed providers the model is recovered only when the
request matches a recognized path shape (Bedrock
`/model/{id}/{invoke|converse|...}`, Vertex
`/v1/projects/.../publishers/.../models/...`). Any shape the parser did
not recognize reached the guardrail with no model and was allowed
regardless of the allowlist.

Fix (provider-agnostic): **fail closed**. When an allowlist is
configured and the model cannot be determined (absent or empty), the
request is denied `403` with a distinct `llm_policy.model_unknown`
reason. This closes the bypass for Bedrock, Vertex, and any future
URL-routed provider in one place. When no allowlist is configured,
behavior is unchanged.

The model allowlist is enforced solely in the proxy `llm_guardrail`;
management's `CheckLLMPolicyLimits` handles only token/budget caps, so
no management change is required.

## Issue ticket number and link

<https://github.com/netbirdio/netbird/discussions/6751>

## Stack

- \#6726 <!-- branch-stack -->
  - \#6764 :point\_left:

### Checklist

- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [x] Created tests that fail without the change (if possible)
- [x] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation

Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

Bug fix that restores the documented allowlist behavior; no user-facing
surface changes.

### Docs PR URL (required if "docs added" is checked)

Paste the PR link from <https://github.com/netbirdio/docs> here:

<https://github.com/netbirdio/docs/pull/>\_\_

## Tests

- `llm_guardrail`: absent/empty model under a configured allowlist now
denies (`model_unknown`); empty allowlist still allows a missing model
(fail-closed only applies when a list is set); existing
allow/deny/case-insensitive cases retained.
- `llm_request_parser`: new parser→guardrail integration test drives
real **Bedrock** (`/model/{id}/invoke`) and **Vertex**
(`/v1/projects/.../models/...`) URL shapes and asserts allowed→200,
disallowed→403 (`model_blocked`), and an unrecognized Bedrock action→403
(`model_unknown`, the #6751 regression guard).

Note: a full through-tunnel e2e for the allowlist is intentionally
deferred — the agent-network e2e (`WaitProxyPeer`) is currently red on
`main`/`0.74.x` for an unrelated lazy-connection reason; it will be
added once that harness gate is fixed.
2026-07-14 19:03:01 +02:00
Viktor Liu
3d87547d95 [client] Bump golang.org/x/crypto to v0.54.0 and Go toolchain to 1.25.12 (#6709) 2026-07-10 17:42:06 +02:00
Viktor Liu
4d4cc551fd [client] Recover from rosenpass key desync (#6714) 2026-07-10 17:38:29 +02:00
blaugrau90
08e46aa62f [management] fix: prevent reverse proxy domain from being pushed as DNS search domain (#6498)
SynthesizePrivateServiceZones created CustomZones for private services
without setting SearchDomainDisabled, causing the reverse proxy domain
to be injected as a search domain suffix on all connected peers.

This broke local hostname resolution: short names like 'myserver' were
expanded to 'myserver.app.example.com' (matching the reverse proxy
domain) before local DNS search domains were tried.

Fix: set SearchDomainDisabled: true so the zone is registered as a
match-only supplemental resolver, consistent with the NonAuthoritative
intent already expressed on the same zone.
2026-07-10 12:20:57 +02:00
Theodor Midtlien
7cd5c1732b [client] Fix hanging status command during relay dial (#6694)
* Add regression test for relay state lock
* Make connect not hold a lock in openConnVia
2026-07-08 14:36:42 +02:00
Maycon Santos
816d80602f [client] Update gopsutil to v4 (#6688) 2026-07-08 10:15:31 +02:00
15 changed files with 70 additions and 599 deletions

View File

@@ -551,7 +551,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
} else {
log.Infof("running rosenpass in strict mode")
}
e.rpManager, err = rosenpass.NewManager(e.config.PreSharedKey, e.config.WgIfaceName, publicKey, e.config.StateDir)
e.rpManager, err = rosenpass.NewManager(e.config.PreSharedKey, e.config.WgIfaceName, publicKey)
if err != nil {
return fmt.Errorf("create rosenpass manager: %w", err)
}
@@ -1809,7 +1809,6 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
PubKey: e.getRosenpassPubKey(),
Addr: e.getRosenpassAddr(),
PermissiveMode: e.config.RosenpassPermissive,
KeyResolver: e.rosenpassKeyResolver(),
},
ICEConfig: e.createICEConfig(),
}
@@ -1880,8 +1879,6 @@ func (e *Engine) receiveSignalEvents() error {
log.Debugf("receiveMSG: took %s to get lock for peer %s with session id %s", gotLock, msg.Key, offerAnswer.SessionID)
e.applyRosenpassKeyExchange(msg, offerAnswer)
if msg.Body.Type == sProto.Body_OFFER {
conn.OnRemoteOffer(*offerAnswer)
} else {
@@ -2225,34 +2222,6 @@ func (e *Engine) getRosenpassAddr() string {
return ""
}
// rosenpassKeyResolver returns the Rosenpass manager as the offer/answer key
// resolver, or a true nil interface when Rosenpass is disabled (returning the
// typed-nil *Manager would make the interface non-nil and panic on use).
func (e *Engine) rosenpassKeyResolver() peer.RosenpassKeyResolver {
if e.rpManager == nil {
return nil
}
return e.rpManager
}
// applyRosenpassKeyExchange reconciles the fingerprint/cache fields of an incoming
// offer/answer against the Rosenpass manager's cache: it resolves the remote peer's
// full public key (from the message or the cache) into the OfferAnswer, and records
// whether the peer acknowledged holding our key. No-op when Rosenpass is disabled.
func (e *Engine) applyRosenpassKeyExchange(msg *sProto.Message, oa *peer.OfferAnswer) {
if e.rpManager == nil {
return
}
cfg := msg.GetBody().GetRosenpassConfig()
if cfg == nil {
return
}
remoteWgKey := msg.GetKey()
oa.RosenpassPubKey = e.rpManager.ResolveRemotePubKey(remoteWgKey, cfg.GetRosenpassPubKey(), cfg.GetRosenpassPubKeyHash())
e.rpManager.SetRemoteAck(remoteWgKey, cfg.GetAcknowledgedRosenpassPubKeyHash())
}
// RunHealthProbes executes health checks for Signal, Management, Relay, and WireGuard services
// and updates the status recorder with the latest states.
//

View File

@@ -65,20 +65,6 @@ type WgConfig struct {
PreSharedKey *wgtypes.Key
}
// RosenpassKeyResolver lets the handshaker fill the fingerprint/cache fields of an
// offer/answer without depending on the Rosenpass manager directly. Implemented by
// rosenpass.Manager and wired in by the engine.
type RosenpassKeyResolver interface {
// LocalPubKeyHash is the SHA256 of our own Rosenpass public key.
LocalPubKeyHash() []byte
// RemotePubKeyAck is the SHA256 of the remote peer's cached key (nil if we do
// not hold it), sent back as an acknowledgement.
RemotePubKeyAck(remoteWgKey string) []byte
// RemoteHasLocalKey reports whether the peer already holds our key, so the full
// key may be omitted.
RemoteHasLocalKey(remoteWgKey string) bool
}
type RosenpassConfig struct {
// RosenpassPubKey is this peer's Rosenpass public key
PubKey []byte
@@ -86,10 +72,6 @@ type RosenpassConfig struct {
Addr string
PermissiveMode bool
// KeyResolver drives fingerprint-based key caching over signalling. Nil when
// Rosenpass is disabled, which makes the handshaker always send the full key.
KeyResolver RosenpassKeyResolver
}
// ConnConfig is a peer Connection configuration

View File

@@ -33,13 +33,8 @@ type OfferAnswer struct {
// Version of NetBird Agent
Version string
// RosenpassPubKey is the Rosenpass public key of the remote peer when receiving this message
// This value is the local Rosenpass server public key when sending the message.
// May be empty on send when the remote peer has acknowledged already holding it (see RosenpassPubKeyAck).
// This value is the local Rosenpass server public key when sending the message
RosenpassPubKey []byte
// RosenpassPubKeyHash is the SHA256 of the sender's own RosenpassPubKey. Always set when Rosenpass is enabled.
RosenpassPubKeyHash []byte
// RosenpassPubKeyAck is the SHA256 of the remote peer's key the sender holds cached; empty means "send it in full".
RosenpassPubKeyAck []byte
// RosenpassAddr is the Rosenpass server address (IP:port) of the remote peer when receiving this message
// This value is the local Rosenpass server address when sending the message
RosenpassAddr string
@@ -214,11 +209,11 @@ func (h *Handshaker) sendAnswer() error {
func (h *Handshaker) buildOfferAnswer() OfferAnswer {
answer := OfferAnswer{
WgListenPort: h.config.LocalWgPort,
Version: version.NetbirdVersion(),
RosenpassAddr: h.config.RosenpassConfig.Addr,
WgListenPort: h.config.LocalWgPort,
Version: version.NetbirdVersion(),
RosenpassPubKey: h.config.RosenpassConfig.PubKey,
RosenpassAddr: h.config.RosenpassConfig.Addr,
}
h.setRosenpassPubKey(&answer)
if h.ice != nil && h.RemoteICESupported() {
uFrag, pwd := h.ice.GetLocalUserCredentials()
@@ -235,30 +230,6 @@ func (h *Handshaker) buildOfferAnswer() OfferAnswer {
return answer
}
// setRosenpassPubKey fills the Rosenpass key fields of an outgoing offer/answer.
// With a resolver wired it advertises our key hash and the ack for the remote key
// we hold, and includes the full public key only when the peer has not yet
// acknowledged holding it. Without a resolver (Rosenpass disabled, or an older
// code path) it always sends the full key, preserving the previous behaviour.
func (h *Handshaker) setRosenpassPubKey(answer *OfferAnswer) {
localKey := h.config.RosenpassConfig.PubKey
if len(localKey) == 0 {
return
}
resolver := h.config.RosenpassConfig.KeyResolver
if resolver == nil {
answer.RosenpassPubKey = localKey
return
}
answer.RosenpassPubKeyHash = resolver.LocalPubKeyHash()
answer.RosenpassPubKeyAck = resolver.RemotePubKeyAck(h.config.Key)
if !resolver.RemoteHasLocalKey(h.config.Key) {
answer.RosenpassPubKey = localKey
}
}
func (h *Handshaker) updateRemoteICEState(offer *OfferAnswer) {
hasICE := offer.hasICECredentials()
prev := h.remoteICESupported.Swap(hasICE)

View File

@@ -1,65 +0,0 @@
package peer
import (
"testing"
"github.com/stretchr/testify/require"
)
type fakeRPResolver struct {
localHash []byte
ack []byte
hasLocal bool
}
func (f fakeRPResolver) LocalPubKeyHash() []byte { return f.localHash }
func (f fakeRPResolver) RemotePubKeyAck(string) []byte { return f.ack }
func (f fakeRPResolver) RemoteHasLocalKey(remote string) bool { return f.hasLocal }
func TestSetRosenpassPubKey_NoResolverAlwaysSendsFullKey(t *testing.T) {
localKey := []byte{1, 2, 3}
h := &Handshaker{config: ConnConfig{RosenpassConfig: RosenpassConfig{PubKey: localKey}}}
var a OfferAnswer
h.setRosenpassPubKey(&a)
require.Equal(t, localKey, a.RosenpassPubKey)
require.Nil(t, a.RosenpassPubKeyHash)
require.Nil(t, a.RosenpassPubKeyAck)
}
func TestSetRosenpassPubKey_ResolverIncludesFullKeyUntilAcked(t *testing.T) {
localKey := []byte{1, 2, 3}
res := fakeRPResolver{localHash: []byte{9}, ack: []byte{8}, hasLocal: false}
h := &Handshaker{config: ConnConfig{Key: "peerA", RosenpassConfig: RosenpassConfig{PubKey: localKey, KeyResolver: res}}}
var a OfferAnswer
h.setRosenpassPubKey(&a)
require.Equal(t, localKey, a.RosenpassPubKey, "full key must be sent until the peer acks it")
require.Equal(t, []byte{9}, a.RosenpassPubKeyHash)
require.Equal(t, []byte{8}, a.RosenpassPubKeyAck)
}
func TestSetRosenpassPubKey_ResolverOmitsFullKeyOnceAcked(t *testing.T) {
localKey := []byte{1, 2, 3}
res := fakeRPResolver{localHash: []byte{9}, ack: []byte{8}, hasLocal: true}
h := &Handshaker{config: ConnConfig{Key: "peerA", RosenpassConfig: RosenpassConfig{PubKey: localKey, KeyResolver: res}}}
var a OfferAnswer
h.setRosenpassPubKey(&a)
require.Nil(t, a.RosenpassPubKey, "full key must be omitted once the peer holds it")
require.Equal(t, []byte{9}, a.RosenpassPubKeyHash)
require.Equal(t, []byte{8}, a.RosenpassPubKeyAck)
}
func TestSetRosenpassPubKey_DisabledSetsNothing(t *testing.T) {
h := &Handshaker{config: ConnConfig{RosenpassConfig: RosenpassConfig{}}}
var a OfferAnswer
h.setRosenpassPubKey(&a)
require.Nil(t, a.RosenpassPubKey)
require.Nil(t, a.RosenpassPubKeyHash)
}

View File

@@ -61,13 +61,11 @@ func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string,
UFrag: offerAnswer.IceCredentials.UFrag,
Pwd: offerAnswer.IceCredentials.Pwd,
},
RosenpassPubKey: offerAnswer.RosenpassPubKey,
RosenpassPubKeyHash: offerAnswer.RosenpassPubKeyHash,
RosenpassPubKeyAck: offerAnswer.RosenpassPubKeyAck,
RosenpassAddr: offerAnswer.RosenpassAddr,
RelaySrvAddress: offerAnswer.RelaySrvAddress,
RelaySrvIP: offerAnswer.RelaySrvIP,
SessionID: sessionIDBytes,
RosenpassPubKey: offerAnswer.RosenpassPubKey,
RosenpassAddr: offerAnswer.RosenpassAddr,
RelaySrvAddress: offerAnswer.RelaySrvAddress,
RelaySrvIP: offerAnswer.RelaySrvIP,
SessionID: sessionIDBytes,
})
if err != nil {
return err

View File

@@ -1,62 +0,0 @@
package rosenpass
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
)
func newCacheTestManager(spk []byte) *Manager {
return &Manager{
spk: spk,
remotePubKeys: make(map[string][]byte),
remoteHasLocalKey: make(map[string]bool),
}
}
func TestResolveRemotePubKey(t *testing.T) {
m := newCacheTestManager([]byte{0x01, 0x02})
full := bytes.Repeat([]byte{0xAB}, 64)
// a received full key is cached and returned
require.Equal(t, full, m.ResolveRemotePubKey("peerA", full, nil))
// a later hash-only message resolves from the cache
require.Equal(t, full, m.ResolveRemotePubKey("peerA", nil, rawRosenpassKeyHash(full)))
// hash mismatch is a cache miss
require.Nil(t, m.ResolveRemotePubKey("peerA", nil, bytes.Repeat([]byte{0x01}, 32)))
// no key and no hash (remote without Rosenpass) resolves to nil
require.Nil(t, m.ResolveRemotePubKey("peerB", nil, nil))
}
func TestRemotePubKeyAck(t *testing.T) {
m := newCacheTestManager([]byte{0x01})
// unknown peer -> no ack (signals "send me the full key")
require.Nil(t, m.RemotePubKeyAck("peerA"))
full := bytes.Repeat([]byte{0x09}, 48)
m.ResolveRemotePubKey("peerA", full, nil)
require.Equal(t, rawRosenpassKeyHash(full), m.RemotePubKeyAck("peerA"))
}
func TestSetRemoteAckAndRemoteHasLocalKey(t *testing.T) {
m := newCacheTestManager(bytes.Repeat([]byte{0x07}, 100))
require.False(t, m.RemoteHasLocalKey("peerA"))
// an ack matching our own key hash marks the peer as holding our key
m.SetRemoteAck("peerA", m.LocalPubKeyHash())
require.True(t, m.RemoteHasLocalKey("peerA"))
// empty ack clears it
m.SetRemoteAck("peerA", nil)
require.False(t, m.RemoteHasLocalKey("peerA"))
// a non-matching ack does not count
m.SetRemoteAck("peerA", bytes.Repeat([]byte{0x01}, 32))
require.False(t, m.RemoteHasLocalKey("peerA"))
}

View File

@@ -8,7 +8,6 @@ import (
"log/slog"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -29,11 +28,6 @@ func hashRosenpassKey(key []byte) string {
return hex.EncodeToString(hasher.Sum(nil))
}
func rawRosenpassKeyHash(key []byte) []byte {
sum := sha256.Sum256(key)
return sum[:]
}
// rpServer is the subset of rp.Server used by Manager. Defined as an interface
// so tests can substitute a mock without spinning up a real UDP server.
type rpServer interface {
@@ -56,29 +50,12 @@ type Manager struct {
lock sync.Mutex
port int
wgIface PresharedKeySetter
// remotePubKeys caches remote peers' full Rosenpass public keys keyed by their
// WireGuard public key, so a peer that already sent us its (large) key over
// signalling need only send its hash on subsequent offers/answers. RAM only —
// never persisted (1000 peers x ~512KB would be ~512MB on disk).
remotePubKeys map[string][]byte
// remoteHasLocalKey tracks, per remote WireGuard key, whether that peer has
// acknowledged holding our current Rosenpass public key, letting us omit it.
remoteHasLocalKey map[string]bool
}
// NewManager creates a new Rosenpass manager. localWgKey is the local
// WireGuard public key, used to derive the per-peer rendezvous key. When stateDir
// is non-empty the static keypair is persisted under it and reused across
// restarts, keeping the public key (and the fingerprint peers cache) stable;
// an empty stateDir keeps the previous behaviour of an ephemeral per-run keypair.
func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string, localWgKey wgtypes.Key, stateDir string) (*Manager, error) {
var keyPath string
if stateDir != "" {
keyPath = filepath.Join(stateDir, keypairFileName)
}
public, secret, err := loadOrGenerateKeypair(keyPath)
// WireGuard public key, used to derive the per-peer rendezvous key.
func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string, localWgKey wgtypes.Key) (*Manager, error) {
public, secret, err := rp.GenerateKeyPair()
if err != nil {
return nil, err
}
@@ -99,10 +76,8 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string, localWgKey wgtype
// nil receiver in addPeer -> m.rpWgHandler.AddPeer. generateConfig will
// replace it with a fresh handler on each Run() to clear stale peer
// state from previous engine sessions.
rpWgHandler: NewNetbirdHandler((*[32]byte)(preSharedKey), localWgKey),
lock: sync.Mutex{},
remotePubKeys: make(map[string][]byte),
remoteHasLocalKey: make(map[string]bool),
rpWgHandler: NewNetbirdHandler((*[32]byte)(preSharedKey), localWgKey),
lock: sync.Mutex{},
}, nil
}
@@ -115,68 +90,6 @@ func (m *Manager) GetAddress() *net.UDPAddr {
return &net.UDPAddr{Port: m.port}
}
// LocalPubKeyHash returns the raw SHA256 of the local Rosenpass public key. It is
// advertised on every offer/answer so the remote peer can tell (via its cache)
// whether it already holds our full key.
func (m *Manager) LocalPubKeyHash() []byte {
return rawRosenpassKeyHash(m.spk)
}
// RemotePubKeyAck returns the SHA256 of the remote peer's cached public key, used
// as the acknowledgement we send back. Nil means we do not hold the peer's key,
// which signals the peer to include its full key next time.
func (m *Manager) RemotePubKeyAck(remoteWgKey string) []byte {
m.lock.Lock()
defer m.lock.Unlock()
key, ok := m.remotePubKeys[remoteWgKey]
if !ok {
return nil
}
return rawRosenpassKeyHash(key)
}
// RemoteHasLocalKey reports whether the remote peer acknowledged holding our
// current public key, so we may omit the full key from the next offer/answer.
func (m *Manager) RemoteHasLocalKey(remoteWgKey string) bool {
m.lock.Lock()
defer m.lock.Unlock()
return m.remoteHasLocalKey[remoteWgKey]
}
// ResolveRemotePubKey reconciles the Rosenpass key material from a received
// offer/answer: it caches a received full key, or — when only a hash was sent —
// returns the cached key matching that hash. It returns nil when the remote peer
// does not use Rosenpass (no key, no hash) or on a cache miss (hash sent but not
// held); a miss self-heals because our resulting empty ack makes the peer resend
// its full key.
func (m *Manager) ResolveRemotePubKey(remoteWgKey string, full, hash []byte) []byte {
m.lock.Lock()
defer m.lock.Unlock()
if len(full) > 0 {
m.remotePubKeys[remoteWgKey] = full
return full
}
if len(hash) == 0 {
return nil
}
if cached, ok := m.remotePubKeys[remoteWgKey]; ok && bytes.Equal(rawRosenpassKeyHash(cached), hash) {
return cached
}
return nil
}
// SetRemoteAck records whether the remote peer's acknowledgement matches our
// current public key hash, i.e. whether it already holds our key.
func (m *Manager) SetRemoteAck(remoteWgKey string, ack []byte) {
m.lock.Lock()
defer m.lock.Unlock()
m.remoteHasLocalKey[remoteWgKey] = len(ack) > 0 && bytes.Equal(ack, rawRosenpassKeyHash(m.spk))
}
// addPeer adds a new peer to the Rosenpass server
func (m *Manager) addPeer(rosenpassPubKey []byte, rosenpassAddr string, wireGuardIP string, wireGuardPubKey string) error {
// Defense in depth against issue #4341 (Android crash): if Run() has not

View File

@@ -255,7 +255,7 @@ func TestAddPeer_NilServer_ReturnsErrorNoCrash(t *testing.T) {
// issue #4341 cannot occur in the window between NewManager and Run().
func TestNewManager_PreInitializesHandler(t *testing.T) {
psk := wgtypes.Key{}
m, err := NewManager(&psk, "wt0", wgtypes.Key{0x01}, "")
m, err := NewManager(&psk, "wt0", wgtypes.Key{0x01})
require.NoError(t, err)
require.NotNil(t, m.rpWgHandler, "rpWgHandler must be initialized in NewManager")
}

View File

@@ -1,92 +0,0 @@
package rosenpass
import (
"context"
"fmt"
"os"
rp "cunicu.li/go-rosenpass"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/util"
)
const (
// keypairFileName is the file, relative to the state directory, that holds
// the persisted local Rosenpass static keypair.
keypairFileName = "rosenpass_key.json"
// rpStaticPublicKeySize is the byte length of a Rosenpass (Classic McEliece)
// static public key as produced by the pinned go-rosenpass version. Used as a
// version-compatibility guard: a persisted key of any other size is treated as
// stale and regenerated instead of being fed to go-rosenpass (which would fail).
rpStaticPublicKeySize = 524160
// keypairFormatVersion is bumped whenever the on-disk representation changes so
// old files are discarded and regenerated rather than misparsed.
keypairFormatVersion = 1
)
// persistedKeypair is the on-disk representation of the local Rosenpass static
// keypair. Keys are stored raw (base64 via JSON) with the same restricted 0600
// permission as the WireGuard private key and other client secrets.
type persistedKeypair struct {
Version int `json:"version"`
PublicKey []byte `json:"public_key"`
SecretKey []byte `json:"secret_key"`
}
// loadOrGenerateKeypair returns a Rosenpass static keypair. When keyPath is set
// and holds a valid persisted keypair it is reused, so the local public key —
// and therefore the fingerprint advertised to remote peers over signalling —
// stays stable across restarts. Otherwise a fresh keypair is generated and, when
// keyPath is set, persisted for subsequent runs. A missing or corrupt file is not
// fatal: it degrades to generating an ephemeral keypair, matching the pre-persistence
// behaviour.
func loadOrGenerateKeypair(keyPath string) (public []byte, secret []byte, err error) {
if keyPath != "" {
public, secret, err = loadKeypair(keyPath)
switch {
case err == nil:
return public, secret, nil
case os.IsNotExist(err):
// first run for this state dir; fall through to generate
default:
log.Warnf("failed to load persisted rosenpass keypair, generating a new one: %v", err)
}
}
pub, sec, err := rp.GenerateKeyPair()
if err != nil {
return nil, nil, fmt.Errorf("generate rosenpass key pair: %w", err)
}
if keyPath != "" {
if err := saveKeypair(keyPath, pub, sec); err != nil {
log.Warnf("failed to persist rosenpass keypair, key will be regenerated on next restart: %v", err)
}
}
return pub, sec, nil
}
func loadKeypair(keyPath string) ([]byte, []byte, error) {
var kp persistedKeypair
if _, err := util.ReadJson(keyPath, &kp); err != nil {
return nil, nil, err
}
if kp.Version != keypairFormatVersion || len(kp.PublicKey) != rpStaticPublicKeySize || len(kp.SecretKey) == 0 {
return nil, nil, fmt.Errorf("persisted rosenpass keypair is incompatible (version %d, public %d bytes, secret %d bytes)", kp.Version, len(kp.PublicKey), len(kp.SecretKey))
}
return kp.PublicKey, kp.SecretKey, nil
}
func saveKeypair(keyPath string, public, secret []byte) error {
return util.WriteJsonWithRestrictedPermission(context.Background(), keyPath, persistedKeypair{
Version: keypairFormatVersion,
PublicKey: public,
SecretKey: secret,
})
}

View File

@@ -1,66 +0,0 @@
package rosenpass
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestLoadOrGenerateKeypair_EphemeralWhenNoPath(t *testing.T) {
pub, sec, err := loadOrGenerateKeypair("")
require.NoError(t, err)
require.Len(t, pub, rpStaticPublicKeySize)
require.NotEmpty(t, sec)
}
func TestLoadOrGenerateKeypair_PersistsAndReloads(t *testing.T) {
keyPath := filepath.Join(t.TempDir(), keypairFileName)
pub1, sec1, err := loadOrGenerateKeypair(keyPath)
require.NoError(t, err)
info, err := os.Stat(keyPath)
require.NoError(t, err, "keypair file must be written")
require.Equal(t, os.FileMode(0600), info.Mode().Perm(), "keypair file must be 0600")
pub2, sec2, err := loadOrGenerateKeypair(keyPath)
require.NoError(t, err)
require.True(t, bytes.Equal(pub1, pub2), "public key must be stable across reloads")
require.True(t, bytes.Equal(sec1, sec2), "secret key must be stable across reloads")
}
func TestLoadOrGenerateKeypair_RegeneratesOnCorruptFile(t *testing.T) {
keyPath := filepath.Join(t.TempDir(), keypairFileName)
require.NoError(t, os.WriteFile(keyPath, []byte("not json"), 0600))
pub, sec, err := loadOrGenerateKeypair(keyPath)
require.NoError(t, err)
require.Len(t, pub, rpStaticPublicKeySize)
require.NotEmpty(t, sec)
// the corrupt file must have been overwritten with a valid, reloadable keypair
pub2, _, err := loadOrGenerateKeypair(keyPath)
require.NoError(t, err)
require.True(t, bytes.Equal(pub, pub2))
}
func TestLoadOrGenerateKeypair_RegeneratesOnVersionMismatch(t *testing.T) {
keyPath := filepath.Join(t.TempDir(), keypairFileName)
pub1, _, err := loadOrGenerateKeypair(keyPath)
require.NoError(t, err)
// rewrite with a bumped/unknown format version -> must be discarded
bs, err := json.Marshal(persistedKeypair{Version: keypairFormatVersion + 1, PublicKey: pub1, SecretKey: []byte{0x01}})
require.NoError(t, err)
require.NoError(t, os.WriteFile(keyPath, bs, 0600))
pub2, sec2, err := loadOrGenerateKeypair(keyPath)
require.NoError(t, err)
require.Len(t, pub2, rpStaticPublicKeySize)
require.NotEmpty(t, sec2)
}

View File

@@ -1,24 +1,19 @@
package main
import (
"log"
"net/http"
// nolint:gosec
_ "net/http/pprof"
"os"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/management/cmd"
)
func main() {
if pprofAddr := os.Getenv("NB_PPROF_ADDR"); pprofAddr != "" {
log.Infof("pprof enabled, listening on: %s", pprofAddr)
go func() {
log.Println(http.ListenAndServe(pprofAddr, nil))
}()
}
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
if err := cmd.Execute(); err != nil {
os.Exit(1)
}

View File

@@ -7,7 +7,6 @@ import (
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/netbirdio/netbird/client/ssh/auth"
@@ -43,14 +42,6 @@ type NetworkMapComponents struct {
PostureFailedPeers map[string]map[string]struct{}
RouterPeers map[string]*nbpeer.Peer
routesByPeerOnce sync.Once
routesByPeerIdx map[string][]routeIndexEntry
}
type routeIndexEntry struct {
route *route.Route
viaGroup bool
}
type AccountSettingsInfo struct {
@@ -539,43 +530,33 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute
disabledRoutes = append(disabledRoutes, r)
}
for _, entry := range c.routesByPeer()[peerID] {
if entry.viaGroup {
newPeerRoute := entry.route.Copy()
newPeerRoute.PeerGroups = nil
newPeerRoute.ID = route.ID(string(entry.route.ID) + ":" + peerID)
takeRoute(newPeerRoute)
continue
for _, r := range c.Routes {
for _, groupID := range r.PeerGroups {
group := c.GetGroupInfo(groupID)
if group == nil {
continue
}
for _, id := range group.Peers {
if id != peerID {
continue
}
newPeerRoute := r.Copy()
newPeerRoute.Peer = id
newPeerRoute.PeerGroups = nil
newPeerRoute.ID = route.ID(string(r.ID) + ":" + id)
takeRoute(newPeerRoute)
break
}
}
if r.Peer == peerID {
takeRoute(r.Copy())
}
takeRoute(entry.route.Copy())
}
return enabledRoutes, disabledRoutes
}
func (c *NetworkMapComponents) routesByPeer() map[string][]routeIndexEntry {
c.routesByPeerOnce.Do(func() {
idx := make(map[string][]routeIndexEntry)
for _, r := range c.Routes {
for _, groupID := range r.PeerGroups {
group := c.GetGroupInfo(groupID)
if group == nil {
continue
}
for _, id := range group.Peers {
idx[id] = append(idx[id], routeIndexEntry{route: r, viaGroup: true})
}
}
if r.Peer != "" {
idx[r.Peer] = append(idx[r.Peer], routeIndexEntry{route: r})
}
}
c.routesByPeerIdx = idx
})
return c.routesByPeerIdx
}
func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route {
var filteredRoutes []*route.Route
for _, r := range routes {

View File

@@ -51,17 +51,10 @@ type CredentialPayload struct {
WgListenPort int
Credential *Credential
RosenpassPubKey []byte
// RosenpassPubKeyHash is the SHA256 of the sender's own RosenpassPubKey (empty
// when Rosenpass is disabled). RosenpassPubKey may be omitted when the peer has
// already acknowledged this hash. See RosenpassConfig in the proto.
RosenpassPubKeyHash []byte
// RosenpassPubKeyAck is the SHA256 of the remote peer's key the sender holds
// cached; empty means "send me the full key".
RosenpassPubKeyAck []byte
RosenpassAddr string
RelaySrvAddress string
RelaySrvIP netip.Addr
SessionID []byte
RosenpassAddr string
RelaySrvAddress string
RelaySrvIP netip.Addr
SessionID []byte
}
// UnMarshalCredential parses the credentials from the message and returns a Credential instance
@@ -85,10 +78,8 @@ func MarshalCredential(myKey wgtypes.Key, remoteKey string, p CredentialPayload)
WgListenPort: uint32(p.WgListenPort),
NetBirdVersion: version.NetbirdVersion(),
RosenpassConfig: &proto.RosenpassConfig{
RosenpassPubKey: p.RosenpassPubKey,
RosenpassServerAddr: p.RosenpassAddr,
RosenpassPubKeyHash: p.RosenpassPubKeyHash,
AcknowledgedRosenpassPubKeyHash: p.RosenpassPubKeyAck,
RosenpassPubKey: p.RosenpassPubKey,
RosenpassServerAddr: p.RosenpassAddr,
},
SessionId: p.SessionID,
}

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v6.33.1
// protoc v3.21.12
// source: signalexchange.proto
package proto
@@ -399,17 +399,6 @@ type RosenpassConfig struct {
RosenpassPubKey []byte `protobuf:"bytes,1,opt,name=rosenpassPubKey,proto3" json:"rosenpassPubKey,omitempty"`
// rosenpassServerAddr is an IP:port of the rosenpass service
RosenpassServerAddr string `protobuf:"bytes,2,opt,name=rosenpassServerAddr,proto3" json:"rosenpassServerAddr,omitempty"`
// rosenpassPubKeyHash is the SHA256 of the sender's own rosenpassPubKey. It is
// always set when Rosenpass is enabled and lets the receiver detect (via a
// per-peer cache) whether it already holds the sender's full public key,
// avoiding re-sending the large key on every offer/answer.
RosenpassPubKeyHash []byte `protobuf:"bytes,3,opt,name=rosenpassPubKeyHash,proto3" json:"rosenpassPubKeyHash,omitempty"`
// acknowledgedRosenpassPubKeyHash is the SHA256 of the remote peer's rosenpassPubKey
// that the sender currently holds cached. When it matches the receiver's own key hash
// the receiver may omit its full rosenpassPubKey from the message. Empty means the
// sender does not have the remote key and needs it sent in full. Absent from peers
// that predate this field, which keeps them receiving the full key as before.
AcknowledgedRosenpassPubKeyHash []byte `protobuf:"bytes,4,opt,name=acknowledgedRosenpassPubKeyHash,proto3" json:"acknowledgedRosenpassPubKeyHash,omitempty"`
}
func (x *RosenpassConfig) Reset() {
@@ -458,20 +447,6 @@ func (x *RosenpassConfig) GetRosenpassServerAddr() string {
return ""
}
func (x *RosenpassConfig) GetRosenpassPubKeyHash() []byte {
if x != nil {
return x.RosenpassPubKeyHash
}
return nil
}
func (x *RosenpassConfig) GetAcknowledgedRosenpassPubKeyHash() []byte {
if x != nil {
return x.AcknowledgedRosenpassPubKeyHash
}
return nil
}
var File_signalexchange_proto protoreflect.FileDescriptor
var file_signalexchange_proto_rawDesc = []byte{
@@ -531,35 +506,27 @@ var file_signalexchange_proto_rawDesc = []byte{
0x65, 0x72, 0x49, 0x50, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x22, 0x2e, 0x0a, 0x04, 0x4d, 0x6f,
0x64, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01,
0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, 0x42,
0x09, 0x0a, 0x07, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0xe9, 0x01, 0x0a, 0x0f, 0x52,
0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28,
0x0a, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65,
0x6e, 0x70, 0x61, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f,
0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73,
0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x48, 0x0a, 0x1f,
0x61, 0x63, 0x6b, 0x6e, 0x6f, 0x77, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x64, 0x52, 0x6f, 0x73, 0x65,
0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18,
0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x1f, 0x61, 0x63, 0x6b, 0x6e, 0x6f, 0x77, 0x6c, 0x65, 0x64,
0x67, 0x65, 0x64, 0x52, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b,
0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x32, 0xb9, 0x01, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x61,
0x6c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x4c, 0x0a, 0x04, 0x53, 0x65, 0x6e,
0x64, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e,
0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68,
0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x6e, 0x65,
0x63, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61,
0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67,
0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72,
0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01,
0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x09, 0x0a, 0x07, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0x6d, 0x0a, 0x0f, 0x52, 0x6f,
0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a,
0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e,
0x70, 0x61, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x53,
0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x32, 0xb9, 0x01, 0x0a, 0x0e, 0x53, 0x69,
0x67, 0x6e, 0x61, 0x6c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x4c, 0x0a, 0x04,
0x53, 0x65, 0x6e, 0x64, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63,
0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65,
0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x0d, 0x43, 0x6f,
0x6e, 0x6e, 0x65, 0x63, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x2e, 0x73, 0x69,
0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63,
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e,
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45,
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22,
0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@@ -86,15 +86,4 @@ message RosenpassConfig {
bytes rosenpassPubKey = 1;
// rosenpassServerAddr is an IP:port of the rosenpass service
string rosenpassServerAddr = 2;
// rosenpassPubKeyHash is the SHA256 of the sender's own rosenpassPubKey. It is
// always set when Rosenpass is enabled and lets the receiver detect (via a
// per-peer cache) whether it already holds the sender's full public key,
// avoiding re-sending the large key on every offer/answer.
bytes rosenpassPubKeyHash = 3;
// acknowledgedRosenpassPubKeyHash is the SHA256 of the remote peer's rosenpassPubKey
// that the sender currently holds cached. When it matches the receiver's own key hash
// the receiver may omit its full rosenpassPubKey from the message. Empty means the
// sender does not have the remote key and needs it sent in full. Absent from peers
// that predate this field, which keeps them receiving the full key as before.
bytes acknowledgedRosenpassPubKeyHash = 4;
}