multi_coalesce.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. package batch
  2. import (
  3. "cmp"
  4. "errors"
  5. "io"
  6. "log/slog"
  7. "slices"
  8. "github.com/slackhq/nebula/firewall"
  9. )
  10. // MultiCoalescer stages plaintext packets with their (epoch, counter) sort keys and, at Flush,
  11. // replays them in sender-transmission order into lane-specific batchers selected by L4 protocol.
  12. //
  13. // Sorting before dispatch keeps the ordering story simple: each lane consumes packets in
  14. // transmission order, builds slots in that order, and emits them in creation order. Wire reorder
  15. // inside a flush batch is repaired here, before it can fragment a lane's coalesce chains, so the
  16. // lanes carry no reorder-repair machinery.
  17. //
  18. // The contract is per-tunnel transmission order within each lane, with two exceptions: a pure TCP
  19. // ACK may be overtaken by later same-flow data (it does not close the flow's open chain; a late
  20. // ACK is just a stale ACK), and an unparseable shape seals every open chain in its lane (its flow
  21. // is unknown) and rides the lane as an in-lane verbatim, still in transmission order. Routing
  22. // follows the flow: a flow's non-coalesceable shapes ride its protocol lane rather than falling
  23. // to the later-flushed pt lane.
  24. //
  25. // Cross-lane order (TCP vs UDP vs everything else) is not preserved.
  26. type MultiCoalescer struct {
  27. tcp *TCPCoalescer
  28. udp *UDPCoalescer
  29. pt *Passthrough
  30. // staged holds this batch's packets and sort keys until Flush. Borrowed: the caller keeps
  31. // each pkt alive until Flush returns.
  32. staged []stagedPacket
  33. }
  34. // stagedPacket carries the scalars dispatch needs from the firewall's ParsedPacket, copied by
  35. // value: pp is reused by the caller per packet and must not be retained past Commit.
  36. type stagedPacket struct {
  37. pkt []byte
  38. key SortKey
  39. proto byte
  40. fragAny bool
  41. ipHdrLen uint16
  42. }
  43. // NewMultiCoalescer builds a multi-lane batcher over w, based on available protocol support. The
  44. // staging sort applies even when no GSO lane is available: passthrough-only platforms still get
  45. // transmission-order repair.
  46. func NewMultiCoalescer(w io.Writer, l *slog.Logger) *MultiCoalescer {
  47. m := &MultiCoalescer{
  48. pt: NewPassthrough(w),
  49. staged: make([]stagedPacket, 0, initialSlots),
  50. }
  51. m.tcp = NewTCPCoalescer(w, l)
  52. m.udp = NewUDPCoalescer(w)
  53. return m
  54. }
  55. // Commit stages pkt for the next Flush; dispatch is deferred so it runs on packets already in
  56. // transmission order. key carries the packet's tunnel epoch and message counter. pkt is borrowed:
  57. // the caller must keep it valid until the next Flush and not re-use it, and Flush may patch a
  58. // coalesced packet's headers in place. pp is the firewall's parse of pkt and is borrowed only
  59. // for this call, so the fields dispatch needs are copied here.
  60. func (m *MultiCoalescer) Commit(pkt []byte, key SortKey, pp *firewall.ParsedPacket) error {
  61. m.staged = append(m.staged, stagedPacket{
  62. pkt: pkt,
  63. key: key,
  64. proto: pp.Protocol,
  65. fragAny: pp.FragAny,
  66. ipHdrLen: uint16(pp.IPHdrLen),
  67. })
  68. return nil
  69. }
  70. // compareStaged orders staged packets by (epoch, counter)
  71. func compareStaged(a, b stagedPacket) int {
  72. if c := cmp.Compare(a.key.Epoch, b.key.Epoch); c != 0 {
  73. return c
  74. }
  75. return cmp.Compare(a.key.Counter, b.key.Counter)
  76. }
  77. // dispatch routes one staged packet to its protocol lane (see commitStaged), or to the verbatim
  78. // passthrough when the lane has no GSO support.
  79. func (m *MultiCoalescer) dispatch(sp stagedPacket) error {
  80. switch sp.proto {
  81. case ipProtoTCP:
  82. if m.tcp != nil {
  83. return m.tcp.commitStaged(sp)
  84. }
  85. case ipProtoUDP:
  86. if m.udp != nil {
  87. return m.udp.commitStaged(sp)
  88. }
  89. }
  90. return m.pt.enqueue(sp.pkt)
  91. }
  92. // Flush sorts the staged batch into transmission order, replays it into the lanes, then flushes each lane.
  93. // Drains everything and returns the joined errors; one bad packet does not hold up the rest.
  94. // After Flush returns, committed payload slices may be recycled.
  95. func (m *MultiCoalescer) Flush() error {
  96. // Arrival order is already almost sorted (reorder is the exception), which pdqsort detects
  97. // and handles in near-linear time.
  98. slices.SortFunc(m.staged, compareStaged)
  99. var errs []error
  100. for _, sp := range m.staged {
  101. if err := m.dispatch(sp); err != nil {
  102. errs = append(errs, err)
  103. }
  104. }
  105. clear(m.staged) // drop borrowed pkt refs
  106. m.staged = m.staged[:0]
  107. if m.tcp != nil {
  108. if err := m.tcp.Flush(); err != nil {
  109. errs = append(errs, err)
  110. }
  111. }
  112. if m.udp != nil {
  113. if err := m.udp.Flush(); err != nil {
  114. errs = append(errs, err)
  115. }
  116. }
  117. if err := m.pt.Flush(); err != nil {
  118. errs = append(errs, err)
  119. }
  120. return errors.Join(errs...)
  121. }