flow_tx_ring_test.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. //go:build linux && !android && !e2e_testing
  2. package nebula
  3. import (
  4. "fmt"
  5. "net/netip"
  6. "sync"
  7. "time"
  8. "os"
  9. "os/exec"
  10. "testing"
  11. "github.com/slackhq/nebula/overlay/tio"
  12. "github.com/flynn/noise"
  13. "github.com/slackhq/nebula/header"
  14. "github.com/slackhq/nebula/udp"
  15. "github.com/slackhq/nebula/util"
  16. "github.com/stretchr/testify/require"
  17. )
  18. // queuedWire models two CPU-selected NIC queues with independent completion: the reader's
  19. // queue is congested, while another CPU's queue drains immediately. WriteBatch returning only
  20. // means the kernel accepted the packets; draining staged slots does not drain a NIC queue.
  21. // The sampled affinity makes the schedule deterministic without a real socket or NIC. An
  22. // unpinned sender alternates between allowed CPUs, even if it happened not to in this run.
  23. func (c *affinityConn) queuedWire(readerCPU int) [][]byte {
  24. var slow, fast [][]byte
  25. wire := c.captured()
  26. off := 0
  27. for i, n := range c.sizes {
  28. mask := c.masks[i]
  29. if mask.IsSet(readerCPU) && (mask.Count() == 1 || i%2 == 0) {
  30. slow = append(slow, wire[off:off+n]...)
  31. } else {
  32. fast = append(fast, wire[off:off+n]...)
  33. }
  34. off += n
  35. }
  36. return append(fast, slow...)
  37. }
  38. // TestFlowStagedTXRelayQueuedWire reproduces reorder after successful, ordered socket
  39. // submissions. Mixed reads switch the sticky gate with an empty software pipeline; relayed
  40. // data must stay on the reader throughout, without moving the direct sender. Each routine owns a flow.
  41. func TestFlowStagedTXRelayQueuedWire(t *testing.T) {
  42. if os.Getenv("NEBULA_FLOW_RING_TEST") != "1" {
  43. cmd := exec.Command(os.Args[0], "-test.run=^TestFlowStagedTXRelayQueuedWire$")
  44. cmd.Env = append(os.Environ(), "NEBULA_FLOW_RING_TEST=1")
  45. out, err := cmd.CombinedOutput()
  46. require.NoError(t, err, "%s", out)
  47. return
  48. }
  49. allowed, err := util.AllowedCPUs()
  50. require.NoError(t, err)
  51. if len(allowed) < 2 {
  52. t.Skip("requires two allowed CPUs")
  53. }
  54. for _, mode := range []string{"gate", "always-staged", "oversized"} {
  55. for _, explicit := range []bool{false, true} {
  56. for _, routines := range []int{1, 2} {
  57. for q := range routines {
  58. t.Run(fmt.Sprintf("%s/explicit=%t/routines=%d/q=%d", mode, explicit, routines, q), func(t *testing.T) {
  59. sc := gated(stagedGateHold)
  60. if mode != "gate" {
  61. sc.inlineBelow = 0
  62. }
  63. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, sc)
  64. f := sender.f
  65. f.pinThreads, f.routines = true, routines
  66. if explicit {
  67. f.cpuAffinity = allowed[:2]
  68. }
  69. _, relayHI, relayPeer := addTestRelay(t, sender, ReplayWindow)
  70. conn := &affinityConn{}
  71. f.writers = make([]udp.Conn, routines)
  72. for i := range f.writers {
  73. f.writers[i] = conn
  74. }
  75. shape := append(repeatBool(false, 2), repeatBool(true, stagedGateHold+2)...)
  76. shape = append(shape, repeatBool(false, stagedGateHold+2)...)
  77. if mode != "gate" {
  78. shape = []bool{false, true, false, true}
  79. }
  80. reads, want := gateReads(shape...)
  81. if mode == "oversized" {
  82. // A deliberately tiny outer budget alone forces the oversized fallback.
  83. relayHI.ConnectionState.window = NewBits(16)
  84. }
  85. queue := &affinityQueue{flowBatchQueue: newFlowBatchQueue(reads), conn: conn}
  86. // Each Read waits for all previous socket submissions, making software drain
  87. // irrelevant to the independently queued packets in queuedWire.
  88. f.listenIn(queue, q)
  89. unwrap := func(wire [][]byte) [][]byte {
  90. inner := make([][]byte, 0, len(wire))
  91. for _, pkt := range wire {
  92. inner = append(inner, pkt[header.Len:len(pkt)-relayPeer.dKey.Overhead()])
  93. }
  94. return inner
  95. }
  96. require.Equal(t, want, decryptWire(t, receiver, unwrap(conn.captured())), "socket submission order")
  97. require.Equal(t, want, decryptWire(t, receiver, unwrap(conn.queuedWire(f.threadCPU(q)))), "NIC queue completion order")
  98. })
  99. }
  100. }
  101. }
  102. }
  103. }
  104. // Mixed reads must keep direct sends on the original staged sender, including when relay data is present.
  105. func TestFlowStagedTXMixedPlacement(t *testing.T) {
  106. if os.Getenv("NEBULA_FLOW_MIXED_TEST") != "1" {
  107. cmd := exec.Command(os.Args[0], "-test.run=^TestFlowStagedTXMixedPlacement$")
  108. cmd.Env = append(os.Environ(), "NEBULA_FLOW_MIXED_TEST=1")
  109. out, err := cmd.CombinedOutput()
  110. require.NoError(t, err, "%s", out)
  111. return
  112. }
  113. allowed, err := util.AllowedCPUs()
  114. require.NoError(t, err)
  115. if len(allowed) < 2 {
  116. t.Skip("requires two allowed CPUs")
  117. }
  118. for _, explicit := range []bool{false, true} {
  119. t.Run(fmt.Sprintf("explicit=%t", explicit), func(t *testing.T) {
  120. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
  121. _, _, relayPeer := addTestRelay(t, sender, ReplayWindow)
  122. direct, directReceiver := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
  123. directAddr := netip.MustParseAddr("10.0.0.4")
  124. direct.hostinfo.vpnAddrs = []netip.Addr{directAddr}
  125. f := sender.f
  126. f.hostMap.Hosts[directAddr] = direct.hostinfo
  127. f.pinThreads, f.routines = true, 2
  128. sendCPU := -1
  129. if explicit {
  130. f.cpuAffinity = allowed[:2]
  131. sendCPU = allowed[1]
  132. }
  133. conn := &affinityConn{}
  134. f.writers = []udp.Conn{conn, conn}
  135. var reads [][]tio.Packet
  136. var wantRelay, wantDirect []decodedSeg
  137. for seq := range uint32(6) {
  138. r := flowTCPSuperpacket(0, seq, 3)
  139. d := flowTCPSuperpacket(1, seq, 9)
  140. copy(d.Bytes[16:20], directAddr.AsSlice())
  141. reads = append(reads, []tio.Packet{r, d})
  142. for i := range 3 {
  143. wantRelay = append(wantRelay, decodedSeg{seq: seq, idx: uint16(i)})
  144. }
  145. for i := range 9 {
  146. wantDirect = append(wantDirect, decodedSeg{flow: 1, seq: seq, idx: uint16(i)})
  147. }
  148. }
  149. queue := &affinityQueue{flowBatchQueue: newFlowBatchQueue(reads), conn: conn}
  150. f.listenIn(queue, 0)
  151. wire := conn.captured()
  152. var directWire, relayWire [][]byte
  153. off := 0
  154. for i, n := range conn.sizes {
  155. for _, pkt := range wire[off : off+n] {
  156. var h header.H
  157. require.NoError(t, h.Parse(pkt))
  158. cpu := sendCPU
  159. if h.Subtype == header.MessageRelay {
  160. cpu = f.threadCPU(0)
  161. require.NoError(t, relayPeer.VerifyRelay(nil, h.MessageCounter, pkt, make([]byte, 12)))
  162. relayWire = append(relayWire, pkt[header.Len:len(pkt)-relayPeer.dKey.Overhead()])
  163. } else {
  164. directWire = append(directWire, pkt)
  165. }
  166. requireFlowCPU(t, conn.masks[i:i+1], cpu, allowed)
  167. }
  168. off += n
  169. }
  170. require.Equal(t, wantDirect, decryptWire(t, directReceiver, directWire))
  171. require.Equal(t, wantRelay, decryptWire(t, receiver, relayWire))
  172. })
  173. }
  174. }
  175. // A relayed batch shares its outer tunnel with older staged direct data on the SAME routine. Inline completion
  176. // must not refund that direct sender's credit while its oldest reservation is stalled.
  177. func TestFlowStagedTXRelaySharedBudget(t *testing.T) {
  178. sender, _ := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
  179. _, via, _ := addTestRelay(t, sender, ReplayWindow)
  180. // Give the relay host the same ordinary peer metadata as a direct tunnel.
  181. direct := newFlowTestPeer(t, flowTestLocal, via.vpnAddrs[0], via.ConnectionState, 300, 400)
  182. via.buildNetworks(sender.f.myVpnNetworksTable, direct.hostinfo.ConnectionState.peerCert.Certificate)
  183. via.remoteIndexId = direct.hostinfo.remoteIndexId
  184. ci := via.ConnectionState
  185. ci.window = NewBits(32)
  186. start := ci.messageCounter.Load()
  187. entered, unblock := make(chan struct{}), make(chan struct{})
  188. var release sync.Once
  189. delayCrypto(direct, func(n uint64) {
  190. if n == start+1 {
  191. close(entered)
  192. <-unblock
  193. }
  194. })
  195. p := newStagedTx(sender.f, newFlowBatchQueue(nil), 0)
  196. defer p.close()
  197. defer release.Do(func() { close(unblock) })
  198. d := flowTCPSuperpacket(1, 0, 9)
  199. copy(d.Bytes[16:20], via.vpnAddrs[0].AsSlice())
  200. p.send([]tio.Packet{d})
  201. select {
  202. case <-entered:
  203. case <-time.After(5 * time.Second):
  204. t.Fatal("direct worker did not start")
  205. }
  206. for seq := range uint32(7) {
  207. p.send([]tio.Packet{flowTCPSuperpacket(0, seq, 1)})
  208. }
  209. require.Equal(t, uint64(16), (ci.txBudget.state.Load()>>24)&txBudgetMask)
  210. done := make(chan struct{})
  211. go func() { defer close(done); p.send([]tio.Packet{flowTCPSuperpacket(0, 7, 1)}) }()
  212. select {
  213. case <-done:
  214. t.Error("relay recycled credit around a stalled direct reservation")
  215. case <-time.After(50 * time.Millisecond):
  216. }
  217. require.Equal(t, start+16, ci.messageCounter.Load())
  218. release.Do(func() { close(unblock) })
  219. <-done
  220. p.drain()
  221. require.Zero(t, ci.txBudget.state.Load())
  222. }