netchange_darwin_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. //go:build darwin && !ios && !e2e_testing
  2. package udp
  3. import (
  4. "context"
  5. "encoding/binary"
  6. "os"
  7. "testing"
  8. "time"
  9. "github.com/slackhq/nebula/config"
  10. "github.com/slackhq/nebula/test"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. "go.uber.org/goleak"
  14. "golang.org/x/sys/unix"
  15. )
  16. // routeMsg builds the first four bytes of a routing message, which is all isNetworkChange reads.
  17. func routeMsg(msgType uint8, extra int) []byte {
  18. msg := make([]byte, 4+extra)
  19. binary.NativeEndian.PutUint16(msg[0:2], uint16(len(msg)))
  20. msg[2] = unix.RTM_VERSION
  21. msg[3] = msgType
  22. return msg
  23. }
  24. func TestIsNetworkChange(t *testing.T) {
  25. // The three that mean our addressing may have moved
  26. assert.True(t, isNetworkChange(routeMsg(unix.RTM_NEWADDR, 0)))
  27. assert.True(t, isNetworkChange(routeMsg(unix.RTM_DELADDR, 0)))
  28. assert.True(t, isNetworkChange(routeMsg(unix.RTM_IFINFO, 0)))
  29. // Route churn is not something a rebind helps with
  30. assert.False(t, isNetworkChange(routeMsg(unix.RTM_ADD, 0)))
  31. assert.False(t, isNetworkChange(routeMsg(unix.RTM_DELETE, 0)))
  32. assert.False(t, isNetworkChange(routeMsg(unix.RTM_GET, 0)))
  33. // Garbage must not be mistaken for a change
  34. assert.False(t, isNetworkChange(nil), "empty")
  35. assert.False(t, isNetworkChange([]byte{0, 0, 0}), "short header")
  36. wrongVersion := routeMsg(unix.RTM_NEWADDR, 0)
  37. wrongVersion[2] = unix.RTM_VERSION + 1
  38. assert.False(t, isNetworkChange(wrongVersion), "wrong rtm_version")
  39. lying := routeMsg(unix.RTM_NEWADDR, 0)
  40. binary.NativeEndian.PutUint16(lying[0:2], 512)
  41. assert.False(t, isNetworkChange(lying), "msglen longer than what we read")
  42. }
  43. // socketPair returns a connected pair of datagram sockets, the first wrapped the same way the routing socket is. It
  44. // stands in for the kernel so the watch loop can be driven with synthetic messages.
  45. func socketPair(t *testing.T) (*os.File, int) {
  46. t.Helper()
  47. fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
  48. require.NoError(t, err)
  49. require.NoError(t, unix.SetNonblock(fds[0], true))
  50. f := os.NewFile(uintptr(fds[0]), "route")
  51. t.Cleanup(func() {
  52. _ = f.Close()
  53. _ = unix.Close(fds[1])
  54. })
  55. return f, fds[1]
  56. }
  57. func TestWatchRouteSocketCoalescesABurst(t *testing.T) {
  58. sock, kernel := socketPair(t)
  59. changes := make(chan struct{}, 1)
  60. done := make(chan struct{})
  61. go func() {
  62. watchRouteSocket(test.NewLogger(), sock, changes)
  63. close(done)
  64. }()
  65. // One network change is a burst of messages. All of these land inside the settle window, so they must produce
  66. // exactly one report rather than one apiece.
  67. for range 5 {
  68. _, err := unix.Write(kernel, routeMsg(unix.RTM_NEWADDR, 8))
  69. require.NoError(t, err)
  70. }
  71. // Uninteresting messages in the middle of a burst must not add a report of their own either.
  72. _, err := unix.Write(kernel, routeMsg(unix.RTM_ADD, 8))
  73. require.NoError(t, err)
  74. select {
  75. case <-changes:
  76. case <-time.After(netChangeSettleWindow * 4):
  77. t.Fatal("a burst should have reported a change")
  78. }
  79. // Nothing more from that burst
  80. select {
  81. case <-changes:
  82. t.Fatal("a burst should report exactly once")
  83. case <-time.After(netChangeSettleWindow):
  84. }
  85. // A change after the window has closed is a separate event and gets its own report.
  86. _, err = unix.Write(kernel, routeMsg(unix.RTM_IFINFO, 8))
  87. require.NoError(t, err)
  88. select {
  89. case <-changes:
  90. case <-time.After(netChangeSettleWindow * 4):
  91. t.Fatal("a later change should report again")
  92. }
  93. // Closing the socket is how the real thing shuts down
  94. require.NoError(t, sock.Close())
  95. select {
  96. case <-done:
  97. case <-time.After(time.Second * 5):
  98. t.Fatal("watchRouteSocket did not return after the socket was closed")
  99. }
  100. }
  101. func TestWatchRouteSocketIgnoresUninterestingMessages(t *testing.T) {
  102. sock, kernel := socketPair(t)
  103. changes := make(chan struct{}, 1)
  104. done := make(chan struct{})
  105. go func() {
  106. watchRouteSocket(test.NewLogger(), sock, changes)
  107. close(done)
  108. }()
  109. for _, msgType := range []uint8{unix.RTM_ADD, unix.RTM_DELETE, unix.RTM_GET, unix.RTM_MISS} {
  110. _, err := unix.Write(kernel, routeMsg(msgType, 8))
  111. require.NoError(t, err)
  112. }
  113. select {
  114. case <-changes:
  115. t.Fatal("route churn alone must not report a change")
  116. case <-time.After(netChangeSettleWindow * 2):
  117. }
  118. require.NoError(t, sock.Close())
  119. select {
  120. case <-done:
  121. case <-time.After(time.Second * 5):
  122. t.Fatal("watchRouteSocket did not return after the socket was closed")
  123. }
  124. }
  125. // TestWatchRouteSocketDropsRatherThanBlocks covers the coalescing send. A reader that is busy rebinding must not
  126. // wedge the watcher, and a second pending "the network moved" tells it nothing new anyway.
  127. func TestWatchRouteSocketDropsRatherThanBlocks(t *testing.T) {
  128. sock, kernel := socketPair(t)
  129. changes := make(chan struct{}, 1)
  130. done := make(chan struct{})
  131. go func() {
  132. watchRouteSocket(test.NewLogger(), sock, changes)
  133. close(done)
  134. }()
  135. // Nobody is reading changes, so after the first report the buffer is full for the rest of this test
  136. for range 3 {
  137. _, err := unix.Write(kernel, routeMsg(unix.RTM_NEWADDR, 8))
  138. require.NoError(t, err)
  139. time.Sleep(netChangeSettleWindow + time.Millisecond*250)
  140. }
  141. // The watcher must still be alive and responsive to a close
  142. require.NoError(t, sock.Close())
  143. select {
  144. case <-done:
  145. case <-time.After(time.Second * 5):
  146. t.Fatal("watchRouteSocket wedged on a full channel")
  147. }
  148. assert.Len(t, changes, 1, "the pending report should have coalesced, not queued")
  149. }
  150. // TestWatchNetworkChangesStopsWithContext covers the detection path against a real routing socket, including that
  151. // cancelling the context closes the channel so a ranging caller falls out of its loop.
  152. func TestWatchNetworkChangesStopsWithContext(t *testing.T) {
  153. ctx, cancel := context.WithCancel(context.Background())
  154. changes, err := watchNetworkChanges(ctx, test.NewLogger())
  155. require.NoError(t, err)
  156. require.NotNil(t, changes, "darwin should support watching")
  157. drained := make(chan struct{})
  158. go func() {
  159. for range changes {
  160. }
  161. close(drained)
  162. }()
  163. cancel()
  164. select {
  165. case <-drained:
  166. case <-time.After(time.Second * 5):
  167. t.Fatal("cancelling the context should close the changes channel")
  168. }
  169. }
  170. // TestNetworkChangeMonitorStopsWithContext drives the whole monitor against a real routing socket: Start must block
  171. // watching, and cancelling the context (which is all Control does on shutdown, it never stops the monitor directly)
  172. // must return it and clean up the watch goroutines.
  173. func TestNetworkChangeMonitorStopsWithContext(t *testing.T) {
  174. // IgnoreCurrent because other tests in this package leave readers running; we only care about what this test
  175. // leaks itself.
  176. defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
  177. ctx, cancel := context.WithCancel(context.Background())
  178. l := test.NewLogger()
  179. c := config.NewC(l)
  180. require.NoError(t, c.LoadString("listen:\n rebind_on_network_change: true\n"))
  181. m := NewNetworkChangeMonitor(ctx, l, c)
  182. done := make(chan struct{})
  183. go func() {
  184. m.Start(func() {})
  185. close(done)
  186. }()
  187. // Start should be sitting on the routing socket, not have fallen out. If it returned early it either failed to
  188. // watch or no-op'd, both of which we want to catch.
  189. select {
  190. case <-done:
  191. t.Fatal("Start returned instead of watching")
  192. case <-time.After(time.Millisecond * 250):
  193. }
  194. cancel()
  195. select {
  196. case <-done:
  197. case <-time.After(time.Second * 5):
  198. t.Fatal("Start did not return after the context was cancelled")
  199. }
  200. // Starting again after the context is dead must not open anything.
  201. m.Start(func() {})
  202. }