| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- package nebula
- import (
- "encoding/json"
- "fmt"
- "log/slog"
- "sync"
- "sync/atomic"
- "github.com/slackhq/nebula/cert"
- "github.com/slackhq/nebula/handshake"
- "github.com/slackhq/nebula/header"
- "github.com/slackhq/nebula/noiseutil"
- )
- const (
- // ReplayWindow is the build-time receive window and the assumed peer window
- // for TX reservation credit. Use a power of two; peers must match or have a
- // larger receive window. The bitmap costs ReplayWindow/8 bytes per tunnel.
- ReplayWindow = 8192
- // RehandshakeAfterMessages rolls keys inside the AES-GCM data-volume margin (~2^-36 advantage at 64KB frames).
- RehandshakeAfterMessages = uint64(1) << 34
- // RejectAfterMessages is the nonce ceiling enforced by noiseutil; a tunnel here is deleted locally, not notified.
- RejectAfterMessages = noiseutil.RejectAfterMessages
- )
- // RehandshakeAfterMessages must stay below RejectAfterMessages so tunnels roll before the hard send stop.
- const _ = RejectAfterMessages - RehandshakeAfterMessages
- // sessionEpoch hands out a receiver-local ordinal to every ConnectionState at creation. The RX
- // staging sort (overlay/batch) orders packets by (epoch, message counter). A re-handshake never
- // rekeys an existing tunnel; it brings up a new hostinfo and ConnectionState with a counter space
- // starting near zero, while the old tunnel keeps decrypting until torn down. During that cutover
- // one flush batch can hold packets from both tunnels, and the epoch keeps the old tunnel's
- // packets sorted first.
- var sessionEpoch atomic.Uint64
- type ConnectionState struct {
- eKey noiseutil.CipherState
- dKey noiseutil.CipherState
- myCert cert.Certificate
- peerCert *cert.CachedCertificate
- initiator bool
- messageCounter atomic.Uint64
- txBudget txBudget
- window *Bits
- decryptLock sync.Mutex
- writeLock sync.Mutex
- // epoch is this session's sessionEpoch ordinal. Immutable after creation.
- epoch uint64
- }
- // newConnectionStateFromResult builds a fully-populated ConnectionState from a
- // completed handshake.Result. It seeds messageCounter and the replay window so
- // that the post-handshake message indices already used on the wire don't count
- // as missed traffic in the data plane.
- func newConnectionStateFromResult(r *handshake.Result) (*ConnectionState, error) {
- // Refuse a MessageIndex too big for the replay window: it can only be a bug, and would spin the seed loop below.
- if r.MessageIndex >= ReplayWindow {
- return nil, fmt.Errorf("handshake message index %d exceeds replay window", r.MessageIndex)
- }
- ci := &ConnectionState{
- myCert: r.MyCert,
- initiator: r.Initiator,
- peerCert: r.RemoteCert,
- eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
- dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
- window: NewBits(ReplayWindow),
- epoch: sessionEpoch.Add(1),
- }
- ci.messageCounter.Add(r.MessageIndex)
- for i := uint64(1); i <= r.MessageIndex; i++ {
- ci.window.Update(nil, i)
- }
- return ci, nil
- }
- func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
- return json.Marshal(m{
- "certificate": cs.peerCert,
- "initiator": cs.initiator,
- "message_counter": cs.messageCounter.Load(),
- })
- }
- // NextMessageCounter reserves the next 1-based counter; RejectAfterMessages is the first we refuse, pinned to not wrap.
- func (cs *ConnectionState) NextMessageCounter() (uint64, bool) {
- c := cs.messageCounter.Add(1)
- if c >= RejectAfterMessages {
- cs.messageCounter.Store(RejectAfterMessages)
- return c, false
- }
- return c, true
- }
- func (cs *ConnectionState) Curve() cert.Curve {
- return cs.myCert.Curve()
- }
- func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) ([]byte, error) {
- cs.decryptLock.Lock()
- result := cs.window.Check(l, messageCounter)
- cs.decryptLock.Unlock()
- if !result {
- return nil, ErrAlreadySeen
- }
- out, err := cs.decryptPacket(messageCounter, packet, nb)
- if err != nil {
- return nil, err
- }
- cs.decryptLock.Lock()
- result = cs.window.Update(l, messageCounter)
- cs.decryptLock.Unlock()
- if !result {
- return nil, ErrAlreadySeen
- }
- return out, nil
- }
- // decryptPacket authenticates and decrypts packet in place, leaving the replay window to the caller.
- func (cs *ConnectionState) decryptPacket(messageCounter uint64, packet, nb []byte) ([]byte, error) {
- return cs.dKey.DecryptDanger(packet[header.Len:header.Len], packet[:header.Len], packet[header.Len:], messageCounter, nb)
- }
- func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
- cs.decryptLock.Lock()
- result := cs.window.Check(l, messageCounter)
- cs.decryptLock.Unlock()
- if !result {
- return ErrAlreadySeen
- }
- // The entire body is sent as AD, not encrypted.
- // The packet consists of a 16-byte parsed Nebula header, Associated Data-protected payload, and a trailing 16-byte AEAD signature value.
- // The packet is guaranteed to be at least 16 bytes at this point, b/c it got past the h.Parse() call above. If it's
- // otherwise malformed (meaning, there is no trailing 16 byte AEAD value), then this will result in at worst a 0-length slice
- // which will gracefully fail in the DecryptDanger call.
- signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
- signatureValue := packet[len(packet)-cs.dKey.Overhead():]
- _, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
- if err != nil {
- return err
- }
- cs.decryptLock.Lock()
- result = cs.window.Update(l, messageCounter)
- cs.decryptLock.Unlock()
- if !result {
- return ErrAlreadySeen
- }
- return nil
- }
|