mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 22:09:09 +02:00
200 lines
5.9 KiB
Go
200 lines
5.9 KiB
Go
package pqkem
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// 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 (d *Driver) handleOffer(remoteWgKey string, o *OfferMsg) error {
|
|
d.mu.Lock()
|
|
if ex := d.exchanges[remoteWgKey]; ex != nil && ex.id == o.ExchangeID {
|
|
last := ex.lastSent
|
|
d.mu.Unlock()
|
|
if last == nil {
|
|
// another goroutine reserved this exchange and is deriving the answer;
|
|
// it will send it. Dropping this duplicate avoids a second derivation
|
|
// (Respond is randomized -> a different PSK).
|
|
return nil
|
|
}
|
|
return d.transport.Send(remoteWgKey, last)
|
|
}
|
|
// Reserve the slot (lastSent nil = deriving) so a concurrent duplicate offer bails.
|
|
d.exchanges[remoteWgKey] = &exchangeCtl{
|
|
id: o.ExchangeID,
|
|
startedAt: time.Now(),
|
|
}
|
|
d.mu.Unlock()
|
|
|
|
answer, err := d.mgr.HandleOffer(remoteWgKey, o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := answer.Encode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.mu.Lock()
|
|
if ex := d.exchanges[remoteWgKey]; ex != nil && ex.id == o.ExchangeID {
|
|
ex.lastSent = raw
|
|
}
|
|
d.mu.Unlock()
|
|
|
|
return d.transport.Send(remoteWgKey, raw)
|
|
}
|
|
|
|
// handleAnswer (initiator) derives the PSK, surfaces it, and sends the confirm. The
|
|
// exchange then switches its retransmit payload to the confirm. Stale or duplicate
|
|
// answers are ignored.
|
|
func (d *Driver) handleAnswer(remoteWgKey string, a *AnswerMsg) error {
|
|
// Ignore stale answers, answers to a responder-side exchange (cancel == nil), and
|
|
// duplicates once we are already in the confirm phase. A duplicate that still
|
|
// slips through is caught by the Manager, whose HandleAnswer bails under its lock.
|
|
d.mu.Lock()
|
|
ex := d.exchanges[remoteWgKey]
|
|
if ex == nil || ex.id != a.ExchangeID || ex.cancel == nil || isConfirmPhase(ex.lastSent) {
|
|
d.mu.Unlock()
|
|
return nil
|
|
}
|
|
d.mu.Unlock()
|
|
|
|
psk, confirm, err := d.mgr.HandleAnswer(remoteWgKey, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := d.wg.OnNewPSKReady(remoteWgKey, psk); err != nil {
|
|
return err
|
|
}
|
|
raw, err := confirm.Encode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.mu.Lock()
|
|
if ex := d.exchanges[remoteWgKey]; ex != nil && ex.id == a.ExchangeID {
|
|
ex.lastSent = raw // loop now retransmits the confirm
|
|
d.established[remoteWgKey] = true
|
|
d.failures[remoteWgKey] = 0
|
|
_ = time.Since(ex.startedAt) // convergence latency (metrics hook, later step)
|
|
}
|
|
d.mu.Unlock()
|
|
|
|
return d.transport.Send(remoteWgKey, raw)
|
|
}
|
|
|
|
// handleConfirm (responder) commits the pending PSK. Stale or duplicate confirms
|
|
// (the initiator best-effort resends it) are ignored.
|
|
func (d *Driver) handleConfirm(remoteWgKey string, c *ConfirmMsg) error {
|
|
// Claim-and-remove the exchange under the lock so a duplicate confirm (the
|
|
// initiator best-effort resends it) bails before calling the Manager.
|
|
d.mu.Lock()
|
|
ex := d.exchanges[remoteWgKey]
|
|
if ex == nil || ex.id != c.ExchangeID {
|
|
d.mu.Unlock()
|
|
return nil
|
|
}
|
|
delete(d.exchanges, remoteWgKey)
|
|
d.established[remoteWgKey] = true
|
|
d.failures[remoteWgKey] = 0
|
|
_ = time.Since(ex.startedAt) // convergence latency (metrics hook, later step)
|
|
d.mu.Unlock()
|
|
|
|
psk, err := d.mgr.HandleConfirm(remoteWgKey, c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return d.wg.OnNewPSKReady(remoteWgKey, psk)
|
|
}
|
|
|
|
// initiatorLoop retransmits the initiator's outstanding message: the offer until
|
|
// the answer arrives (bounded by maxRetries -> failure), then the confirm a few
|
|
// best-effort times before stopping. The convergence deadline is thus derived from
|
|
// maxRetries * retryInterval; there is no separate deadline timer.
|
|
func (d *Driver) initiatorLoop(ctx context.Context, remoteWgKey string, id ExchangeID) {
|
|
defer d.wait.Done()
|
|
t := time.NewTicker(d.retryInterval)
|
|
defer t.Stop()
|
|
|
|
offerAttempts, confirmsSent := 0, 0
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
d.mu.Lock()
|
|
ex := d.exchanges[remoteWgKey]
|
|
if ex == nil || ex.id != id {
|
|
d.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
if !isConfirmPhase(ex.lastSent) {
|
|
if offerAttempts >= d.maxRetries {
|
|
delete(d.exchanges, remoteWgKey)
|
|
fail := d.registerFailureLocked(remoteWgKey)
|
|
d.mu.Unlock()
|
|
d.raiseFailure(remoteWgKey, fail)
|
|
return
|
|
}
|
|
msg := ex.lastSent
|
|
offerAttempts++
|
|
d.mu.Unlock()
|
|
d.retransmit(remoteWgKey, msg)
|
|
continue
|
|
}
|
|
|
|
if confirmsSent >= confirmRetransmits {
|
|
delete(d.exchanges, remoteWgKey)
|
|
d.mu.Unlock()
|
|
return
|
|
}
|
|
msg := ex.lastSent
|
|
confirmsSent++
|
|
d.mu.Unlock()
|
|
d.retransmit(remoteWgKey, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// registerFailureLocked applies policy B and reports whether OnRekeyFailed is due:
|
|
// 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. Whether it is initial is read live from established.
|
|
// Assumes d.mu is held.
|
|
func (d *Driver) registerFailureLocked(remoteWgKey string) bool {
|
|
if !d.established[remoteWgKey] {
|
|
return true
|
|
}
|
|
d.failures[remoteWgKey]++
|
|
if d.failures[remoteWgKey] >= d.maxRekeyFailures {
|
|
d.failures[remoteWgKey] = 0
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isConfirmPhase reports whether the initiator's outstanding message is the confirm
|
|
// (as opposed to the offer), derived from the message's type byte.
|
|
func isConfirmPhase(lastSent []byte) bool {
|
|
return len(lastSent) > 0 && lastSent[0] == byte(MsgConfirm)
|
|
}
|
|
|
|
func (d *Driver) raiseFailure(remoteWgKey string, fail bool) {
|
|
if !fail {
|
|
d.logger.Warn("pqkem rekey attempt timed out, will retry next cycle", "peer", remoteWgKey)
|
|
return
|
|
}
|
|
if err := d.wg.OnRekeyFailed(remoteWgKey); err != nil {
|
|
d.logger.Error("pqkem OnRekeyFailed handler error", "peer", remoteWgKey, "err", err)
|
|
}
|
|
}
|
|
|
|
func (d *Driver) retransmit(remoteWgKey string, msg []byte) {
|
|
if err := d.transport.Send(remoteWgKey, msg); err != nil {
|
|
d.logger.Warn("pqkem retransmit failed", "peer", remoteWgKey, "err", err)
|
|
}
|
|
}
|