| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- //go:build darwin && !ios && !e2e_testing
- package udp
- import (
- "context"
- "encoding/binary"
- "os"
- "testing"
- "time"
- "github.com/slackhq/nebula/config"
- "github.com/slackhq/nebula/test"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "go.uber.org/goleak"
- "golang.org/x/sys/unix"
- )
- // routeMsg builds the first four bytes of a routing message, which is all isNetworkChange reads.
- func routeMsg(msgType uint8, extra int) []byte {
- msg := make([]byte, 4+extra)
- binary.NativeEndian.PutUint16(msg[0:2], uint16(len(msg)))
- msg[2] = unix.RTM_VERSION
- msg[3] = msgType
- return msg
- }
- func TestIsNetworkChange(t *testing.T) {
- // The three that mean our addressing may have moved
- assert.True(t, isNetworkChange(routeMsg(unix.RTM_NEWADDR, 0)))
- assert.True(t, isNetworkChange(routeMsg(unix.RTM_DELADDR, 0)))
- assert.True(t, isNetworkChange(routeMsg(unix.RTM_IFINFO, 0)))
- // Route churn is not something a rebind helps with
- assert.False(t, isNetworkChange(routeMsg(unix.RTM_ADD, 0)))
- assert.False(t, isNetworkChange(routeMsg(unix.RTM_DELETE, 0)))
- assert.False(t, isNetworkChange(routeMsg(unix.RTM_GET, 0)))
- // Garbage must not be mistaken for a change
- assert.False(t, isNetworkChange(nil), "empty")
- assert.False(t, isNetworkChange([]byte{0, 0, 0}), "short header")
- wrongVersion := routeMsg(unix.RTM_NEWADDR, 0)
- wrongVersion[2] = unix.RTM_VERSION + 1
- assert.False(t, isNetworkChange(wrongVersion), "wrong rtm_version")
- lying := routeMsg(unix.RTM_NEWADDR, 0)
- binary.NativeEndian.PutUint16(lying[0:2], 512)
- assert.False(t, isNetworkChange(lying), "msglen longer than what we read")
- }
- // socketPair returns a connected pair of datagram sockets, the first wrapped the same way the routing socket is. It
- // stands in for the kernel so the watch loop can be driven with synthetic messages.
- func socketPair(t *testing.T) (*os.File, int) {
- t.Helper()
- fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
- require.NoError(t, err)
- require.NoError(t, unix.SetNonblock(fds[0], true))
- f := os.NewFile(uintptr(fds[0]), "route")
- t.Cleanup(func() {
- _ = f.Close()
- _ = unix.Close(fds[1])
- })
- return f, fds[1]
- }
- func TestWatchRouteSocketCoalescesABurst(t *testing.T) {
- sock, kernel := socketPair(t)
- changes := make(chan struct{}, 1)
- done := make(chan struct{})
- go func() {
- watchRouteSocket(test.NewLogger(), sock, changes)
- close(done)
- }()
- // One network change is a burst of messages. All of these land inside the settle window, so they must produce
- // exactly one report rather than one apiece.
- for range 5 {
- _, err := unix.Write(kernel, routeMsg(unix.RTM_NEWADDR, 8))
- require.NoError(t, err)
- }
- // Uninteresting messages in the middle of a burst must not add a report of their own either.
- _, err := unix.Write(kernel, routeMsg(unix.RTM_ADD, 8))
- require.NoError(t, err)
- select {
- case <-changes:
- case <-time.After(netChangeSettleWindow * 4):
- t.Fatal("a burst should have reported a change")
- }
- // Nothing more from that burst
- select {
- case <-changes:
- t.Fatal("a burst should report exactly once")
- case <-time.After(netChangeSettleWindow):
- }
- // A change after the window has closed is a separate event and gets its own report.
- _, err = unix.Write(kernel, routeMsg(unix.RTM_IFINFO, 8))
- require.NoError(t, err)
- select {
- case <-changes:
- case <-time.After(netChangeSettleWindow * 4):
- t.Fatal("a later change should report again")
- }
- // Closing the socket is how the real thing shuts down
- require.NoError(t, sock.Close())
- select {
- case <-done:
- case <-time.After(time.Second * 5):
- t.Fatal("watchRouteSocket did not return after the socket was closed")
- }
- }
- func TestWatchRouteSocketIgnoresUninterestingMessages(t *testing.T) {
- sock, kernel := socketPair(t)
- changes := make(chan struct{}, 1)
- done := make(chan struct{})
- go func() {
- watchRouteSocket(test.NewLogger(), sock, changes)
- close(done)
- }()
- for _, msgType := range []uint8{unix.RTM_ADD, unix.RTM_DELETE, unix.RTM_GET, unix.RTM_MISS} {
- _, err := unix.Write(kernel, routeMsg(msgType, 8))
- require.NoError(t, err)
- }
- select {
- case <-changes:
- t.Fatal("route churn alone must not report a change")
- case <-time.After(netChangeSettleWindow * 2):
- }
- require.NoError(t, sock.Close())
- select {
- case <-done:
- case <-time.After(time.Second * 5):
- t.Fatal("watchRouteSocket did not return after the socket was closed")
- }
- }
- // TestWatchRouteSocketDropsRatherThanBlocks covers the coalescing send. A reader that is busy rebinding must not
- // wedge the watcher, and a second pending "the network moved" tells it nothing new anyway.
- func TestWatchRouteSocketDropsRatherThanBlocks(t *testing.T) {
- sock, kernel := socketPair(t)
- changes := make(chan struct{}, 1)
- done := make(chan struct{})
- go func() {
- watchRouteSocket(test.NewLogger(), sock, changes)
- close(done)
- }()
- // Nobody is reading changes, so after the first report the buffer is full for the rest of this test
- for range 3 {
- _, err := unix.Write(kernel, routeMsg(unix.RTM_NEWADDR, 8))
- require.NoError(t, err)
- time.Sleep(netChangeSettleWindow + time.Millisecond*250)
- }
- // The watcher must still be alive and responsive to a close
- require.NoError(t, sock.Close())
- select {
- case <-done:
- case <-time.After(time.Second * 5):
- t.Fatal("watchRouteSocket wedged on a full channel")
- }
- assert.Len(t, changes, 1, "the pending report should have coalesced, not queued")
- }
- // TestWatchNetworkChangesStopsWithContext covers the detection path against a real routing socket, including that
- // cancelling the context closes the channel so a ranging caller falls out of its loop.
- func TestWatchNetworkChangesStopsWithContext(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- changes, err := watchNetworkChanges(ctx, test.NewLogger())
- require.NoError(t, err)
- require.NotNil(t, changes, "darwin should support watching")
- drained := make(chan struct{})
- go func() {
- for range changes {
- }
- close(drained)
- }()
- cancel()
- select {
- case <-drained:
- case <-time.After(time.Second * 5):
- t.Fatal("cancelling the context should close the changes channel")
- }
- }
- // TestNetworkChangeMonitorStopsWithContext drives the whole monitor against a real routing socket: Start must block
- // watching, and cancelling the context (which is all Control does on shutdown, it never stops the monitor directly)
- // must return it and clean up the watch goroutines.
- func TestNetworkChangeMonitorStopsWithContext(t *testing.T) {
- // IgnoreCurrent because other tests in this package leave readers running; we only care about what this test
- // leaks itself.
- defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
- ctx, cancel := context.WithCancel(context.Background())
- l := test.NewLogger()
- c := config.NewC(l)
- require.NoError(t, c.LoadString("listen:\n rebind_on_network_change: true\n"))
- m := NewNetworkChangeMonitor(ctx, l, c)
- done := make(chan struct{})
- go func() {
- m.Start(func() {})
- close(done)
- }()
- // Start should be sitting on the routing socket, not have fallen out. If it returned early it either failed to
- // watch or no-op'd, both of which we want to catch.
- select {
- case <-done:
- t.Fatal("Start returned instead of watching")
- case <-time.After(time.Millisecond * 250):
- }
- cancel()
- select {
- case <-done:
- case <-time.After(time.Second * 5):
- t.Fatal("Start did not return after the context was cancelled")
- }
- // Starting again after the context is dead must not open anything.
- m.Start(func() {})
- }
|