netchange_darwin_test.go 7.3 KB

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