flow_tx_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. //go:build linux && !android && !e2e_testing
  2. package nebula
  3. import (
  4. "fmt"
  5. "net/netip"
  6. "os"
  7. "os/exec"
  8. "slices"
  9. "sync"
  10. "testing"
  11. "time"
  12. "github.com/flynn/noise"
  13. "github.com/rcrowley/go-metrics"
  14. "github.com/slackhq/nebula/config"
  15. "github.com/slackhq/nebula/cpupick"
  16. "github.com/slackhq/nebula/firewall"
  17. "github.com/slackhq/nebula/header"
  18. "github.com/slackhq/nebula/noiseutil"
  19. "github.com/slackhq/nebula/overlay/batch"
  20. "github.com/slackhq/nebula/overlay/tio"
  21. "github.com/slackhq/nebula/test"
  22. "github.com/slackhq/nebula/udp"
  23. "github.com/slackhq/nebula/util"
  24. "github.com/stretchr/testify/require"
  25. "go.uber.org/goleak"
  26. "golang.org/x/sys/unix"
  27. )
  28. // TestReservedEncryptionMatchesInline encrypts a batch with counters reserved up front: the receiver accepts every
  29. // segment, in order.
  30. func TestReservedEncryptionMatchesInline(t *testing.T) {
  31. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, stagedConfig{})
  32. packets, want := flowTestBatches(1, 3, 3, 9)
  33. r := txReserver{f: sender.f}
  34. items := r.collect(nil, packets[0], &firewall.ParsedPacket{}, make([]byte, mtu), 0, nil)
  35. require.Equal(t, uint64(len(want)), r.tunnels[0].n)
  36. sb := batch.NewSendBatch(sender.conn, 128, 65536)
  37. for _, it := range r.reserve(items) {
  38. sender.f.encryptTxItem(&it, make([]byte, 12), sb)
  39. }
  40. sender.f.flushSendBatch(sb, 0)
  41. require.Equal(t, want, decryptWire(t, receiver, sender.conn.captured()))
  42. }
  43. // TestSegmentCount checks tio.Packet.SegmentCount predicts exactly the segments tio.SegmentSuperpacket cuts, odd tails included.
  44. func TestSegmentCount(t *testing.T) {
  45. require.Equal(t, 1, tio.Packet{Bytes: make([]byte, 60)}.SegmentCount())
  46. for _, segs := range []int{1, 2, 3, 45} {
  47. for _, tail := range []int{0, 1, flowTestMSS - 1} {
  48. pkt := flowTCPSuperpacket(0, 0, segs)
  49. pkt.Bytes = pkt.Bytes[:len(pkt.Bytes)-tail]
  50. pkt.GSO = tio.GSOInfo{Size: flowTestMSS, HdrLen: 40, CsumStart: 20, Proto: tio.GSOProtoTCP}
  51. want, got := pkt.SegmentCount(), 0
  52. require.NoError(t, tio.SegmentSuperpacket(pkt, func([]byte) error { got++; return nil }))
  53. require.Equal(t, want, got, "segs %d tail %d", segs, tail)
  54. }
  55. }
  56. }
  57. // TestFlowStagedTXOrderAndCounters runs the staged transmit pipeline with shuffled workers and a slow socket, so the
  58. // sender lags and the reader runs into backpressure, then fails the tun read with batches in flight: the wire must
  59. // carry every segment once, in tun read order, under strictly increasing counters that were each reserved exactly
  60. // once, and the read error is still reported.
  61. func TestFlowStagedTXOrderAndCounters(t *testing.T) {
  62. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  63. for _, cipher := range []noise.CipherFunc{noiseutil.CipherAESGCM, noise.CipherChaChaPoly} {
  64. t.Run(cipher.CipherName(), func(t *testing.T) {
  65. if cipher.CipherName() == noiseutil.CipherAESGCM.CipherName() && noiseutil.EncryptLockNeeded {
  66. t.Skip("AES-GCM requires encryption in nonce order")
  67. }
  68. sender, receiver := newFlowTestPair(t, cipher, stagedOn(4))
  69. delayCrypto(sender, func(n uint64) {
  70. if n%54 == 0 {
  71. time.Sleep(time.Millisecond)
  72. }
  73. })
  74. // Far slower than the reader, even under the race detector
  75. sender.conn.delay = 8 * time.Millisecond
  76. batches, want := flowTestBatches(60, 6, 3, 9)
  77. start := sender.hostinfo.ConnectionState.messageCounter.Load()
  78. tx := metrics.GetOrRegisterCounter("flow.tx.batches", nil)
  79. tx0 := tx.Count()
  80. sender.f.listenIn(newFlowBatchQueue(batches), 0)
  81. require.Equal(t, int64(len(batches)), tx.Count()-tx0, "every batch uses the ordered sender")
  82. require.Equal(t, want, decryptWire(t, receiver, sender.conn.captured()), "wire order must match tun read order")
  83. require.Equal(t, start+uint64(len(want)), sender.hostinfo.ConnectionState.messageCounter.Load())
  84. require.NotNil(t, sender.f.fatalErr.Load())
  85. })
  86. }
  87. }
  88. // TestFlowStagedTXCloseInFlight closes the interface and tun while batches are in flight, 20 times over.
  89. func TestFlowStagedTXCloseInFlight(t *testing.T) {
  90. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  91. for range 20 {
  92. sender, _ := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(3))
  93. sender.conn.delay = time.Millisecond
  94. batches, _ := flowTestBatches(400, 4, 2, 8)
  95. q := newFlowBatchQueue(batches)
  96. done := make(chan struct{})
  97. go func() { sender.f.listenIn(q, 0); close(done) }()
  98. require.Eventually(t, func() bool { return q.reads.Load() > 5 }, 5*time.Second, 100*time.Microsecond)
  99. sender.f.closed.Store(true)
  100. require.NoError(t, q.Close())
  101. <-done
  102. }
  103. }
  104. // TestFlowStagedTXExhausted drops every batch of a tunnel whose counter runs out, like inline sends do.
  105. func TestFlowStagedTXExhausted(t *testing.T) {
  106. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  107. sender, _ := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
  108. ci := sender.hostinfo.ConnectionState
  109. ci.messageCounter.Store(RejectAfterMessages - 3)
  110. sender.f.listenIn(newFlowBatchQueue([][]tio.Packet{{flowTCPSuperpacket(0, 0, 10)}, {flowTCPSuperpacket(0, 1, 2)}}), 0)
  111. require.Empty(t, sender.conn.captured())
  112. require.Equal(t, RejectAfterMessages, ci.messageCounter.Load())
  113. require.Equal(t, int64(12), sender.f.messageMetrics.txExhausted.Count())
  114. require.Zero(t, ci.txBudget.state.Load())
  115. }
  116. // TestFlowStagedTXRelay sends staged batches, and an empty read, through a relay: outer and inner layers both verify,
  117. // in order, and go to the relay.
  118. // With a tiny MSS, the middle superpacket alone exceeds the tunnels' whole credit: it must be split, not admitted as an
  119. // unbounded lone batch, with either replay window.
  120. func TestFlowStagedTXRelay(t *testing.T) {
  121. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  122. for _, window := range budgetWindows {
  123. t.Run(fmt.Sprintf("window=%d", window), func(t *testing.T) {
  124. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
  125. sender.hostinfo.ConnectionState.window = NewBits(window)
  126. receiver.hostinfo.ConnectionState.window = NewBits(window)
  127. relayRemote, relayHI, relayPeer := addTestRelay(t, sender, window)
  128. batches, want := flowTestBatches(2, 3, 1, 6)
  129. var payload []byte
  130. large := flowTCPSuperpacket(0, 9, 45)
  131. large.GSO.Size = 1
  132. payload = append([]byte(nil), large.Bytes[40:]...)
  133. batches = [][]tio.Packet{batches[0], {large}, batches[1]}
  134. sender.f.listenIn(newFlowBatchQueue(append(batches, nil)), 0)
  135. wire := sender.conn.captured()
  136. require.Len(t, wire, len(want)+len(payload))
  137. var got []decodedSeg
  138. var tiny []byte
  139. var lastOuter, lastInner uint64
  140. for i, pkt := range wire {
  141. require.Equal(t, relayRemote, sender.conn.addrs[i])
  142. h := &header.H{}
  143. require.NoError(t, h.Parse(pkt))
  144. require.Greater(t, h.MessageCounter, lastOuter)
  145. lastOuter = h.MessageCounter
  146. require.NoError(t, relayPeer.VerifyRelay(nil, h.MessageCounter, pkt, make([]byte, 12)))
  147. pkt = pkt[header.Len : len(pkt)-16]
  148. require.NoError(t, h.Parse(pkt))
  149. require.Greater(t, h.MessageCounter, lastInner)
  150. lastInner = h.MessageCounter
  151. plain, err := receiver.hostinfo.ConnectionState.Decrypt(receiver.f.l, h.MessageCounter, pkt, make([]byte, 12))
  152. require.NoError(t, err)
  153. if len(plain) == 41 {
  154. tiny = append(tiny, plain[40])
  155. } else {
  156. got = append(got, decodeSeg(plain[40:]))
  157. }
  158. }
  159. require.Equal(t, want, got)
  160. require.Equal(t, payload, tiny)
  161. require.Zero(t, sender.hostinfo.ConnectionState.txBudget.state.Load())
  162. require.Zero(t, relayHI.ConnectionState.txBudget.state.Load())
  163. })
  164. }
  165. }
  166. // addTestRelay makes sender reach its peer only through a relay tunnel with replay windows of window. It returns the
  167. // relay's underlay address, its hostinfo and the relay's side of the tunnel.
  168. func addTestRelay(t testing.TB, sender *flowTestPeer, window uint64) (netip.AddrPort, *HostInfo, *ConnectionState) {
  169. relayInit, relayResp := runTestHandshake(t)
  170. relayCI, err := newConnectionStateFromResult(relayInit)
  171. require.NoError(t, err)
  172. relayPeer, err := newConnectionStateFromResult(relayResp)
  173. require.NoError(t, err)
  174. relayCI.window, relayPeer.window = NewBits(window), NewBits(window)
  175. relayAddr := netip.MustParseAddr("10.0.0.3")
  176. relayRemote := netip.MustParseAddrPort("192.0.2.3:4242")
  177. relay := &Relay{Type: TerminalType, State: Established, LocalIndex: 5, RemoteIndex: 6, PeerAddr: flowTestRemote}
  178. relayHI := &HostInfo{
  179. vpnAddrs: []netip.Addr{relayAddr},
  180. ConnectionState: relayCI,
  181. relayState: RelayState{
  182. relayForByAddr: map[netip.Addr]*Relay{flowTestRemote: relay},
  183. relayForByIdx: map[uint32]*Relay{relay.LocalIndex: relay},
  184. },
  185. }
  186. relayHI.remote.Store(&relayRemote)
  187. sender.f.hostMap.Hosts[relayAddr] = relayHI
  188. sender.hostinfo.remote.Store(&netip.AddrPort{})
  189. sender.hostinfo.relayState.relays = []netip.Addr{relayAddr}
  190. return relayRemote, relayHI, relayPeer
  191. }
  192. // affinityQueue makes every read start with an idle pipeline: before each read it waits until conn has sent what was
  193. // read before, recording how many sends that took, then samples the reader's actual thread affinity.
  194. type affinityQueue struct {
  195. *flowBatchQueue
  196. conn *affinityConn
  197. sent int
  198. boundaries []int
  199. masks []unix.CPUSet
  200. }
  201. func (q *affinityQueue) Read() ([]tio.Packet, error) {
  202. if n := q.reads.Load(); n > 0 {
  203. for _, p := range q.batches[n-1] {
  204. q.sent += p.SegmentCount()
  205. }
  206. }
  207. q.boundaries = append(q.boundaries, q.conn.waitFor(q.sent))
  208. q.masks = append(q.masks, threadMask())
  209. return q.flowBatchQueue.Read()
  210. }
  211. // affinityConn captures every send, sampling the sending thread's actual affinity in each WriteBatch.
  212. type affinityConn struct {
  213. flowCaptureConn
  214. masks []unix.CPUSet
  215. sizes []int
  216. }
  217. func (c *affinityConn) WriteBatch(p [][]byte, addrs []netip.AddrPort) (int, error) {
  218. mask := threadMask()
  219. c.mu.Lock()
  220. c.masks = append(c.masks, mask)
  221. c.sizes = append(c.sizes, len(p))
  222. c.mu.Unlock()
  223. return c.flowCaptureConn.WriteBatch(p, addrs)
  224. }
  225. // waitFor waits until n packets were sent and returns how many WriteBatch calls that took.
  226. func (c *affinityConn) waitFor(n int) int {
  227. for len(c.captured()) < n {
  228. time.Sleep(50 * time.Microsecond)
  229. }
  230. c.mu.Lock()
  231. defer c.mu.Unlock()
  232. return len(c.masks)
  233. }
  234. func threadMask() unix.CPUSet {
  235. var mask unix.CPUSet
  236. if err := unix.SchedGetaffinity(0, &mask); err != nil {
  237. panic(err)
  238. }
  239. return mask
  240. }
  241. // requireFlowCPU checks every mask is just cpu, or with cpu -1, the whole allowed set.
  242. func requireFlowCPU(t *testing.T, masks []unix.CPUSet, cpu int, allowed []int) {
  243. t.Helper()
  244. require.NotEmpty(t, masks)
  245. for _, mask := range masks {
  246. if cpu >= 0 {
  247. require.Equal(t, 1, mask.Count())
  248. require.True(t, mask.IsSet(cpu), "expected CPU %d", cpu)
  249. continue
  250. }
  251. require.Equal(t, len(allowed), mask.Count())
  252. for _, c := range allowed {
  253. require.True(t, mask.IsSet(c))
  254. }
  255. }
  256. }
  257. // TestFlowStagedTXAffinity sends large, small and large reads, each from an idle pipeline, and checks the order on the
  258. // wire and where the reader and each send actually ran, for every CPU placement, with one routine and with several. A
  259. // staged sender is only pinned to an explicit entry other than its reader's CPU, and inline sends stay on the reader.
  260. // With the threshold, only the small read goes inline.
  261. // The reader only reads again once its earlier reads are on the wire, so this also checks the sender never waits for
  262. // the reader. Pinning threads changes the process, so the test runs in a child.
  263. func TestFlowStagedTXAffinity(t *testing.T) {
  264. if os.Getenv("NEBULA_FLOW_AFFINITY_TEST") != "1" {
  265. cmd := exec.Command(os.Args[0], "-test.run=^TestFlowStagedTXAffinity$")
  266. cmd.Env = append(os.Environ(), "NEBULA_FLOW_AFFINITY_TEST=1")
  267. out, err := cmd.CombinedOutput()
  268. require.NoError(t, err, "%s", out)
  269. return
  270. }
  271. allowed, err := util.AllowedCPUs()
  272. require.NoError(t, err)
  273. for _, placement := range []string{"fallback", "automatic", "explicit", "explicit-single"} {
  274. for _, pin := range []bool{false, true} {
  275. for _, sc := range sendPaths() {
  276. for _, routines := range []int{1, 2} {
  277. t.Run(fmt.Sprintf("%s/pin=%t/%s/routines=%d", placement, pin, stagedName(sc), routines), func(t *testing.T) {
  278. if pin && len(allowed) < 2 {
  279. t.Skip("requires at least two allowed CPUs")
  280. }
  281. if sc.inlineBelow > 0 {
  282. // Switch on every read that wants the other mode, so the small read goes inline between
  283. // staged ones
  284. sc.gateHold = 1
  285. }
  286. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, sc)
  287. f := sender.f
  288. f.pinThreads, f.routines = pin, routines
  289. switch placement {
  290. case "automatic":
  291. f.defaultCpuAffinity = cpupick.Default(routines, 4242, nil)
  292. case "explicit":
  293. f.cpuAffinity = []int{allowed[len(allowed)-1], allowed[0]}
  294. case "explicit-single":
  295. f.cpuAffinity = allowed[len(allowed)-1:]
  296. }
  297. readerCPU, sendCPU := -1, -1
  298. if pin {
  299. readerCPU = f.threadCPU(0)
  300. sendCPU = readerCPU
  301. if sc.tx {
  302. // Never the reader's CPU, and no automatic choice: unpinned unless an explicit entry fits
  303. sendCPU = -1
  304. if placement == "explicit" {
  305. sendCPU = f.cpuAffinity[1]
  306. }
  307. }
  308. }
  309. smallCPU := sendCPU
  310. if sc.inlineBelow > 0 {
  311. smallCPU = readerCPU
  312. }
  313. conn := &affinityConn{}
  314. f.writers = []udp.Conn{conn}
  315. batches, want := flowTestBatches(3, 8, 1, 45)
  316. batches[1] = batches[1][:1]
  317. want = append(want[:405], want[720:]...)
  318. q := &affinityQueue{flowBatchQueue: newFlowBatchQueue(batches), conn: conn}
  319. inline := metrics.GetOrRegisterCounter("flow.tx.inline", nil)
  320. inline0 := inline.Count()
  321. f.listenIn(q, 0)
  322. require.Equal(t, want, decryptWire(t, receiver, conn.captured()))
  323. if sc.inlineBelow > 0 {
  324. require.Equal(t, int64(1), inline.Count()-inline0)
  325. }
  326. b := q.boundaries
  327. requireFlowCPU(t, q.masks, readerCPU, allowed)
  328. requireFlowCPU(t, conn.masks[:b[1]], sendCPU, allowed)
  329. requireFlowCPU(t, conn.masks[b[1]:b[2]], smallCPU, allowed)
  330. requireFlowCPU(t, conn.masks[b[2]:], sendCPU, allowed)
  331. })
  332. }
  333. }
  334. }
  335. }
  336. }
  337. // sendPaths is inline, staged and, with the threshold, adaptive transmit.
  338. func sendPaths() []stagedConfig {
  339. return []stagedConfig{{}, stagedOn(2), adaptive(2)}
  340. }
  341. // requireAccepted checks the receiver accepts every packet of wire in the order given. With several readers the wire
  342. // order differs from counter order, but every packet must still authenticate and fit within the replay window.
  343. func requireAccepted(t *testing.T, receiver *flowTestPeer, wire [][]byte) {
  344. t.Helper()
  345. for _, pkt := range wire {
  346. h := &header.H{}
  347. require.NoError(t, h.Parse(pkt))
  348. _, err := receiver.hostinfo.ConnectionState.Decrypt(receiver.f.l, h.MessageCounter, pkt, make([]byte, 12))
  349. require.NoError(t, err, "counter %d", h.MessageCounter)
  350. }
  351. }
  352. // slowConn delays every WriteBatch, then writes to the shared capture.
  353. type slowConn struct {
  354. *flowCaptureConn
  355. delay time.Duration
  356. }
  357. func (c slowConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) (int, error) {
  358. time.Sleep(c.delay)
  359. return c.flowCaptureConn.WriteBatch(bufs, addrs)
  360. }
  361. // TestFlowTXBudgetManyReaders saturates one tunnel from eight readers, one of them on a slow socket that lets the
  362. // others overtake it, alongside a burst of unreserved control messages: in the order they reach the wire, every packet
  363. // is accepted by the receiver's replay window, so none was sent twice or displaced past the window.
  364. func TestFlowTXBudgetManyReaders(t *testing.T) {
  365. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  366. for _, window := range budgetWindows {
  367. t.Run(fmt.Sprintf("window=%d", window), func(t *testing.T) {
  368. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(3))
  369. ci := sender.hostinfo.ConnectionState
  370. ci.window = NewBits(window)
  371. receiver.hostinfo.ConnectionState.window = NewBits(window)
  372. sender.f.routines = 8
  373. sender.f.writers = append([]udp.Conn{slowConn{sender.conn, 4 * time.Millisecond}}, slices.Repeat(sender.f.writers, 8)...)
  374. start := ci.messageCounter.Load()
  375. var wg sync.WaitGroup
  376. for q := range 8 {
  377. wg.Go(func() {
  378. batches, _ := flowTestBatches(28, 8, 2, 45)
  379. p := newStagedTx(sender.f, newFlowBatchQueue(nil), q)
  380. defer p.close()
  381. for _, packets := range batches {
  382. p.send(packets)
  383. }
  384. })
  385. }
  386. // A bounded burst of unreserved control messages uses the documented margin, on writer 8
  387. wg.Go(func() {
  388. for range 128 {
  389. sender.f.sendNoMetrics(header.Test, header.TestRequest, ci, sender.hostinfo, flowTestUnder, []byte("x"), make([]byte, 12), make([]byte, mtu), 8)
  390. time.Sleep(100 * time.Microsecond)
  391. }
  392. })
  393. wg.Wait()
  394. require.Zero(t, ci.txBudget.state.Load())
  395. wire := sender.conn.captured()
  396. require.Len(t, wire, 8*28*360+128)
  397. require.Equal(t, start+uint64(len(wire)), ci.messageCounter.Load())
  398. requireAccepted(t, receiver, wire)
  399. })
  400. }
  401. }
  402. // TestFlowStagedTXInlineOnlyWhenEmpty: a small read behind a staged batch still in flight must not go inline before
  403. // that batch is on the wire, it would overtake it.
  404. func TestFlowStagedTXInlineOnlyWhenEmpty(t *testing.T) {
  405. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  406. sc := adaptive(2)
  407. sc.inlineBelow, sc.gateHold = 4, 1
  408. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, sc)
  409. p := newStagedTx(sender.f, newFlowBatchQueue(nil), 0)
  410. block := make(chan struct{})
  411. delayCrypto(sender, func(uint64) { <-block })
  412. batches, want := flowTestBatches(1, 4, 1, 3)
  413. p.send(batches[0][:3]) // switches to fanout, its worker is blocked
  414. require.Equal(t, 1, p.inFlight)
  415. unblock := time.AfterFunc(20*time.Millisecond, func() { close(block) })
  416. defer unblock.Stop()
  417. p.send(batches[0][3:]) // switches to inline, which waits for the staged batch first
  418. require.Zero(t, p.inFlight)
  419. p.close()
  420. require.Equal(t, want, decryptWire(t, receiver, sender.conn.captured()))
  421. }
  422. // TestFlowStagedTXInlineBelowZero: with inline_below 0 an idle reader never sends a batch itself, in either
  423. // direction: every read goes through the workers and the ordered sender or writer.
  424. func TestFlowStagedTXInlineBelowZero(t *testing.T) {
  425. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  426. l := test.NewLogger()
  427. c := config.NewC(l)
  428. c.Settings = map[string]any{"tun": map[string]any{"staged": map[string]any{"workers": 2, "inline_below": 0}}}
  429. sc := parseStagedConfig(c, l)
  430. if !sc.tx {
  431. t.Skip("transmit stays inline in this build")
  432. }
  433. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, sc)
  434. batches, want := flowTestBatches(20, 2, 1, 4)
  435. txInline := metrics.GetOrRegisterCounter("flow.tx.inline", nil)
  436. tx := metrics.GetOrRegisterCounter("flow.tx.batches", nil)
  437. rxInline := metrics.GetOrRegisterCounter("flow.rx.inline", nil)
  438. txInline0, tx0, rxInline0, rx0 := txInline.Count(), tx.Count(), rxInline.Count(), rxBatches.Count()
  439. sender.f.listenIn(newFlowBatchQueue(batches), 0)
  440. wire := sender.conn.captured()
  441. require.Equal(t, want, decryptWire(t, receiver, wire))
  442. require.Zero(t, txInline.Count()-txInline0)
  443. require.Equal(t, int64(len(batches)), tx.Count()-tx0)
  444. receiver.receive(&rxConn{wire: wire, sizes: []int{1, 2, 4}})
  445. require.Equal(t, want, receiver.tun.segments(t))
  446. require.Zero(t, rxInline.Count()-rxInline0)
  447. require.Positive(t, rxBatches.Count()-rx0)
  448. }
  449. // TestFlowStagedTXDisabled: with tx false the reader never builds a staged pipeline, even on a queue that could hand
  450. // off its batches. Receive stays staged.
  451. func TestFlowStagedTXDisabled(t *testing.T) {
  452. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  453. l := test.NewLogger()
  454. c := config.NewC(l)
  455. c.Settings = map[string]any{"tun": map[string]any{"staged": map[string]any{"workers": 2, "tx": false}}}
  456. sc := parseStagedConfig(c, l)
  457. require.False(t, sc.tx)
  458. require.True(t, sc.rx)
  459. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, sc)
  460. batches, want := flowTestBatches(4, 8, 1, 45)
  461. tx := metrics.GetOrRegisterCounter("flow.tx.batches", nil)
  462. txInline := metrics.GetOrRegisterCounter("flow.tx.inline", nil)
  463. tx0, txInline0, rx0 := tx.Count(), txInline.Count(), rxBatches.Count()
  464. q := &swapCountQueue{flowBatchQueue: newFlowBatchQueue(batches)}
  465. sender.f.listenIn(q, 0)
  466. wire := sender.conn.captured()
  467. require.Equal(t, want, decryptWire(t, receiver, wire))
  468. require.Zero(t, q.swaps, "newStagedTx always takes the queue's buffer, so it never ran")
  469. require.Equal(t, tx0, tx.Count())
  470. require.Equal(t, txInline0, txInline.Count())
  471. receiver.receive(&rxConn{wire: wire, batchLen: 512})
  472. require.Equal(t, want, receiver.tun.segments(t))
  473. require.Greater(t, rxBatches.Count(), rx0)
  474. }
  475. // swapCountQueue counts SwapRxBuf calls.
  476. type swapCountQueue struct {
  477. *flowBatchQueue
  478. swaps int
  479. }
  480. func (q *swapCountQueue) SwapRxBuf(buf []byte) []byte {
  481. q.swaps++
  482. return q.flowBatchQueue.SwapRxBuf(buf)
  483. }
  484. // TestFlowStagedTXAdaptiveBudget: a fast reader's adaptive sends must stop spending credit until another reader's
  485. // earlier reservation reaches the wire.
  486. func TestFlowStagedTXAdaptiveBudget(t *testing.T) {
  487. for _, shape := range []struct {
  488. name string
  489. packets, segments int
  490. }{
  491. {"single", 1, 1}, {"batch", 8, 30},
  492. } {
  493. t.Run(shape.name, func(t *testing.T) {
  494. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  495. sc := adaptive(2)
  496. // p0's large read must go to the workers at once, p1 stays inline
  497. sc.gateHold = 1
  498. sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, sc)
  499. sender.f.routines = 2
  500. sender.f.writers = []udp.Conn{sender.conn, sender.conn}
  501. ci := sender.hostinfo.ConnectionState
  502. start := ci.messageCounter.Load()
  503. entered, unblock := make(chan struct{}), make(chan struct{})
  504. delayCrypto(sender, func(n uint64) {
  505. if n == start+1 {
  506. close(entered)
  507. <-unblock
  508. }
  509. })
  510. p0 := newStagedTx(sender.f, newFlowBatchQueue(nil), 0)
  511. p1 := newStagedTx(sender.f, newFlowBatchQueue(nil), 1)
  512. defer p0.close()
  513. defer p1.close()
  514. var release sync.Once
  515. defer release.Do(func() { close(unblock) })
  516. large, _ := flowTestBatches(1, 8, 1, 45)
  517. p0.send(large[0])
  518. <-entered
  519. perBatch := shape.packets * shape.segments
  520. small, _ := flowTestBatches(9600/perBatch, shape.packets, 1, shape.segments)
  521. fits := int(ci.txBudgetLimit()-360) / perBatch
  522. for _, packets := range small[:fits] {
  523. p1.send(packets)
  524. }
  525. require.Equal(t, uint64(360+fits*perBatch), (ci.txBudget.state.Load()>>24)&txBudgetMask, "inline sends must spend shared credit")
  526. done := make(chan struct{})
  527. go func() {
  528. defer close(done)
  529. for _, packets := range small[fits:] {
  530. p1.send(packets)
  531. }
  532. }()
  533. select {
  534. case <-done:
  535. t.Error("adaptive reader bypassed outstanding credit")
  536. case <-time.After(50 * time.Millisecond):
  537. }
  538. require.LessOrEqual(t, ci.messageCounter.Load()-start, ci.txBudgetLimit())
  539. release.Do(func() { close(unblock) })
  540. <-done
  541. p0.drain()
  542. p1.drain()
  543. wire := sender.conn.captured()
  544. require.Len(t, wire, 360+40*240)
  545. requireAccepted(t, receiver, wire)
  546. require.Zero(t, ci.txBudget.state.Load())
  547. })
  548. }
  549. }
  550. // BenchmarkFlowStagedTX runs reads of eight 45-segment superpackets through listenIn into a socket that discards
  551. // them, inline and staged.
  552. func BenchmarkFlowStagedTX(b *testing.B) {
  553. for _, cipher := range []noise.CipherFunc{noiseutil.CipherAESGCM, noise.CipherChaChaPoly} {
  554. for _, sc := range []stagedConfig{{}, stagedOn(4)} {
  555. b.Run(cipher.CipherName()+"/"+stagedName(sc), func(b *testing.B) {
  556. if sc.tx && cipher.CipherName() == noiseutil.CipherAESGCM.CipherName() && noiseutil.EncryptLockNeeded {
  557. b.Skip("AES-GCM requires encryption in nonce order")
  558. }
  559. sender, _ := newFlowTestPair(b, cipher, sc)
  560. sender.f.writers = []udp.Conn{udp.NoopConn{}}
  561. batch, _ := flowTestBatches(1, 8, 4, 45)
  562. batches := make([][]tio.Packet, b.N)
  563. for i := range batches {
  564. batches[i] = batch[0]
  565. }
  566. b.SetBytes(int64(8 * 45 * flowTestMSS))
  567. b.ReportAllocs()
  568. b.ResetTimer()
  569. sender.f.listenIn(newFlowBatchQueue(batches), 0)
  570. })
  571. }
  572. }
  573. }