mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 12:59:08 +02:00
[client,management] sync 0.74.4 changes (#6727)
* [management] fix: prevent reverse proxy domain from being pushed as DNS search domain by @blaugrau90 in https://github.com/netbirdio/netbird/pull/6498 * [client] Recover from rosenpass key desync by @lixmal in https://github.com/netbirdio/netbird/pull/6714 * [client] Bump golang.org/x/crypto to v0.54.0 by @lixmal in https://github.com/netbirdio/netbird/pull/6709 * [client] fix MDM managementURL conflict on default-port URL echo by @riccardomanfrin in https://github.com/netbirdio/netbird/pull/6672 * [client] Update gopsutil to v4 by @mlsmaycon in https://github.com/netbirdio/netbird/pull/6688 * [client] Fix hanging status command during relay dial by @theodorsm in https://github.com/netbirdio/netbird/pull/6694 --------- Co-authored-by: Theodor Midtlien <theodor@midtlien.com> Co-authored-by: blaugrau90 <61945343+blaugrau90@users.noreply.github.com> Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com>
This commit is contained in:
co-authored by
Theodor Midtlien
blaugrau90
Viktor Liu
parent
8e02154bf5
commit
30d15ecc3d
@@ -551,7 +551,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
} else {
|
} else {
|
||||||
log.Infof("running rosenpass in strict mode")
|
log.Infof("running rosenpass in strict mode")
|
||||||
}
|
}
|
||||||
e.rpManager, err = rosenpass.NewManager(e.config.PreSharedKey, e.config.WgIfaceName)
|
e.rpManager, err = rosenpass.NewManager(e.config.PreSharedKey, e.config.WgIfaceName, publicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create rosenpass manager: %w", err)
|
return fmt.Errorf("create rosenpass manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ import (
|
|||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// wgTimeoutEscalationThreshold is the number of consecutive WireGuard
|
||||||
|
// handshake timeouts after which the rosenpass state for the peer is
|
||||||
|
// considered desynced and gets reset.
|
||||||
|
const wgTimeoutEscalationThreshold = 3
|
||||||
|
|
||||||
// MetricsRecorder is an interface for recording peer connection metrics
|
// MetricsRecorder is an interface for recording peer connection metrics
|
||||||
type MetricsRecorder interface {
|
type MetricsRecorder interface {
|
||||||
RecordConnectionStages(
|
RecordConnectionStages(
|
||||||
@@ -118,6 +123,9 @@ type Conn struct {
|
|||||||
wgWatcher *WGWatcher
|
wgWatcher *WGWatcher
|
||||||
wgWatcherWg sync.WaitGroup
|
wgWatcherWg sync.WaitGroup
|
||||||
wgWatcherCancel context.CancelFunc
|
wgWatcherCancel context.CancelFunc
|
||||||
|
// wgTimeouts counts consecutive WireGuard handshake timeouts without a
|
||||||
|
// successful handshake in between. Guarded by mu.
|
||||||
|
wgTimeouts int
|
||||||
|
|
||||||
// used to store the remote Rosenpass key for Relayed connection in case of connection update from ice
|
// used to store the remote Rosenpass key for Relayed connection in case of connection update from ice
|
||||||
rosenpassRemoteKey []byte
|
rosenpassRemoteKey []byte
|
||||||
@@ -683,6 +691,29 @@ func (conn *Conn) onWGDisconnected() {
|
|||||||
default:
|
default:
|
||||||
conn.Log.Debugf("No active connection to close on WG timeout")
|
conn.Log.Debugf("No active connection to close on WG timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn.escalateWGTimeoutLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// escalateWGTimeoutLocked resets the peer's rosenpass state after repeated
|
||||||
|
// handshake timeouts. With rosenpass enabled, persistent timeouts mean the
|
||||||
|
// preshared keys have desynced; the renewal exchange runs over the dead
|
||||||
|
// tunnel and cannot resync them. Reporting the peer disconnected drops its
|
||||||
|
// rosenpass state, so the next connection configuration programs the
|
||||||
|
// rendezvous key and the tunnel can bootstrap again. Callers must hold mu.
|
||||||
|
func (conn *Conn) escalateWGTimeoutLocked() {
|
||||||
|
if conn.config.RosenpassConfig.PubKey == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.wgTimeouts++
|
||||||
|
if conn.wgTimeouts < wgTimeoutEscalationThreshold || conn.onDisconnected == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.wgTimeouts = 0
|
||||||
|
|
||||||
|
conn.Log.Warnf("%d consecutive WireGuard handshake timeouts, resetting rosenpass state for peer", wgTimeoutEscalationThreshold)
|
||||||
|
conn.onDisconnected(conn.config.WgConfig.RemoteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []byte, updateTime time.Time) {
|
func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []byte, updateTime time.Time) {
|
||||||
@@ -812,7 +843,7 @@ func (conn *Conn) enableWgWatcherIfNeeded(enabledTime time.Time) {
|
|||||||
conn.wgWatcherWg.Add(1)
|
conn.wgWatcherWg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer conn.wgWatcherWg.Done()
|
defer conn.wgWatcherWg.Done()
|
||||||
conn.wgWatcher.EnableWgWatcher(wgWatcherCtx, enabledTime, conn.onWGDisconnected, conn.onWGHandshakeSuccess)
|
conn.wgWatcher.EnableWgWatcher(wgWatcherCtx, enabledTime, conn.onWGDisconnected, conn.onWGHandshakeSuccess, conn.onWGCheckSuccess)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,6 +923,15 @@ func (conn *Conn) onWGHandshakeSuccess(when time.Time) {
|
|||||||
conn.recordConnectionMetrics()
|
conn.recordConnectionMetrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onWGCheckSuccess is called for every watcher check that observed a fresh
|
||||||
|
// handshake, including handshakes of connections that were already up when
|
||||||
|
// the watcher started.
|
||||||
|
func (conn *Conn) onWGCheckSuccess() {
|
||||||
|
conn.mu.Lock()
|
||||||
|
conn.wgTimeouts = 0
|
||||||
|
conn.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// recordConnectionMetrics records connection stage timestamps as metrics
|
// recordConnectionMetrics records connection stage timestamps as metrics
|
||||||
func (conn *Conn) recordConnectionMetrics() {
|
func (conn *Conn) recordConnectionMetrics() {
|
||||||
if conn.metricsRecorder == nil {
|
if conn.metricsRecorder == nil {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
@@ -304,3 +305,84 @@ func TestConn_presharedKey_RosenpassManaged(t *testing.T) {
|
|||||||
t.Fatalf("expected non-nil presharedKey before Rosenpass manages PSK")
|
t.Fatalf("expected non-nil presharedKey before Rosenpass manages PSK")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newWGTimeoutTestConn(rosenpassEnabled bool, disconnected *[]string) *Conn {
|
||||||
|
cfg := ConnConfig{
|
||||||
|
Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
WgConfig: WgConfig{RemoteKey: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU="},
|
||||||
|
}
|
||||||
|
if rosenpassEnabled {
|
||||||
|
cfg.RosenpassConfig = RosenpassConfig{PubKey: []byte("dummykey")}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &Conn{
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: cfg,
|
||||||
|
Log: log.WithField("peer", cfg.Key),
|
||||||
|
metricsStages: &MetricsStages{},
|
||||||
|
}
|
||||||
|
conn.SetOnDisconnected(func(remotePeer string) {
|
||||||
|
*disconnected = append(*disconnected, remotePeer)
|
||||||
|
})
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConn_onWGDisconnected_EscalatesToRosenpassReset: repeated handshake
|
||||||
|
// timeouts with rosenpass enabled mean the preshared keys have desynced. The
|
||||||
|
// renewal exchange runs over the dead tunnel and cannot resync them, so after
|
||||||
|
// wgTimeoutEscalationThreshold consecutive timeouts the conn must report the
|
||||||
|
// peer disconnected, dropping its rosenpass state so the next configuration
|
||||||
|
// programs the rendezvous key.
|
||||||
|
func TestConn_onWGDisconnected_EscalatesToRosenpassReset(t *testing.T) {
|
||||||
|
var disconnected []string
|
||||||
|
conn := newWGTimeoutTestConn(true, &disconnected)
|
||||||
|
|
||||||
|
for i := 0; i < wgTimeoutEscalationThreshold-1; i++ {
|
||||||
|
conn.onWGDisconnected()
|
||||||
|
}
|
||||||
|
assert.Empty(t, disconnected, "escalation must not fire below the threshold")
|
||||||
|
|
||||||
|
conn.onWGDisconnected()
|
||||||
|
assert.Equal(t, []string{conn.config.WgConfig.RemoteKey}, disconnected,
|
||||||
|
"reaching the threshold must report the peer disconnected once")
|
||||||
|
|
||||||
|
for i := 0; i < wgTimeoutEscalationThreshold-1; i++ {
|
||||||
|
conn.onWGDisconnected()
|
||||||
|
}
|
||||||
|
assert.Len(t, disconnected, 1, "escalation must restart counting after firing")
|
||||||
|
|
||||||
|
conn.onWGDisconnected()
|
||||||
|
assert.Len(t, disconnected, 2, "continued timeouts must escalate again")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConn_onWGDisconnected_CheckSuccessResetsEscalation: a successful
|
||||||
|
// handshake between timeouts means the tunnel recovered; the counter must
|
||||||
|
// start over.
|
||||||
|
func TestConn_onWGDisconnected_CheckSuccessResetsEscalation(t *testing.T) {
|
||||||
|
var disconnected []string
|
||||||
|
conn := newWGTimeoutTestConn(true, &disconnected)
|
||||||
|
|
||||||
|
for i := 0; i < wgTimeoutEscalationThreshold-1; i++ {
|
||||||
|
conn.onWGDisconnected()
|
||||||
|
}
|
||||||
|
conn.onWGCheckSuccess()
|
||||||
|
|
||||||
|
for i := 0; i < wgTimeoutEscalationThreshold-1; i++ {
|
||||||
|
conn.onWGDisconnected()
|
||||||
|
}
|
||||||
|
assert.Empty(t, disconnected, "handshake success must reset the timeout count")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConn_onWGDisconnected_NoEscalationWithoutRosenpass: without rosenpass
|
||||||
|
// there is no per-peer key state to reset; repeated timeouts must not report
|
||||||
|
// disconnects.
|
||||||
|
func TestConn_onWGDisconnected_NoEscalationWithoutRosenpass(t *testing.T) {
|
||||||
|
var disconnected []string
|
||||||
|
conn := newWGTimeoutTestConn(false, &disconnected)
|
||||||
|
|
||||||
|
for i := 0; i < wgTimeoutEscalationThreshold*3; i++ {
|
||||||
|
conn.onWGDisconnected()
|
||||||
|
}
|
||||||
|
assert.Empty(t, disconnected, "escalation must be limited to rosenpass connections")
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,9 +71,11 @@ func (w *WGWatcher) PrepareInitialHandshake() (ok bool) {
|
|||||||
|
|
||||||
// EnableWgWatcher runs the WireGuard watcher loop using the handshake baseline captured by
|
// EnableWgWatcher runs the WireGuard watcher loop using the handshake baseline captured by
|
||||||
// PrepareInitialHandshake. The watcher runs until ctx is cancelled. Caller is responsible
|
// PrepareInitialHandshake. The watcher runs until ctx is cancelled. Caller is responsible
|
||||||
// for context lifecycle management.
|
// for context lifecycle management. onHandshakeSuccessFn is called only for the first
|
||||||
func (w *WGWatcher) EnableWgWatcher(ctx context.Context, enabledTime time.Time, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time)) {
|
// handshake observed by this run, onCheckSuccessFn for every check that observed a fresh
|
||||||
w.periodicHandshakeCheck(ctx, onDisconnectedFn, onHandshakeSuccessFn, enabledTime, w.initialHandshake)
|
// handshake, including the first.
|
||||||
|
func (w *WGWatcher) EnableWgWatcher(ctx context.Context, enabledTime time.Time, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), onCheckSuccessFn func()) {
|
||||||
|
w.periodicHandshakeCheck(ctx, onDisconnectedFn, onHandshakeSuccessFn, onCheckSuccessFn, enabledTime, w.initialHandshake)
|
||||||
|
|
||||||
w.muEnabled.Lock()
|
w.muEnabled.Lock()
|
||||||
w.enabled = false
|
w.enabled = false
|
||||||
@@ -90,7 +92,7 @@ func (w *WGWatcher) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wgStateCheck help to check the state of the WireGuard handshake and relay connection
|
// wgStateCheck help to check the state of the WireGuard handshake and relay connection
|
||||||
func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), enabledTime time.Time, initialHandshake time.Time) {
|
func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn func(), onHandshakeSuccessFn func(when time.Time), onCheckSuccessFn func(), enabledTime time.Time, initialHandshake time.Time) {
|
||||||
w.log.Infof("WireGuard watcher started")
|
w.log.Infof("WireGuard watcher started")
|
||||||
|
|
||||||
timer := time.NewTimer(wgHandshakeOvertime)
|
timer := time.NewTimer(wgHandshakeOvertime)
|
||||||
@@ -117,6 +119,10 @@ func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, onDisconnectedFn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if onCheckSuccessFn != nil && ctx.Err() == nil {
|
||||||
|
onCheckSuccessFn()
|
||||||
|
}
|
||||||
|
|
||||||
lastHandshake = *handshake
|
lastHandshake = *handshake
|
||||||
|
|
||||||
resetTime := time.Until(handshake.Add(checkPeriod))
|
resetTime := time.Until(handshake.Add(checkPeriod))
|
||||||
|
|||||||
@@ -24,6 +24,72 @@ func (m *MocWgIface) disconnect() {
|
|||||||
m.stop = true
|
m.stop = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockHandshakeStats struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
handshake time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHandshakeStats) GetStats() (map[string]configurer.WGStats, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return map[string]configurer.WGStats{"": {LastHandshake: m.handshake}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHandshakeStats) advance() {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
m.handshake = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWGWatcher_CheckSuccessCallback: onCheckSuccessFn must fire for a fresh
|
||||||
|
// handshake even when the watcher started with an existing handshake baseline,
|
||||||
|
// the case where onHandshakeSuccessFn stays silent.
|
||||||
|
func TestWGWatcher_CheckSuccessCallback(t *testing.T) {
|
||||||
|
// checkPeriod bounds how stale a handshake may be before the watcher treats it
|
||||||
|
// as a suspended-machine timeout. The first check fires after wgHandshakeOvertime,
|
||||||
|
// so keep checkPeriod well above any scheduling jitter to avoid a false timeout
|
||||||
|
// converting the expected success into a disconnect on a loaded runner.
|
||||||
|
checkPeriod = 1 * time.Minute
|
||||||
|
wgHandshakeOvertime = 1 * time.Second
|
||||||
|
|
||||||
|
mlog := log.WithField("peer", "tet")
|
||||||
|
// Use an old baseline so advance() yields a strictly newer handshake even on
|
||||||
|
// platforms with coarse clock resolution (Windows), where two time.Now() calls
|
||||||
|
// microseconds apart can return the same instant and read as a timed-out handshake.
|
||||||
|
stats := &mockHandshakeStats{handshake: time.Now().Add(-time.Hour)}
|
||||||
|
watcher := NewWGWatcher(mlog, stats, "", newStateDump("peer", mlog, &Status{}))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
require.True(t, watcher.PrepareInitialHandshake())
|
||||||
|
|
||||||
|
firstHandshake := make(chan struct{}, 1)
|
||||||
|
checkSuccess := make(chan struct{}, 1)
|
||||||
|
go watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) {
|
||||||
|
firstHandshake <- struct{}{}
|
||||||
|
}, func() {
|
||||||
|
select {
|
||||||
|
case checkSuccess <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stats.advance()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-checkSuccess:
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Errorf("timeout waiting for check success callback")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-firstHandshake:
|
||||||
|
t.Errorf("first-handshake callback must not fire for a non-zero baseline")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWGWatcher_EnableWgWatcher(t *testing.T) {
|
func TestWGWatcher_EnableWgWatcher(t *testing.T) {
|
||||||
checkPeriod = 5 * time.Second
|
checkPeriod = 5 * time.Second
|
||||||
wgHandshakeOvertime = 1 * time.Second
|
wgHandshakeOvertime = 1 * time.Second
|
||||||
@@ -44,7 +110,7 @@ func TestWGWatcher_EnableWgWatcher(t *testing.T) {
|
|||||||
onDisconnected <- struct{}{}
|
onDisconnected <- struct{}{}
|
||||||
}, func(when time.Time) {
|
}, func(when time.Time) {
|
||||||
mlog.Infof("onHandshakeSuccess: %v", when)
|
mlog.Infof("onHandshakeSuccess: %v", when)
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// wait for initial reading
|
// wait for initial reading
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
@@ -73,7 +139,7 @@ func TestWGWatcher_ReEnable(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) {})
|
watcher.EnableWgWatcher(ctx, time.Now(), func() {}, func(when time.Time) {}, nil)
|
||||||
}()
|
}()
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
@@ -89,7 +155,7 @@ func TestWGWatcher_ReEnable(t *testing.T) {
|
|||||||
onDisconnected := make(chan struct{}, 1)
|
onDisconnected := make(chan struct{}, 1)
|
||||||
go watcher.EnableWgWatcher(ctx, time.Now(), func() {
|
go watcher.EnableWgWatcher(ctx, time.Now(), func() {
|
||||||
onDisconnected <- struct{}{}
|
onDisconnected <- struct{}{}
|
||||||
}, func(when time.Time) {})
|
}, func(when time.Time) {}, nil)
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
mocWgIface.disconnect()
|
mocWgIface.disconnect()
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type rpServer interface {
|
|||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
ifaceName string
|
ifaceName string
|
||||||
|
localWgKey wgtypes.Key
|
||||||
spk []byte
|
spk []byte
|
||||||
ssk []byte
|
ssk []byte
|
||||||
rpKeyHash string
|
rpKeyHash string
|
||||||
@@ -51,8 +52,9 @@ type Manager struct {
|
|||||||
wgIface PresharedKeySetter
|
wgIface PresharedKeySetter
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new Rosenpass manager
|
// NewManager creates a new Rosenpass manager. localWgKey is the local
|
||||||
func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error) {
|
// 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()
|
public, secret, err := rp.GenerateKeyPair()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -62,6 +64,7 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error)
|
|||||||
log.Tracef("generated new rosenpass key pair with public key %s", rpKeyHash)
|
log.Tracef("generated new rosenpass key pair with public key %s", rpKeyHash)
|
||||||
return &Manager{
|
return &Manager{
|
||||||
ifaceName: wgIfaceName,
|
ifaceName: wgIfaceName,
|
||||||
|
localWgKey: localWgKey,
|
||||||
rpKeyHash: rpKeyHash,
|
rpKeyHash: rpKeyHash,
|
||||||
spk: public,
|
spk: public,
|
||||||
ssk: secret,
|
ssk: secret,
|
||||||
@@ -73,7 +76,7 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error)
|
|||||||
// nil receiver in addPeer -> m.rpWgHandler.AddPeer. generateConfig will
|
// nil receiver in addPeer -> m.rpWgHandler.AddPeer. generateConfig will
|
||||||
// replace it with a fresh handler on each Run() to clear stale peer
|
// replace it with a fresh handler on each Run() to clear stale peer
|
||||||
// state from previous engine sessions.
|
// state from previous engine sessions.
|
||||||
rpWgHandler: NewNetbirdHandler(),
|
rpWgHandler: NewNetbirdHandler((*[32]byte)(preSharedKey), localWgKey),
|
||||||
lock: sync.Mutex{},
|
lock: sync.Mutex{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -161,7 +164,7 @@ func (m *Manager) generateConfig() (rp.Config, error) {
|
|||||||
cfg.Peers = []rp.PeerConfig{}
|
cfg.Peers = []rp.PeerConfig{}
|
||||||
|
|
||||||
m.lock.Lock()
|
m.lock.Lock()
|
||||||
m.rpWgHandler = NewNetbirdHandler()
|
m.rpWgHandler = NewNetbirdHandler(m.preSharedKey, m.localWgKey)
|
||||||
if m.wgIface != nil {
|
if m.wgIface != nil {
|
||||||
m.rpWgHandler.SetInterface(m.wgIface)
|
m.rpWgHandler.SetInterface(m.wgIface)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func newTestManager(spkFirstByte byte, mock *mockServer) *Manager {
|
|||||||
ssk: make([]byte, 32),
|
ssk: make([]byte, 32),
|
||||||
rpKeyHash: "test-hash",
|
rpKeyHash: "test-hash",
|
||||||
rpPeerIDs: make(map[string]*rp.PeerID),
|
rpPeerIDs: make(map[string]*rp.PeerID),
|
||||||
rpWgHandler: NewNetbirdHandler(),
|
rpWgHandler: NewNetbirdHandler(nil, wgtypes.Key{0x01}),
|
||||||
server: mock,
|
server: mock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ func TestAddPeer_NilServer_ReturnsErrorNoCrash(t *testing.T) {
|
|||||||
// issue #4341 cannot occur in the window between NewManager and Run().
|
// issue #4341 cannot occur in the window between NewManager and Run().
|
||||||
func TestNewManager_PreInitializesHandler(t *testing.T) {
|
func TestNewManager_PreInitializesHandler(t *testing.T) {
|
||||||
psk := wgtypes.Key{}
|
psk := wgtypes.Key{}
|
||||||
m, err := NewManager(&psk, "wt0")
|
m, err := NewManager(&psk, "wt0", wgtypes.Key{0x01})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, m.rpWgHandler, "rpWgHandler must be initialized in NewManager")
|
require.NotNil(t, m.rpWgHandler, "rpWgHandler must be initialized in NewManager")
|
||||||
}
|
}
|
||||||
@@ -329,10 +329,10 @@ func TestIsPresharedKeyInitialized_AddedButNotHandshaken_ReturnsFalse(t *testing
|
|||||||
require.False(t, m.IsPresharedKeyInitialized(wgKey))
|
require.False(t, m.IsPresharedKeyInitialized(wgKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NetbirdHandler.outputKey ----------------------------------------------
|
// --- NetbirdHandler.applyKey ----------------------------------------------
|
||||||
|
|
||||||
func TestHandler_OutputKey_FirstCallUsesUpdateOnlyFalse(t *testing.T) {
|
func TestHandler_ApplyKey_FirstCallUsesUpdateOnlyFalse(t *testing.T) {
|
||||||
h := NewNetbirdHandler()
|
h := NewNetbirdHandler(nil, wgtypes.Key{0x01})
|
||||||
iface := &mockIface{}
|
iface := &mockIface{}
|
||||||
h.SetInterface(iface)
|
h.SetInterface(iface)
|
||||||
|
|
||||||
@@ -348,8 +348,8 @@ func TestHandler_OutputKey_FirstCallUsesUpdateOnlyFalse(t *testing.T) {
|
|||||||
require.Equal(t, wgKey.String(), iface.calls[0].peerKey)
|
require.Equal(t, wgKey.String(), iface.calls[0].peerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_OutputKey_SubsequentCallsUseUpdateOnlyTrue(t *testing.T) {
|
func TestHandler_ApplyKey_SubsequentCallsUseUpdateOnlyTrue(t *testing.T) {
|
||||||
h := NewNetbirdHandler()
|
h := NewNetbirdHandler(nil, wgtypes.Key{0x01})
|
||||||
iface := &mockIface{}
|
iface := &mockIface{}
|
||||||
h.SetInterface(iface)
|
h.SetInterface(iface)
|
||||||
|
|
||||||
@@ -364,8 +364,8 @@ func TestHandler_OutputKey_SubsequentCallsUseUpdateOnlyTrue(t *testing.T) {
|
|||||||
require.True(t, iface.calls[1].updateOnly, "subsequent rotations must use updateOnly=true")
|
require.True(t, iface.calls[1].updateOnly, "subsequent rotations must use updateOnly=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_OutputKey_NilInterface_NoCrashNoCall(t *testing.T) {
|
func TestHandler_ApplyKey_NilInterface_NoCrashNoCall(t *testing.T) {
|
||||||
h := NewNetbirdHandler()
|
h := NewNetbirdHandler(nil, wgtypes.Key{0x01})
|
||||||
// no SetInterface — iface remains nil
|
// no SetInterface — iface remains nil
|
||||||
pid := rp.PeerID{0x03}
|
pid := rp.PeerID{0x03}
|
||||||
h.AddPeer(pid, "wt0", rp.Key(wgtypes.Key{}))
|
h.AddPeer(pid, "wt0", rp.Key(wgtypes.Key{}))
|
||||||
@@ -374,8 +374,8 @@ func TestHandler_OutputKey_NilInterface_NoCrashNoCall(t *testing.T) {
|
|||||||
h.HandshakeCompleted(pid, rp.Key{})
|
h.HandshakeCompleted(pid, rp.Key{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_OutputKey_UnknownPeer_NoCall(t *testing.T) {
|
func TestHandler_ApplyKey_UnknownPeer_NoCall(t *testing.T) {
|
||||||
h := NewNetbirdHandler()
|
h := NewNetbirdHandler(nil, wgtypes.Key{0x01})
|
||||||
iface := &mockIface{}
|
iface := &mockIface{}
|
||||||
h.SetInterface(iface)
|
h.SetInterface(iface)
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ func TestHandler_OutputKey_UnknownPeer_NoCall(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_RemovePeer_ClearsInitializedState(t *testing.T) {
|
func TestHandler_RemovePeer_ClearsInitializedState(t *testing.T) {
|
||||||
h := NewNetbirdHandler()
|
h := NewNetbirdHandler(nil, wgtypes.Key{0x01})
|
||||||
iface := &mockIface{}
|
iface := &mockIface{}
|
||||||
h.SetInterface(iface)
|
h.SetInterface(iface)
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ func TestHandler_RemovePeer_ClearsInitializedState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_SetInterfaceAfterAddPeer_StillReceivesKey(t *testing.T) {
|
func TestHandler_SetInterfaceAfterAddPeer_StillReceivesKey(t *testing.T) {
|
||||||
h := NewNetbirdHandler()
|
h := NewNetbirdHandler(nil, wgtypes.Key{0x01})
|
||||||
pid := rp.PeerID{0x05}
|
pid := rp.PeerID{0x05}
|
||||||
wgKey := wgtypes.Key{0xEE}
|
wgKey := wgtypes.Key{0xEE}
|
||||||
h.AddPeer(pid, "wt0", rp.Key(wgKey))
|
h.AddPeer(pid, "wt0", rp.Key(wgKey))
|
||||||
|
|||||||
@@ -18,19 +18,34 @@ type PresharedKeySetter interface {
|
|||||||
type wireGuardPeer struct {
|
type wireGuardPeer struct {
|
||||||
Interface string
|
Interface string
|
||||||
PublicKey rp.Key
|
PublicKey rp.Key
|
||||||
|
// initialized is true once a completed exchange has set a
|
||||||
|
// Rosenpass-managed PSK for this peer.
|
||||||
|
initialized bool
|
||||||
|
// chainKey is the key output by the last completed exchange, advanced by
|
||||||
|
// one ratchet step on expiry. Nil until the first exchange completes and
|
||||||
|
// after the peer has fallen back to the rendezvous key.
|
||||||
|
chainKey *wgtypes.Key
|
||||||
|
// expiries counts failed renewals since the last completed exchange.
|
||||||
|
expiries int
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetbirdHandler struct {
|
type NetbirdHandler struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
iface PresharedKeySetter
|
iface PresharedKeySetter
|
||||||
peers map[rp.PeerID]wireGuardPeer
|
// preSharedKey is the account-level preshared key, used as the rendezvous
|
||||||
initializedPeers map[rp.PeerID]bool
|
// key when set. Nil means the deterministic seed key is used instead.
|
||||||
|
preSharedKey *[32]byte
|
||||||
|
// localWgKey is the local WireGuard public key, one of the two inputs to
|
||||||
|
// the deterministic seed key.
|
||||||
|
localWgKey wgtypes.Key
|
||||||
|
peers map[rp.PeerID]*wireGuardPeer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetbirdHandler() *NetbirdHandler {
|
func NewNetbirdHandler(preSharedKey *[32]byte, localWgKey wgtypes.Key) *NetbirdHandler {
|
||||||
return &NetbirdHandler{
|
return &NetbirdHandler{
|
||||||
peers: map[rp.PeerID]wireGuardPeer{},
|
preSharedKey: preSharedKey,
|
||||||
initializedPeers: map[rp.PeerID]bool{},
|
localWgKey: localWgKey,
|
||||||
|
peers: map[rp.PeerID]*wireGuardPeer{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +57,16 @@ func (h *NetbirdHandler) SetInterface(iface PresharedKeySetter) {
|
|||||||
h.iface = iface
|
h.iface = iface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddPeer registers a peer with the handler. Re-adding a known peer (every
|
||||||
|
// reconnection does) keeps its key recovery state.
|
||||||
func (h *NetbirdHandler) AddPeer(pid rp.PeerID, intf string, pk rp.Key) {
|
func (h *NetbirdHandler) AddPeer(pid rp.PeerID, intf string, pk rp.Key) {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
defer h.mu.Unlock()
|
defer h.mu.Unlock()
|
||||||
h.peers[pid] = wireGuardPeer{
|
if existing, ok := h.peers[pid]; ok && existing.PublicKey == pk {
|
||||||
|
existing.Interface = intf
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.peers[pid] = &wireGuardPeer{
|
||||||
Interface: intf,
|
Interface: intf,
|
||||||
PublicKey: pk,
|
PublicKey: pk,
|
||||||
}
|
}
|
||||||
@@ -55,7 +76,6 @@ func (h *NetbirdHandler) RemovePeer(pid rp.PeerID) {
|
|||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
defer h.mu.Unlock()
|
defer h.mu.Unlock()
|
||||||
delete(h.peers, pid)
|
delete(h.peers, pid)
|
||||||
delete(h.initializedPeers, pid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPeerInitialized returns true if Rosenpass has completed a handshake
|
// IsPeerInitialized returns true if Rosenpass has completed a handshake
|
||||||
@@ -63,50 +83,120 @@ func (h *NetbirdHandler) RemovePeer(pid rp.PeerID) {
|
|||||||
func (h *NetbirdHandler) IsPeerInitialized(pid rp.PeerID) bool {
|
func (h *NetbirdHandler) IsPeerInitialized(pid rp.PeerID) bool {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
defer h.mu.Unlock()
|
defer h.mu.Unlock()
|
||||||
return h.initializedPeers[pid]
|
peer, ok := h.peers[pid]
|
||||||
|
return ok && peer.initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandshakeCompleted programs the freshly exchanged output key and resets the
|
||||||
|
// peer's key recovery state.
|
||||||
func (h *NetbirdHandler) HandshakeCompleted(pid rp.PeerID, key rp.Key) {
|
func (h *NetbirdHandler) HandshakeCompleted(pid rp.PeerID, key rp.Key) {
|
||||||
h.outputKey(rp.KeyOutputReasonStale, pid, key)
|
psk := wgtypes.Key(key)
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NetbirdHandler) HandshakeExpired(pid rp.PeerID) {
|
|
||||||
key, _ := rp.GeneratePresharedKey()
|
|
||||||
h.outputKey(rp.KeyOutputReasonStale, pid, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *NetbirdHandler) outputKey(_ rp.KeyOutputReason, pid rp.PeerID, psk rp.Key) {
|
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
iface := h.iface
|
defer h.mu.Unlock()
|
||||||
wg, ok := h.peers[pid]
|
|
||||||
isInitialized := h.initializedPeers[pid]
|
|
||||||
h.mu.Unlock()
|
|
||||||
|
|
||||||
if iface == nil {
|
peer, ok := h.peers[pid]
|
||||||
log.Warn("rosenpass: interface not set, cannot update preshared key")
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if peer.expiries > 0 {
|
||||||
|
log.Infof("rosenpass exchange completed for peer %s after %d expired renewals", wgtypes.Key(peer.PublicKey), peer.expiries)
|
||||||
|
}
|
||||||
|
// chainKey tracks the shared exchange output regardless of the local write
|
||||||
|
// outcome, so both ends still converge on the next expiry.
|
||||||
|
peer.chainKey = &psk
|
||||||
|
peer.expiries = 0
|
||||||
|
if !h.applyKeyLocked(pid, psk, peer.initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandshakeExpired replaces the expired key. The renewal exchange runs over
|
||||||
|
// the tunnel keyed by the PSK itself, so the replacement must be derivable on
|
||||||
|
// both ends without communication: the first expiry ratchets the last shared
|
||||||
|
// key forward, repeated expiries (and expiries without a completed exchange)
|
||||||
|
// fall back to the rendezvous key and drop the peer out of the initialized
|
||||||
|
// state so connection reconfigurations reprogram the rendezvous key as well.
|
||||||
|
func (h *NetbirdHandler) HandshakeExpired(pid rp.PeerID) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
peer, ok := h.peers[pid]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peerKey := wgtypes.Key(wg.PublicKey).String()
|
peer.expiries++
|
||||||
pskKey := wgtypes.Key(psk)
|
|
||||||
|
|
||||||
// Use updateOnly=true for later rotations (peer already has Rosenpass PSK)
|
var psk wgtypes.Key
|
||||||
// Use updateOnly=false for first rotation (peer has original/empty PSK)
|
if peer.chainKey != nil && peer.expiries == 1 {
|
||||||
if err := iface.SetPresharedKey(peerKey, pskKey, isInitialized); err != nil {
|
log.Infof("rosenpass key for peer %s expired without renewal, advancing to ratcheted key", wgtypes.Key(peer.PublicKey))
|
||||||
|
psk = RatchetKey(*peer.chainKey)
|
||||||
|
peer.chainKey = &psk
|
||||||
|
} else {
|
||||||
|
rendezvous, err := h.rendezvousKey(peer)
|
||||||
|
if err != nil {
|
||||||
|
// Fail closed: without a rendezvous key the expired key must
|
||||||
|
// still be rotated out, even if the replacement is unusable.
|
||||||
|
log.Errorf("failed to derive rendezvous key, replacing expired key with a random one: %v", err)
|
||||||
|
h.applyRandomKeyLocked(pid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Warnf("rosenpass key for peer %s expired %d times without renewal, falling back to the rendezvous key", wgtypes.Key(peer.PublicKey), peer.expiries)
|
||||||
|
psk = rendezvous
|
||||||
|
peer.chainKey = nil
|
||||||
|
peer.initialized = false
|
||||||
|
}
|
||||||
|
|
||||||
|
h.applyKeyLocked(pid, psk, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rendezvousKey returns the key both ends converge on without communication:
|
||||||
|
// the account-level preshared key when configured, the deterministic seed key
|
||||||
|
// otherwise. It mirrors the key that peer connections program when Rosenpass
|
||||||
|
// does not manage the peer yet.
|
||||||
|
func (h *NetbirdHandler) rendezvousKey(peer *wireGuardPeer) (wgtypes.Key, error) {
|
||||||
|
if h.preSharedKey != nil {
|
||||||
|
return *h.preSharedKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seed, err := DeterministicSeedKey(h.localWgKey.String(), wgtypes.Key(peer.PublicKey).String())
|
||||||
|
if err != nil {
|
||||||
|
return wgtypes.Key{}, err
|
||||||
|
}
|
||||||
|
return *seed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyKeyLocked writes the preshared key for the peer to the WireGuard
|
||||||
|
// interface and reports whether the write succeeded. Callers must hold h.mu
|
||||||
|
// for the whole state-mutation-plus-write so that a concurrent completion and
|
||||||
|
// expiry cannot reorder their writes relative to the in-memory chain key.
|
||||||
|
func (h *NetbirdHandler) applyKeyLocked(pid rp.PeerID, psk wgtypes.Key, updateOnly bool) bool {
|
||||||
|
peer, ok := h.peers[pid]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.iface == nil {
|
||||||
|
log.Warn("rosenpass: interface not set, cannot update preshared key")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
peerKey := wgtypes.Key(peer.PublicKey).String()
|
||||||
|
if err := h.iface.SetPresharedKey(peerKey, psk, updateOnly); err != nil {
|
||||||
log.Errorf("Failed to apply rosenpass key: %v", err)
|
log.Errorf("Failed to apply rosenpass key: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NetbirdHandler) applyRandomKeyLocked(pid rp.PeerID) {
|
||||||
|
key, err := rp.GeneratePresharedKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to generate random preshared key: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
h.applyKeyLocked(pid, wgtypes.Key(key), true)
|
||||||
// Mark peer as isInitialized after the successful first rotation
|
|
||||||
if !isInitialized {
|
|
||||||
h.mu.Lock()
|
|
||||||
if _, exists := h.peers[pid]; exists {
|
|
||||||
h.initializedPeers[pid] = true
|
|
||||||
}
|
|
||||||
h.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package rosenpass
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
rp "cunicu.li/go-rosenpass"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handlerTestLink wires two NetbirdHandlers as the two ends of a single
|
||||||
|
// tunnel: handler A manages the rosenpass peer B and vice versa, the way two
|
||||||
|
// NetBird clients see each other.
|
||||||
|
type handlerTestLink struct {
|
||||||
|
handlerA, handlerB *NetbirdHandler
|
||||||
|
ifaceA, ifaceB *mockIface
|
||||||
|
pidA, pidB rp.PeerID
|
||||||
|
wgKeyA, wgKeyB wgtypes.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandlerTestLink(t *testing.T, preSharedKey *[32]byte) *handlerTestLink {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
link := &handlerTestLink{
|
||||||
|
ifaceA: &mockIface{},
|
||||||
|
ifaceB: &mockIface{},
|
||||||
|
}
|
||||||
|
link.pidA[0] = 0xaa
|
||||||
|
link.pidB[0] = 0xbb
|
||||||
|
link.wgKeyA[31] = 1
|
||||||
|
link.wgKeyB[31] = 2
|
||||||
|
|
||||||
|
link.handlerA = NewNetbirdHandler(preSharedKey, link.wgKeyA)
|
||||||
|
link.handlerB = NewNetbirdHandler(preSharedKey, link.wgKeyB)
|
||||||
|
|
||||||
|
link.handlerA.SetInterface(link.ifaceA)
|
||||||
|
link.handlerB.SetInterface(link.ifaceB)
|
||||||
|
|
||||||
|
link.handlerA.AddPeer(link.pidB, "wt0", rp.Key(link.wgKeyB))
|
||||||
|
link.handlerB.AddPeer(link.pidA, "wt0", rp.Key(link.wgKeyA))
|
||||||
|
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete simulates a completed rosenpass exchange: both ends derive the
|
||||||
|
// same output key.
|
||||||
|
func (l *handlerTestLink) complete(osk rp.Key) {
|
||||||
|
l.handlerA.HandshakeCompleted(l.pidB, osk)
|
||||||
|
l.handlerB.HandshakeCompleted(l.pidA, osk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expire simulates a failed key renewal on both ends.
|
||||||
|
func (l *handlerTestLink) expire() {
|
||||||
|
l.handlerA.HandshakeExpired(l.pidB)
|
||||||
|
l.handlerB.HandshakeExpired(l.pidA)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lastPSK(t *testing.T, m *mockIface) wgtypes.Key {
|
||||||
|
t.Helper()
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
require.NotEmpty(t, m.calls, "expected at least one SetPresharedKey call")
|
||||||
|
return m.calls[len(m.calls)-1].psk
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandshakeCompleted_SetsKeyAndInitializes(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
|
||||||
|
require.Equal(t, wgtypes.Key(osk), lastPSK(t, link.ifaceA), "completed exchange must program the osk")
|
||||||
|
require.False(t, link.ifaceA.calls[0].updateOnly, "first rotation must not be update-only")
|
||||||
|
require.True(t, link.handlerA.IsPeerInitialized(link.pidB), "peer must be initialized after first completed exchange")
|
||||||
|
|
||||||
|
link.complete(osk)
|
||||||
|
require.True(t, link.ifaceA.calls[1].updateOnly, "later rotations must be update-only")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeExpired_BothSidesConverge encodes the core recovery invariant:
|
||||||
|
// rosenpass renewals run over the tunnel that the PSK itself keys, so when a
|
||||||
|
// renewal fails on both ends, both ends must fall back to the same key or the
|
||||||
|
// tunnel can never handshake again.
|
||||||
|
func TestHandshakeExpired_BothSidesConverge(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
keyA := lastPSK(t, link.ifaceA)
|
||||||
|
keyB := lastPSK(t, link.ifaceB)
|
||||||
|
require.NotEqual(t, wgtypes.Key(osk), keyA, "expired key must be rotated out")
|
||||||
|
require.Equal(t, keyA, keyB, "both ends must converge on the same key after expiry")
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
require.Equal(t, lastPSK(t, link.ifaceA), lastPSK(t, link.ifaceB),
|
||||||
|
"both ends must still converge after repeated expiries")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeExpired_ExpiryWithoutCompletionConverges covers the bootstrap
|
||||||
|
// case: the initial exchange never completed (the tunnel ran on the rendezvous
|
||||||
|
// key), so an expiry must not replace the working key with an unrecoverable
|
||||||
|
// one on either end.
|
||||||
|
func TestHandshakeExpired_ExpiryWithoutCompletionConverges(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
require.Equal(t, lastPSK(t, link.ifaceA), lastPSK(t, link.ifaceB),
|
||||||
|
"both ends must converge when the exchange never completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeExpired_RepeatedExpiryClearsInitialized: once renewals keep
|
||||||
|
// failing, the peer must drop out of the initialized state so the next
|
||||||
|
// connection reconfiguration reprograms the rendezvous key instead of
|
||||||
|
// preserving a poisoned rosenpass-managed key.
|
||||||
|
func TestHandshakeExpired_RepeatedExpiryClearsInitialized(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
link.expire()
|
||||||
|
|
||||||
|
require.False(t, link.handlerA.IsPeerInitialized(link.pidB),
|
||||||
|
"repeated expiries must clear the initialized state")
|
||||||
|
require.False(t, link.handlerB.IsPeerInitialized(link.pidA),
|
||||||
|
"repeated expiries must clear the initialized state")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeCompleted_AfterExpiryRecovers: a completed exchange after a
|
||||||
|
// desync must fully reset the recovery state.
|
||||||
|
func TestHandshakeCompleted_AfterExpiryRecovers(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk1, osk2 rp.Key
|
||||||
|
osk1[0] = 1
|
||||||
|
osk2[0] = 2
|
||||||
|
|
||||||
|
link.complete(osk1)
|
||||||
|
link.expire()
|
||||||
|
link.expire()
|
||||||
|
|
||||||
|
link.complete(osk2)
|
||||||
|
require.Equal(t, wgtypes.Key(osk2), lastPSK(t, link.ifaceA), "new exchange must program the fresh osk")
|
||||||
|
require.True(t, link.handlerA.IsPeerInitialized(link.pidB), "peer must be initialized again after recovery")
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
require.Equal(t, lastPSK(t, link.ifaceA), lastPSK(t, link.ifaceB),
|
||||||
|
"recovered link must converge again on the next expiry")
|
||||||
|
require.NotEqual(t, wgtypes.Key(osk2), lastPSK(t, link.ifaceA), "expired key must be rotated out")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeExpired_FirstExpiryRatchetsLastKey: the first expiry must
|
||||||
|
// derive the replacement from the last shared key, so an attacker who only
|
||||||
|
// blocks the renewal exchange gains nothing over the previous key.
|
||||||
|
func TestHandshakeExpired_FirstExpiryRatchetsLastKey(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
require.Equal(t, RatchetKey(wgtypes.Key(osk)), lastPSK(t, link.ifaceA),
|
||||||
|
"first expiry must program the ratcheted key")
|
||||||
|
require.True(t, link.handlerA.IsPeerInitialized(link.pidB),
|
||||||
|
"ratchet step must keep the peer initialized so reconfigurations preserve the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeExpired_RepeatedExpiryFallsBackToSeed: once the ratchet key
|
||||||
|
// also fails, both ends must land on the same key that peer connections
|
||||||
|
// program for uninitialized peers, so a reconnect completes the recovery.
|
||||||
|
func TestHandshakeExpired_RepeatedExpiryFallsBackToSeed(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
link.expire()
|
||||||
|
|
||||||
|
seed, err := DeterministicSeedKey(link.wgKeyA.String(), link.wgKeyB.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, *seed, lastPSK(t, link.ifaceA), "repeated expiry must fall back to the seed key")
|
||||||
|
require.Equal(t, *seed, lastPSK(t, link.ifaceB), "repeated expiry must fall back to the seed key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeExpired_ConfiguredPSKUsedAsRendezvous: with an account-level
|
||||||
|
// preshared key configured, the fallback must be that key, matching what peer
|
||||||
|
// connections program for uninitialized peers.
|
||||||
|
func TestHandshakeExpired_ConfiguredPSKUsedAsRendezvous(t *testing.T) {
|
||||||
|
psk := &[32]byte{0x77}
|
||||||
|
link := newHandlerTestLink(t, psk)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
link.expire()
|
||||||
|
|
||||||
|
require.Equal(t, wgtypes.Key(*psk), lastPSK(t, link.ifaceA),
|
||||||
|
"fallback must be the configured preshared key")
|
||||||
|
require.Equal(t, wgtypes.Key(*psk), lastPSK(t, link.ifaceB),
|
||||||
|
"fallback must be the configured preshared key on both ends")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandshakeExpired_ExpiryWritesAreUpdateOnly: expiry replacements must
|
||||||
|
// never create a WireGuard peer that connection management has removed.
|
||||||
|
func TestHandshakeExpired_ExpiryWritesAreUpdateOnly(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
link.expire()
|
||||||
|
|
||||||
|
for _, call := range link.ifaceA.calls[1:] {
|
||||||
|
require.True(t, call.updateOnly, "expiry writes must be update-only")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddPeer_ReAddKeepsRecoveryState: reconnections re-add the peer on every
|
||||||
|
// OnConnected; that must not reset the expiry chain state.
|
||||||
|
func TestAddPeer_ReAddKeepsRecoveryState(t *testing.T) {
|
||||||
|
link := newHandlerTestLink(t, nil)
|
||||||
|
|
||||||
|
var osk rp.Key
|
||||||
|
osk[0] = 0x42
|
||||||
|
link.complete(osk)
|
||||||
|
link.expire()
|
||||||
|
|
||||||
|
link.handlerA.AddPeer(link.pidB, "wt0", rp.Key(link.wgKeyB))
|
||||||
|
require.True(t, link.handlerA.IsPeerInitialized(link.pidB),
|
||||||
|
"re-adding a known peer must keep its state")
|
||||||
|
|
||||||
|
link.expire()
|
||||||
|
seed, err := DeterministicSeedKey(link.wgKeyA.String(), link.wgKeyB.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, *seed, lastPSK(t, link.ifaceA),
|
||||||
|
"second expiry after re-add must continue to the seed fallback")
|
||||||
|
}
|
||||||
@@ -1,11 +1,28 @@
|
|||||||
package rosenpass
|
package rosenpass
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ratchetLabel domain-separates the expiry ratchet from other uses of the
|
||||||
|
// rosenpass output key.
|
||||||
|
const ratchetLabel = "netbird-rosenpass-expiry-ratchet"
|
||||||
|
|
||||||
|
// RatchetKey derives the successor preshared key from the previous Rosenpass
|
||||||
|
// output key. When a key expires without a completed renewal, both peers
|
||||||
|
// advance their last shared key by one ratchet step: the expired key is
|
||||||
|
// rotated out while both ends still converge on an identical, non-public
|
||||||
|
// replacement without communicating.
|
||||||
|
func RatchetKey(prev wgtypes.Key) wgtypes.Key {
|
||||||
|
input := make([]byte, 0, len(ratchetLabel)+len(prev))
|
||||||
|
input = append(input, ratchetLabel...)
|
||||||
|
input = append(input, prev[:]...)
|
||||||
|
return sha256.Sum256(input)
|
||||||
|
}
|
||||||
|
|
||||||
// DeterministicSeedKey derives a 32-byte WireGuard preshared key from a pair
|
// DeterministicSeedKey derives a 32-byte WireGuard preshared key from a pair
|
||||||
// of peer public keys. Both peers, given the same key pair, produce the same
|
// of peer public keys. Both peers, given the same key pair, produce the same
|
||||||
// output regardless of which side runs the function: the inputs are ordered
|
// output regardless of which side runs the function: the inputs are ordered
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module github.com/netbirdio/netbird
|
|||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
toolchain go1.25.11
|
toolchain go1.25.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cunicu.li/go-rosenpass v0.5.42
|
cunicu.li/go-rosenpass v0.5.42
|
||||||
@@ -19,8 +19,8 @@ require (
|
|||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.54.0
|
||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
@@ -126,11 +126,11 @@ require (
|
|||||||
goauthentik.io/api/v3 v3.2023051.3
|
goauthentik.io/api/v3 v3.2023051.3
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||||
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
|
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
|
||||||
golang.org/x/mod v0.35.0
|
golang.org/x/mod v0.37.0
|
||||||
golang.org/x/net v0.53.0
|
golang.org/x/net v0.56.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.22.0
|
||||||
golang.org/x/term v0.42.0
|
golang.org/x/term v0.45.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
google.golang.org/api v0.276.0
|
google.golang.org/api v0.276.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -314,8 +314,8 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.40.0 // indirect
|
||||||
golang.org/x/tools v0.44.0 // indirect
|
golang.org/x/tools v0.47.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
|
|||||||
@@ -732,8 +732,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -748,8 +748,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
@@ -768,8 +768,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
@@ -784,8 +784,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -821,8 +821,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -835,8 +835,8 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -848,8 +848,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -863,8 +863,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c" //nolint:staticcheck
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/encryption"
|
"github.com/netbirdio/netbird/encryption"
|
||||||
@@ -382,6 +382,7 @@ func (s *BaseServer) serveGRPCWithHTTP(ctx context.Context, listener net.Listene
|
|||||||
// the following magic is needed to support HTTP2 without TLS
|
// the following magic is needed to support HTTP2 without TLS
|
||||||
// and still share a single port between gRPC and HTTP APIs
|
// and still share a single port between gRPC and HTTP APIs
|
||||||
h1s := &http.Server{
|
h1s := &http.Server{
|
||||||
|
//nolint:staticcheck // h2c also handles the HTTP/1 Upgrade mechanism, which http.Server's UnencryptedHTTP2 does not
|
||||||
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
||||||
}
|
}
|
||||||
err = h1s.Serve(listener)
|
err = h1s.Serve(listener)
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ func (a *Account) SynthesizePrivateServiceZones(peerID string) []nbdns.CustomZon
|
|||||||
zone = &nbdns.CustomZone{
|
zone = &nbdns.CustomZone{
|
||||||
Domain: dns.Fqdn(serviceDomainZone),
|
Domain: dns.Fqdn(serviceDomainZone),
|
||||||
Records: []nbdns.SimpleRecord{},
|
Records: []nbdns.SimpleRecord{},
|
||||||
NonAuthoritative: true,
|
NonAuthoritative: true,
|
||||||
|
SearchDomainDisabled: true,
|
||||||
}
|
}
|
||||||
zonesByApex[serviceDomainZone] = zone
|
zonesByApex[serviceDomainZone] = zone
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -16,7 +16,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c" //nolint:staticcheck
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/shared/metrics"
|
"github.com/netbirdio/netbird/shared/metrics"
|
||||||
|
|
||||||
@@ -281,6 +281,7 @@ func serveHTTP(httpListener net.Listener, handler http.Handler) {
|
|||||||
go func() {
|
go func() {
|
||||||
// Use h2c to support HTTP/2 without TLS (needed for gRPC)
|
// Use h2c to support HTTP/2 without TLS (needed for gRPC)
|
||||||
h1s := &http.Server{
|
h1s := &http.Server{
|
||||||
|
//nolint:staticcheck // h2c also handles the HTTP/1 Upgrade mechanism, which http.Server's UnencryptedHTTP2 does not
|
||||||
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
||||||
}
|
}
|
||||||
err := h1s.Serve(httpListener)
|
err := h1s.Serve(httpListener)
|
||||||
|
|||||||
Reference in New Issue
Block a user