mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-04 19:45:14 -04:00
Epurate wg refs
This commit is contained in:
@@ -1,19 +1,18 @@
|
||||
package pqkem
|
||||
|
||||
// WGCallbackHandler is implemented by the host (the NetBird wiring layer) and
|
||||
// invoked by the library. The library only reports events; the host owns the
|
||||
// WireGuard reaction. Keeping this an interface — rather than calling wgctrl
|
||||
// directly — is what lets the KEM code be extracted as a standalone library.
|
||||
type WGCallbackHandler interface {
|
||||
// OnNewPSKReady fires when a fresh post-quantum PSK has been derived for a
|
||||
// peer and must be programmed on the WireGuard interface. It is invoked at the
|
||||
// commit point of each side: the initiator on receiving the answer, the
|
||||
// CallbackHandler is implemented by the host and invoked by the library. The
|
||||
// library only reports events; the host owns the reaction. Keeping this an
|
||||
// interface — rather than touching the transport or keying directly — is what lets
|
||||
// the KEM code be extracted as a standalone library.
|
||||
type CallbackHandler interface {
|
||||
// OnNewPSKReady fires when a fresh post-quantum PSK has been derived for a peer
|
||||
// and must be programmed into the consumer's secure channel. It is invoked at
|
||||
// the commit point of each side: the initiator on receiving the answer, the
|
||||
// responder on receiving the confirm.
|
||||
OnNewPSKReady(remoteWgKey string, psk PSK) error
|
||||
OnNewPSKReady(remoteID string, psk PSK) error
|
||||
|
||||
// OnRekeyFailed fires when an exchange fails to converge within the allotted
|
||||
// time. The host should tear the peer connection down so ICE re-establishes,
|
||||
// and log a WARN. The library reports the event; it does not dictate the
|
||||
// reaction.
|
||||
OnRekeyFailed(remoteWgKey string) error
|
||||
// time. The host should tear the peer connection down so it re-establishes, and
|
||||
// log a WARN. The library reports the event; it does not dictate the reaction.
|
||||
OnRekeyFailed(remoteID string) error
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
// handleOffer (responder) derives and sends the answer for a new exchange, or
|
||||
// resends the cached answer for a duplicate offer (same exchangeID) without
|
||||
// re-deriving. The responder is purely reactive: no retransmit loop, no deadline.
|
||||
func (m *Manager) handleOffer(remoteWgKey string, o *OfferMsg) error {
|
||||
func (m *Manager) handleOffer(remoteID string, o *OfferMsg) error {
|
||||
m.mu.Lock()
|
||||
if ex := m.exchanges[remoteWgKey]; ex != nil && ex.id == o.ExchangeID {
|
||||
if ex := m.exchanges[remoteID]; ex != nil && ex.id == o.ExchangeID {
|
||||
state, last := ex.state, ex.lastSent
|
||||
m.mu.Unlock()
|
||||
if state == stateReserved {
|
||||
@@ -18,13 +18,13 @@ func (m *Manager) handleOffer(remoteWgKey string, o *OfferMsg) error {
|
||||
// dropping avoids a second (randomized -> divergent) derivation.
|
||||
return nil
|
||||
}
|
||||
return m.transport.Send(remoteWgKey, last)
|
||||
return m.transport.Send(remoteID, last)
|
||||
}
|
||||
// Reserve the slot so a concurrent duplicate offer bails.
|
||||
m.exchanges[remoteWgKey] = &exchangeCtl{id: o.ExchangeID, state: stateReserved, startedAt: time.Now()}
|
||||
m.exchanges[remoteID] = &exchangeCtl{id: o.ExchangeID, state: stateReserved, startedAt: time.Now()}
|
||||
m.mu.Unlock()
|
||||
|
||||
answerBytes, psk, err := Respond(o.KEMOffer, m.binding(remoteWgKey))
|
||||
answerBytes, psk, err := Respond(o.KEMOffer, m.binding(remoteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -34,22 +34,22 @@ func (m *Manager) handleOffer(remoteWgKey string, o *OfferMsg) error {
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
if ex := m.exchanges[remoteWgKey]; ex != nil && ex.id == o.ExchangeID {
|
||||
if ex := m.exchanges[remoteID]; ex != nil && ex.id == o.ExchangeID {
|
||||
ex.state = stateAwaitingConfirm
|
||||
ex.lastSent = raw
|
||||
ex.pendingPSK = psk
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
return m.transport.Send(remoteWgKey, raw)
|
||||
return m.transport.Send(remoteID, raw)
|
||||
}
|
||||
|
||||
// handleAnswer (initiator) derives the PSK, surfaces it, and sends the confirm, then
|
||||
// switches the retransmit payload to the confirm. Only valid in stateAwaitingAnswer;
|
||||
// advancing the state under the lock makes a concurrent/duplicate answer bail.
|
||||
func (m *Manager) handleAnswer(remoteWgKey string, a *AnswerMsg) error {
|
||||
func (m *Manager) handleAnswer(remoteID string, a *AnswerMsg) error {
|
||||
m.mu.Lock()
|
||||
ex := m.exchanges[remoteWgKey]
|
||||
ex := m.exchanges[remoteID]
|
||||
if ex == nil || ex.id != a.ExchangeID || ex.state != stateAwaitingAnswer {
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
@@ -59,11 +59,11 @@ func (m *Manager) handleAnswer(remoteWgKey string, a *AnswerMsg) error {
|
||||
ex.initiator = nil
|
||||
m.mu.Unlock()
|
||||
|
||||
psk, err := init.Finish(a.KEMAnswer, m.binding(remoteWgKey))
|
||||
psk, err := init.Finish(a.KEMAnswer, m.binding(remoteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.wg.OnNewPSKReady(remoteWgKey, psk); err != nil {
|
||||
if err := m.cbHandler.OnNewPSKReady(remoteID, psk); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := (&ConfirmMsg{ExchangeID: a.ExchangeID}).Encode()
|
||||
@@ -72,42 +72,42 @@ func (m *Manager) handleAnswer(remoteWgKey string, a *AnswerMsg) error {
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
if ex := m.exchanges[remoteWgKey]; ex != nil && ex.id == a.ExchangeID {
|
||||
if ex := m.exchanges[remoteID]; ex != nil && ex.id == a.ExchangeID {
|
||||
ex.lastSent = raw // loop now retransmits the confirm
|
||||
m.established[remoteWgKey] = true
|
||||
m.failures[remoteWgKey] = 0
|
||||
m.established[remoteID] = true
|
||||
m.failures[remoteID] = 0
|
||||
_ = time.Since(ex.startedAt) // convergence latency (metrics hook, later step)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
return m.transport.Send(remoteWgKey, raw)
|
||||
return m.transport.Send(remoteID, raw)
|
||||
}
|
||||
|
||||
// handleConfirm (responder) commits the pending PSK. Only valid in
|
||||
// stateAwaitingConfirm; duplicate confirms (the initiator best-effort resends it)
|
||||
// find the exchange gone and are ignored.
|
||||
func (m *Manager) handleConfirm(remoteWgKey string, c *ConfirmMsg) error {
|
||||
func (m *Manager) handleConfirm(remoteID string, c *ConfirmMsg) error {
|
||||
m.mu.Lock()
|
||||
ex := m.exchanges[remoteWgKey]
|
||||
ex := m.exchanges[remoteID]
|
||||
if ex == nil || ex.id != c.ExchangeID || ex.state != stateAwaitingConfirm {
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
psk := ex.pendingPSK
|
||||
delete(m.exchanges, remoteWgKey)
|
||||
m.established[remoteWgKey] = true
|
||||
m.failures[remoteWgKey] = 0
|
||||
delete(m.exchanges, remoteID)
|
||||
m.established[remoteID] = true
|
||||
m.failures[remoteID] = 0
|
||||
_ = time.Since(ex.startedAt) // convergence latency (metrics hook, later step)
|
||||
m.mu.Unlock()
|
||||
|
||||
return m.wg.OnNewPSKReady(remoteWgKey, psk)
|
||||
return m.cbHandler.OnNewPSKReady(remoteID, psk)
|
||||
}
|
||||
|
||||
// initiatorLoop retransmits the initiator's outstanding message, keyed off the
|
||||
// exchange state: the offer while awaiting the answer (bounded by maxRetries ->
|
||||
// failure), then the confirm a few best-effort times before stopping. The
|
||||
// convergence deadline is thus derived from maxRetries * retryInterval.
|
||||
func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id ExchangeID) {
|
||||
func (m *Manager) initiatorLoop(ctx context.Context, remoteID string, id ExchangeID) {
|
||||
defer m.wait.Done()
|
||||
t := time.NewTicker(m.retryInterval)
|
||||
defer t.Stop()
|
||||
@@ -119,7 +119,7 @@ func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id Exch
|
||||
return
|
||||
case <-t.C:
|
||||
m.mu.Lock()
|
||||
ex := m.exchanges[remoteWgKey]
|
||||
ex := m.exchanges[remoteID]
|
||||
if ex == nil || ex.id != id {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
@@ -128,27 +128,27 @@ func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id Exch
|
||||
switch ex.state {
|
||||
case stateAwaitingAnswer:
|
||||
if offerAttempts >= m.maxRetries {
|
||||
delete(m.exchanges, remoteWgKey)
|
||||
fail := m.registerFailureLocked(remoteWgKey)
|
||||
delete(m.exchanges, remoteID)
|
||||
fail := m.registerFailureLocked(remoteID)
|
||||
m.mu.Unlock()
|
||||
m.raiseFailure(remoteWgKey, fail)
|
||||
m.raiseFailure(remoteID, fail)
|
||||
return
|
||||
}
|
||||
msg := ex.lastSent
|
||||
offerAttempts++
|
||||
m.mu.Unlock()
|
||||
m.retransmit(remoteWgKey, msg)
|
||||
m.retransmit(remoteID, msg)
|
||||
|
||||
case stateConfirming:
|
||||
if confirmsSent >= confirmRetransmits {
|
||||
delete(m.exchanges, remoteWgKey)
|
||||
delete(m.exchanges, remoteID)
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
msg := ex.lastSent
|
||||
confirmsSent++
|
||||
m.mu.Unlock()
|
||||
m.retransmit(remoteWgKey, msg)
|
||||
m.retransmit(remoteID, msg)
|
||||
|
||||
default:
|
||||
m.mu.Unlock()
|
||||
@@ -162,30 +162,30 @@ func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id Exch
|
||||
// an initial exchange (peer never established) fails immediately; a rekey tolerates
|
||||
// up to maxRekeyFailures consecutive misses (we stay on the still-valid previous
|
||||
// PSK) before failing. Assumes m.mu is held.
|
||||
func (m *Manager) registerFailureLocked(remoteWgKey string) bool {
|
||||
if !m.established[remoteWgKey] {
|
||||
func (m *Manager) registerFailureLocked(remoteID string) bool {
|
||||
if !m.established[remoteID] {
|
||||
return true
|
||||
}
|
||||
m.failures[remoteWgKey]++
|
||||
if m.failures[remoteWgKey] >= m.maxRekeyFailures {
|
||||
m.failures[remoteWgKey] = 0
|
||||
m.failures[remoteID]++
|
||||
if m.failures[remoteID] >= m.maxRekeyFailures {
|
||||
m.failures[remoteID] = 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) raiseFailure(remoteWgKey string, fail bool) {
|
||||
func (m *Manager) raiseFailure(remoteID string, fail bool) {
|
||||
if !fail {
|
||||
m.logger.Warn("pqkem rekey attempt timed out, will retry next cycle", "peer", remoteWgKey)
|
||||
m.logger.Warn("pqkem rekey attempt timed out, will retry next cycle", "peer", remoteID)
|
||||
return
|
||||
}
|
||||
if err := m.wg.OnRekeyFailed(remoteWgKey); err != nil {
|
||||
m.logger.Error("pqkem OnRekeyFailed handler error", "peer", remoteWgKey, "err", err)
|
||||
if err := m.cbHandler.OnRekeyFailed(remoteID); err != nil {
|
||||
m.logger.Error("pqkem OnRekeyFailed handler error", "peer", remoteID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) retransmit(remoteWgKey string, msg []byte) {
|
||||
if err := m.transport.Send(remoteWgKey, msg); err != nil {
|
||||
m.logger.Warn("pqkem retransmit failed", "peer", remoteWgKey, "err", err)
|
||||
func (m *Manager) retransmit(remoteID string, msg []byte) {
|
||||
if err := m.transport.Send(remoteID, msg); err != nil {
|
||||
m.logger.Warn("pqkem retransmit failed", "peer", remoteID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package pqkem is a spike (NET-1406) for a post-quantum pre-shared-key exchange
|
||||
// that could replace Rosenpass. It performs an X25519MLKEM768 hybrid key
|
||||
// encapsulation and derives a 32-byte WireGuard PSK.
|
||||
// encapsulation and derives a 32-byte pre-shared key (PSK).
|
||||
//
|
||||
// The exchange is a single round trip designed to ride the (already
|
||||
// authenticated) Signal offer/answer channel:
|
||||
@@ -9,7 +9,7 @@
|
||||
// initiator <--Answer(1120B)-- responder
|
||||
//
|
||||
// Both sides then hold the same PSK, which is bound to the two peers' identities
|
||||
// (their WireGuard static public keys) so the derived key cannot be transplanted
|
||||
// (their peer identity keys) so the derived key cannot be transplanted
|
||||
// to a different peer pair even if the transport authentication were bypassed.
|
||||
//
|
||||
// Combiner note: this follows the IETF hybrid layout (X25519 ‖ ML-KEM on the
|
||||
@@ -35,14 +35,14 @@ const (
|
||||
pskLabel = "netbird-pq-psk-v1"
|
||||
)
|
||||
|
||||
// PSK is the 32-byte pre-shared key handed to WireGuard.
|
||||
// PSK is the 32-byte derived pre-shared key handed to the consumer to key its channel.
|
||||
type PSK [32]byte
|
||||
|
||||
// Binding identifies the peer pair the PSK is derived for. Callers set both
|
||||
// WireGuard static public keys; the order does not matter (it is canonicalised).
|
||||
// peer identity keys; the order does not matter (it is canonicalised).
|
||||
type Binding struct {
|
||||
LocalWgPub []byte
|
||||
RemoteWgPub []byte
|
||||
LocalID []byte
|
||||
RemoteID []byte
|
||||
}
|
||||
|
||||
// Initiator holds the ephemeral secrets between Offer and Finish.
|
||||
@@ -141,7 +141,7 @@ func Respond(offer []byte, b Binding) (answer []byte, psk PSK, err error) {
|
||||
// TODO(NET-1406): replace the SHA-256 concat with the RFC HKDF combiner
|
||||
// (crypto/hkdf, Go 1.24+) and proper labels before this leaves spike status.
|
||||
func derivePSK(ssMLKEM, ssX, offer, answer []byte, b Binding) PSK {
|
||||
lo, hi := canonicalPair(b.LocalWgPub, b.RemoteWgPub)
|
||||
lo, hi := canonicalPair(b.LocalID, b.RemoteID)
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(pskLabel))
|
||||
|
||||
@@ -18,11 +18,11 @@ func TestExchange_DerivesMatchingPSK(t *testing.T) {
|
||||
|
||||
require.Len(t, init.Offer(), OfferSize)
|
||||
|
||||
answer, pskB, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
|
||||
answer, pskB, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, answer, AnswerSize)
|
||||
|
||||
pskA, err := init.Finish(answer, Binding{LocalWgPub: wgA, RemoteWgPub: wgB})
|
||||
pskA, err := init.Finish(answer, Binding{LocalID: wgA, RemoteID: wgB})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, pskB, pskA, "both sides must derive the same PSK")
|
||||
@@ -34,13 +34,13 @@ func TestExchange_PSKBoundToPeerIdentities(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// responder computes with the honest pair...
|
||||
_, pskHonest, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
|
||||
_, pskHonest, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ...a second responder run with a different peer identity yields a different PSK,
|
||||
// even though the KEM material would otherwise combine identically.
|
||||
wgC := []byte("peer-C-wireguard-pubkey-32bytes!")
|
||||
_, pskWrong, err := Respond(init.Offer(), Binding{LocalWgPub: wgC, RemoteWgPub: wgA})
|
||||
_, pskWrong, err := Respond(init.Offer(), Binding{LocalID: wgC, RemoteID: wgA})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, pskHonest, pskWrong, "PSK must be bound to the peer pair")
|
||||
@@ -70,12 +70,12 @@ func TestExchange_ReportSizesAndTiming(t *testing.T) {
|
||||
tInit += time.Since(s0)
|
||||
|
||||
s1 := time.Now()
|
||||
answer, _, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
|
||||
answer, _, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
|
||||
require.NoError(t, err)
|
||||
tResp += time.Since(s1)
|
||||
|
||||
s2 := time.Now()
|
||||
_, err = init.Finish(answer, Binding{LocalWgPub: wgA, RemoteWgPub: wgB})
|
||||
_, err = init.Finish(answer, Binding{LocalID: wgA, RemoteID: wgB})
|
||||
require.NoError(t, err)
|
||||
tFinish += time.Since(s2)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultRekeyInterval matches WireGuard's own REKEY_AFTER_TIME so the freshly
|
||||
// rotated PSK is naturally adopted by WG's next handshake without forcing one.
|
||||
// DefaultRekeyInterval is the default PSK rotation cadence (~2 min), chosen so a
|
||||
// rotated PSK is adopted by the consumer's next transport handshake without
|
||||
// forcing one.
|
||||
DefaultRekeyInterval = 2 * time.Minute
|
||||
// DefaultRetryInterval is how often the initiator retransmits its outstanding
|
||||
// message (offer, then confirm) while an exchange is in flight.
|
||||
@@ -29,11 +30,11 @@ const (
|
||||
)
|
||||
|
||||
// Transport hands an already-encoded exchange message to the peer. The host routes
|
||||
// it over the appropriate channel — Signal before the tunnel is up, the WireGuard
|
||||
// tunnel for rekeys — so the Manager never needs to know which is in use. It is the
|
||||
// analogue of go-rosenpass's Conn seam.
|
||||
// it over the appropriate channel — a signalling channel before the tunnel is up,
|
||||
// the data tunnel for rekeys — so the Manager never needs to know which is in use.
|
||||
// It is the analogue of go-rosenpass's Conn seam.
|
||||
type Transport interface {
|
||||
Send(remoteWgKey string, msg []byte) error
|
||||
Send(remoteID string, msg []byte) error
|
||||
}
|
||||
|
||||
// exchangeState is the single source of truth for an exchange's role and phase.
|
||||
@@ -66,14 +67,14 @@ type exchangeCtl struct {
|
||||
|
||||
// Manager is the stateful orchestrator — the analogue of go-rosenpass's Server. It
|
||||
// runs the per-peer rekey timer, drives the X25519MLKEM768 exchange over a pluggable
|
||||
// Transport, and surfaces the derived PSK to the host via WGCallbackHandler. The
|
||||
// Transport, and surfaces the derived PSK to the host via CallbackHandler. The
|
||||
// cryptography is the pure kem.go primitives; all per-exchange and per-peer state
|
||||
// lives here under one lock.
|
||||
type Manager struct {
|
||||
localWgKey string
|
||||
transport Transport
|
||||
wg WGCallbackHandler
|
||||
logger *slog.Logger
|
||||
localID string
|
||||
transport Transport
|
||||
cbHandler CallbackHandler
|
||||
logger *slog.Logger
|
||||
|
||||
rekeyInterval time.Duration
|
||||
retryInterval time.Duration
|
||||
@@ -91,11 +92,11 @@ type Manager struct {
|
||||
wait sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewManager builds a manager for the local peer identified by its WireGuard public
|
||||
// NewManager builds a manager for the local peer identified by its peer identity
|
||||
// key (used for the deterministic initiator role and the identity binding). A zero
|
||||
// interval falls back to DefaultRekeyInterval; a nil logger to slog.Default().
|
||||
// Retry/retries/K use their defaults and can be overridden before use.
|
||||
func NewManager(localWgKey string, t Transport, h WGCallbackHandler, interval time.Duration, logger *slog.Logger) *Manager {
|
||||
func NewManager(localID string, t Transport, h CallbackHandler, interval time.Duration, logger *slog.Logger) *Manager {
|
||||
if interval <= 0 {
|
||||
interval = DefaultRekeyInterval
|
||||
}
|
||||
@@ -104,9 +105,9 @@ func NewManager(localWgKey string, t Transport, h WGCallbackHandler, interval ti
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Manager{
|
||||
localWgKey: localWgKey,
|
||||
localID: localID,
|
||||
transport: t,
|
||||
wg: h,
|
||||
cbHandler: h,
|
||||
logger: logger,
|
||||
rekeyInterval: interval,
|
||||
retryInterval: DefaultRetryInterval,
|
||||
@@ -122,40 +123,40 @@ func NewManager(localWgKey string, t Transport, h WGCallbackHandler, interval ti
|
||||
}
|
||||
|
||||
// IsInitiator reports whether the local peer drives the exchange for this remote
|
||||
// peer. Roles are deterministic (lexicographic WG-key compare) so exactly one side
|
||||
// initiates, mirroring how Rosenpass picks its handshake initiator.
|
||||
func (m *Manager) IsInitiator(remoteWgKey string) bool {
|
||||
return m.localWgKey > remoteWgKey
|
||||
// peer. Roles are deterministic (lexicographic identity-key compare) so exactly one
|
||||
// side initiates, mirroring how Rosenpass picks its handshake initiator.
|
||||
func (m *Manager) IsInitiator(remoteID string) bool {
|
||||
return m.localID > remoteID
|
||||
}
|
||||
|
||||
// AddPeer registers a remote peer and starts its rekey timer. Re-adding is a no-op.
|
||||
func (m *Manager) AddPeer(remoteWgKey string) {
|
||||
func (m *Manager) AddPeer(remoteID string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, ok := m.peers[remoteWgKey]; ok {
|
||||
if _, ok := m.peers[remoteID]; ok {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(m.rootCtx)
|
||||
m.peers[remoteWgKey] = cancel
|
||||
m.peers[remoteID] = cancel
|
||||
m.wait.Add(1)
|
||||
go m.rekeyLoop(ctx, remoteWgKey)
|
||||
go m.rekeyLoop(ctx, remoteID)
|
||||
}
|
||||
|
||||
// RemovePeer stops a peer's rekey timer and any in-flight exchange, and drops state.
|
||||
func (m *Manager) RemovePeer(remoteWgKey string) {
|
||||
func (m *Manager) RemovePeer(remoteID string) {
|
||||
m.mu.Lock()
|
||||
if cancel, ok := m.peers[remoteWgKey]; ok {
|
||||
if cancel, ok := m.peers[remoteID]; ok {
|
||||
cancel()
|
||||
delete(m.peers, remoteWgKey)
|
||||
delete(m.peers, remoteID)
|
||||
}
|
||||
if ex, ok := m.exchanges[remoteWgKey]; ok {
|
||||
if ex, ok := m.exchanges[remoteID]; ok {
|
||||
if ex.cancel != nil {
|
||||
ex.cancel()
|
||||
}
|
||||
delete(m.exchanges, remoteWgKey)
|
||||
delete(m.exchanges, remoteID)
|
||||
}
|
||||
delete(m.established, remoteWgKey)
|
||||
delete(m.failures, remoteWgKey)
|
||||
delete(m.established, remoteID)
|
||||
delete(m.failures, remoteID)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -171,28 +172,28 @@ func (m *Manager) Stop() {
|
||||
|
||||
// HandleInbound decodes an incoming message and drives the exchange, sending any
|
||||
// response via the transport and surfacing derived PSKs / convergence to the host.
|
||||
func (m *Manager) HandleInbound(remoteWgKey string, raw []byte) error {
|
||||
func (m *Manager) HandleInbound(remoteID string, raw []byte) error {
|
||||
typ, msg, err := Decode(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode from %s: %w", remoteWgKey, err)
|
||||
return fmt.Errorf("decode from %s: %w", remoteID, err)
|
||||
}
|
||||
switch typ {
|
||||
case MsgOffer:
|
||||
return m.handleOffer(remoteWgKey, msg.(*OfferMsg))
|
||||
return m.handleOffer(remoteID, msg.(*OfferMsg))
|
||||
case MsgAnswer:
|
||||
return m.handleAnswer(remoteWgKey, msg.(*AnswerMsg))
|
||||
return m.handleAnswer(remoteID, msg.(*AnswerMsg))
|
||||
case MsgConfirm:
|
||||
return m.handleConfirm(remoteWgKey, msg.(*ConfirmMsg))
|
||||
return m.handleConfirm(remoteID, msg.(*ConfirmMsg))
|
||||
default:
|
||||
return fmt.Errorf("unhandled message type %d from %s", typ, remoteWgKey)
|
||||
return fmt.Errorf("unhandled message type %d from %s", typ, remoteID)
|
||||
}
|
||||
}
|
||||
|
||||
// initiateRekey starts a fresh exchange when the local peer is the initiator for
|
||||
// this remote peer; the responder waits for the offer instead. Exposed (unexported
|
||||
// but directly callable) so tests can drive a rekey without waiting on the ticker.
|
||||
func (m *Manager) initiateRekey(remoteWgKey string) error {
|
||||
if !m.IsInitiator(remoteWgKey) {
|
||||
func (m *Manager) initiateRekey(remoteID string) error {
|
||||
if !m.IsInitiator(remoteID) {
|
||||
return nil
|
||||
}
|
||||
init, err := NewInitiator()
|
||||
@@ -210,10 +211,10 @@ func (m *Manager) initiateRekey(remoteWgKey string) error {
|
||||
|
||||
ctx, cancel := context.WithCancel(m.rootCtx)
|
||||
m.mu.Lock()
|
||||
if old := m.exchanges[remoteWgKey]; old != nil && old.cancel != nil {
|
||||
if old := m.exchanges[remoteID]; old != nil && old.cancel != nil {
|
||||
old.cancel()
|
||||
}
|
||||
m.exchanges[remoteWgKey] = &exchangeCtl{
|
||||
m.exchanges[remoteID] = &exchangeCtl{
|
||||
id: id,
|
||||
state: stateAwaitingAnswer,
|
||||
startedAt: time.Now(),
|
||||
@@ -224,12 +225,12 @@ func (m *Manager) initiateRekey(remoteWgKey string) error {
|
||||
m.mu.Unlock()
|
||||
|
||||
m.wait.Add(1)
|
||||
go m.initiatorLoop(ctx, remoteWgKey, id)
|
||||
go m.initiatorLoop(ctx, remoteID, id)
|
||||
|
||||
return m.transport.Send(remoteWgKey, raw)
|
||||
return m.transport.Send(remoteID, raw)
|
||||
}
|
||||
|
||||
func (m *Manager) rekeyLoop(ctx context.Context, remoteWgKey string) {
|
||||
func (m *Manager) rekeyLoop(ctx context.Context, remoteID string) {
|
||||
defer m.wait.Done()
|
||||
t := time.NewTicker(m.rekeyInterval)
|
||||
defer t.Stop()
|
||||
@@ -238,15 +239,15 @@ func (m *Manager) rekeyLoop(ctx context.Context, remoteWgKey string) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
if err := m.initiateRekey(remoteWgKey); err != nil {
|
||||
m.logger.Error("pqkem rekey failed to start", "peer", remoteWgKey, "err", err)
|
||||
if err := m.initiateRekey(remoteID); err != nil {
|
||||
m.logger.Error("pqkem rekey failed to start", "peer", remoteID, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) binding(remoteWgKey string) Binding {
|
||||
return Binding{LocalWgPub: []byte(m.localWgKey), RemoteWgPub: []byte(remoteWgKey)}
|
||||
func (m *Manager) binding(remoteID string) Binding {
|
||||
return Binding{LocalID: []byte(m.localID), RemoteID: []byte(remoteID)}
|
||||
}
|
||||
|
||||
func newExchangeID() (ExchangeID, error) {
|
||||
|
||||
@@ -15,7 +15,7 @@ type loopback struct {
|
||||
peer *Manager
|
||||
}
|
||||
|
||||
func (l *loopback) Send(remoteWgKey string, msg []byte) error {
|
||||
func (l *loopback) Send(remoteID string, msg []byte) error {
|
||||
cp := append([]byte(nil), msg...)
|
||||
return l.peer.HandleInbound(l.localKey, cp)
|
||||
}
|
||||
@@ -28,17 +28,17 @@ type fakeWG struct {
|
||||
|
||||
func newFakeWG() *fakeWG { return &fakeWG{psks: map[string]PSK{}} }
|
||||
|
||||
func (f *fakeWG) OnNewPSKReady(remoteWgKey string, psk PSK) error {
|
||||
func (f *fakeWG) OnNewPSKReady(remoteID string, psk PSK) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.psks[remoteWgKey] = psk
|
||||
f.psks[remoteID] = psk
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWG) OnRekeyFailed(remoteWgKey string) error {
|
||||
func (f *fakeWG) OnRekeyFailed(remoteID string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.failed = append(f.failed, remoteWgKey)
|
||||
f.failed = append(f.failed, remoteID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
// Wire framing for the PQ-KEM exchange. Messages are self-contained, versioned,
|
||||
// transport-agnostic byte blobs: the same bytes ride the Signal offer/answer
|
||||
// (initial, pre-tunnel) or a WireGuard-tunnel packet (rekey). They are NOT a gRPC
|
||||
// (initial, pre-tunnel) or a data-tunnel packet (rekey). They are NOT a gRPC
|
||||
// service — the network layer only sees opaque []byte.
|
||||
//
|
||||
// Layout (all messages): [type:1][version:1][exchangeID:16][payload...]
|
||||
@@ -59,9 +59,9 @@ type AnswerMsg struct {
|
||||
// derive it to encapsulate), so the initiator needs no confirmation. Only the
|
||||
// responder is left unsure whether the initiator received the answer and committed
|
||||
// the key — this message resolves that. As a bonus, being sent under the new PSK it
|
||||
// triggers the WireGuard handshake with the new key: the responder converges on
|
||||
// receiving it, and the initiator converges by observing that handshake succeed
|
||||
// (which fails on a PSK mismatch, so success proves the responder also committed).
|
||||
// exercises the consumer's channel handshake with the new key: the responder
|
||||
// converges on receiving it, and the initiator converges by observing that handshake
|
||||
// succeed (which fails on a PSK mismatch, so success proves the responder committed too).
|
||||
type ConfirmMsg struct {
|
||||
ExchangeID ExchangeID
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func TestMessageRoundTrip(t *testing.T) {
|
||||
init, err := NewInitiator()
|
||||
require.NoError(t, err)
|
||||
answer, _, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
|
||||
answer, _, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
|
||||
require.NoError(t, err)
|
||||
|
||||
id := ExchangeID{1, 2, 3, 4}
|
||||
|
||||
Reference in New Issue
Block a user