connection_state.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package nebula
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log/slog"
  6. "sync"
  7. "sync/atomic"
  8. "github.com/slackhq/nebula/cert"
  9. "github.com/slackhq/nebula/handshake"
  10. "github.com/slackhq/nebula/header"
  11. "github.com/slackhq/nebula/noiseutil"
  12. )
  13. const (
  14. // ReplayWindow is the build-time receive window and the assumed peer window
  15. // for TX reservation credit. Use a power of two; peers must match or have a
  16. // larger receive window. The bitmap costs ReplayWindow/8 bytes per tunnel.
  17. ReplayWindow = 8192
  18. // RehandshakeAfterMessages rolls keys inside the AES-GCM data-volume margin (~2^-36 advantage at 64KB frames).
  19. RehandshakeAfterMessages = uint64(1) << 34
  20. // RejectAfterMessages is the nonce ceiling enforced by noiseutil; a tunnel here is deleted locally, not notified.
  21. RejectAfterMessages = noiseutil.RejectAfterMessages
  22. )
  23. // RehandshakeAfterMessages must stay below RejectAfterMessages so tunnels roll before the hard send stop.
  24. const _ = RejectAfterMessages - RehandshakeAfterMessages
  25. // sessionEpoch hands out a receiver-local ordinal to every ConnectionState at creation. The RX
  26. // staging sort (overlay/batch) orders packets by (epoch, message counter). A re-handshake never
  27. // rekeys an existing tunnel; it brings up a new hostinfo and ConnectionState with a counter space
  28. // starting near zero, while the old tunnel keeps decrypting until torn down. During that cutover
  29. // one flush batch can hold packets from both tunnels, and the epoch keeps the old tunnel's
  30. // packets sorted first.
  31. var sessionEpoch atomic.Uint64
  32. type ConnectionState struct {
  33. eKey noiseutil.CipherState
  34. dKey noiseutil.CipherState
  35. myCert cert.Certificate
  36. peerCert *cert.CachedCertificate
  37. initiator bool
  38. messageCounter atomic.Uint64
  39. txBudget txBudget
  40. window *Bits
  41. decryptLock sync.Mutex
  42. writeLock sync.Mutex
  43. // epoch is this session's sessionEpoch ordinal. Immutable after creation.
  44. epoch uint64
  45. }
  46. // newConnectionStateFromResult builds a fully-populated ConnectionState from a
  47. // completed handshake.Result. It seeds messageCounter and the replay window so
  48. // that the post-handshake message indices already used on the wire don't count
  49. // as missed traffic in the data plane.
  50. func newConnectionStateFromResult(r *handshake.Result) (*ConnectionState, error) {
  51. // Refuse a MessageIndex too big for the replay window: it can only be a bug, and would spin the seed loop below.
  52. if r.MessageIndex >= ReplayWindow {
  53. return nil, fmt.Errorf("handshake message index %d exceeds replay window", r.MessageIndex)
  54. }
  55. ci := &ConnectionState{
  56. myCert: r.MyCert,
  57. initiator: r.Initiator,
  58. peerCert: r.RemoteCert,
  59. eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
  60. dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
  61. window: NewBits(ReplayWindow),
  62. epoch: sessionEpoch.Add(1),
  63. }
  64. ci.messageCounter.Add(r.MessageIndex)
  65. for i := uint64(1); i <= r.MessageIndex; i++ {
  66. ci.window.Update(nil, i)
  67. }
  68. return ci, nil
  69. }
  70. func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
  71. return json.Marshal(m{
  72. "certificate": cs.peerCert,
  73. "initiator": cs.initiator,
  74. "message_counter": cs.messageCounter.Load(),
  75. })
  76. }
  77. // NextMessageCounter reserves the next 1-based counter; RejectAfterMessages is the first we refuse, pinned to not wrap.
  78. func (cs *ConnectionState) NextMessageCounter() (uint64, bool) {
  79. c := cs.messageCounter.Add(1)
  80. if c >= RejectAfterMessages {
  81. cs.messageCounter.Store(RejectAfterMessages)
  82. return c, false
  83. }
  84. return c, true
  85. }
  86. func (cs *ConnectionState) Curve() cert.Curve {
  87. return cs.myCert.Curve()
  88. }
  89. func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) ([]byte, error) {
  90. cs.decryptLock.Lock()
  91. result := cs.window.Check(l, messageCounter)
  92. cs.decryptLock.Unlock()
  93. if !result {
  94. return nil, ErrAlreadySeen
  95. }
  96. out, err := cs.decryptPacket(messageCounter, packet, nb)
  97. if err != nil {
  98. return nil, err
  99. }
  100. cs.decryptLock.Lock()
  101. result = cs.window.Update(l, messageCounter)
  102. cs.decryptLock.Unlock()
  103. if !result {
  104. return nil, ErrAlreadySeen
  105. }
  106. return out, nil
  107. }
  108. // decryptPacket authenticates and decrypts packet in place, leaving the replay window to the caller.
  109. func (cs *ConnectionState) decryptPacket(messageCounter uint64, packet, nb []byte) ([]byte, error) {
  110. return cs.dKey.DecryptDanger(packet[header.Len:header.Len], packet[:header.Len], packet[header.Len:], messageCounter, nb)
  111. }
  112. func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
  113. cs.decryptLock.Lock()
  114. result := cs.window.Check(l, messageCounter)
  115. cs.decryptLock.Unlock()
  116. if !result {
  117. return ErrAlreadySeen
  118. }
  119. // The entire body is sent as AD, not encrypted.
  120. // The packet consists of a 16-byte parsed Nebula header, Associated Data-protected payload, and a trailing 16-byte AEAD signature value.
  121. // 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
  122. // otherwise malformed (meaning, there is no trailing 16 byte AEAD value), then this will result in at worst a 0-length slice
  123. // which will gracefully fail in the DecryptDanger call.
  124. signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
  125. signatureValue := packet[len(packet)-cs.dKey.Overhead():]
  126. _, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
  127. if err != nil {
  128. return err
  129. }
  130. cs.decryptLock.Lock()
  131. result = cs.window.Update(l, messageCounter)
  132. cs.decryptLock.Unlock()
  133. if !result {
  134. return ErrAlreadySeen
  135. }
  136. return nil
  137. }