test(quicnettest): assert rebind source change and upstream bind family
Extract upstreamBindAddr so each server address family is covered without host networking, and check that Rebind changes the upstream source address.
- commit
- 0d9258e879d992aa7439a6a45920b00316b1feb7
- author
- brice · · 2026-10-07 07:01 UTC
- parent
- 767263f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +36 -11
M internal/adapters/quicnettest/listen_test.go +18 -0
| @@ -24,6 +24,7 @@ func TestExplicitListenSurvivesRebind(t *testing.T) { | |||
| 24 | 24 | client, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip}) | |
| 25 | 25 | require.NoError(t, err) | |
| 26 | 26 | defer client.Close() | |
| 27 | + | var sources []string | |
| 27 | 28 | for attempt := 0; attempt < 2; attempt++ { | |
| 28 | 29 | if attempt == 1 { | |
| 29 | 30 | require.NoError(t, proxy.Rebind()) | |
| @@ -35,6 +36,7 @@ func TestExplicitListenSurvivesRebind(t *testing.T) { | |||
| 35 | 36 | n, from, err := server.ReadFromUDP(data) | |
| 36 | 37 | require.NoError(t, err) | |
| 37 | 38 | require.Equal(t, "fixture", string(data[:n])) | |
| 39 | + | sources = append(sources, from.String()) | |
| 38 | 40 | _, err = server.WriteToUDP(data[:n], from) | |
| 39 | 41 | require.NoError(t, err) | |
| 40 | 42 | require.NoError(t, client.SetReadDeadline(time.Now().Add(time.Second))) | |
| @@ -42,6 +44,22 @@ func TestExplicitListenSurvivesRebind(t *testing.T) { | |||
| 42 | 44 | require.NoError(t, err) | |
| 43 | 45 | require.Equal(t, "fixture", string(data[:n])) | |
| 44 | 46 | } | |
| 47 | + | require.NotEqual(t, sources[0], sources[1], "rebind must change the upstream source address") | |
| 48 | + | }) | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func TestUpstreamBindAddrMatchesServerFamily(t *testing.T) { | |
| 53 | + | for _, tt := range []struct { | |
| 54 | + | server, want net.IP | |
| 55 | + | }{ | |
| 56 | + | {net.IPv4(127, 0, 0, 1), net.IPv4(127, 0, 0, 1)}, | |
| 57 | + | {net.IPv4(192, 0, 2, 7), net.IPv4zero}, | |
| 58 | + | {net.IPv6loopback, net.IPv6loopback}, | |
| 59 | + | {net.ParseIP("2001:db8::7"), net.IPv6unspecified}, | |
| 60 | + | } { | |
| 61 | + | t.Run(tt.server.String(), func(t *testing.T) { | |
| 62 | + | require.True(t, upstreamBindAddr(tt.server).IP.Equal(tt.want)) | |
| 45 | 63 | }) | |
| 46 | 64 | } | |
| 47 | 65 | } | |
| @@ -24,6 +24,7 @@ func TestExplicitListenSurvivesRebind(t *testing.T) { | |||||
| 24 | client, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip}) | 24 | client, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip}) | ||
| 25 | require.NoError(t, err) | 25 | require.NoError(t, err) | ||
| 26 | defer client.Close() | 26 | defer client.Close() | ||
| 27 | + | var sources []string | |||
| 27 | for attempt := 0; attempt < 2; attempt++ { | 28 | for attempt := 0; attempt < 2; attempt++ { | ||
| 28 | if attempt == 1 { | 29 | if attempt == 1 { | ||
| 29 | require.NoError(t, proxy.Rebind()) | 30 | require.NoError(t, proxy.Rebind()) | ||
| @@ -35,6 +36,7 @@ func TestExplicitListenSurvivesRebind(t *testing.T) { | |||||
| 35 | n, from, err := server.ReadFromUDP(data) | 36 | n, from, err := server.ReadFromUDP(data) | ||
| 36 | require.NoError(t, err) | 37 | require.NoError(t, err) | ||
| 37 | require.Equal(t, "fixture", string(data[:n])) | 38 | require.Equal(t, "fixture", string(data[:n])) | ||
| 39 | + | sources = append(sources, from.String()) | |||
| 38 | _, err = server.WriteToUDP(data[:n], from) | 40 | _, err = server.WriteToUDP(data[:n], from) | ||
| 39 | require.NoError(t, err) | 41 | require.NoError(t, err) | ||
| 40 | require.NoError(t, client.SetReadDeadline(time.Now().Add(time.Second))) | 42 | require.NoError(t, client.SetReadDeadline(time.Now().Add(time.Second))) | ||
| @@ -42,6 +44,22 @@ func TestExplicitListenSurvivesRebind(t *testing.T) { | |||||
| 42 | require.NoError(t, err) | 44 | require.NoError(t, err) | ||
| 43 | require.Equal(t, "fixture", string(data[:n])) | 45 | require.Equal(t, "fixture", string(data[:n])) | ||
| 44 | } | 46 | } | ||
| 47 | + | require.NotEqual(t, sources[0], sources[1], "rebind must change the upstream source address") | |||
| 48 | + | }) | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func TestUpstreamBindAddrMatchesServerFamily(t *testing.T) { | |||
| 53 | + | for _, tt := range []struct { | |||
| 54 | + | server, want net.IP | |||
| 55 | + | }{ | |||
| 56 | + | {net.IPv4(127, 0, 0, 1), net.IPv4(127, 0, 0, 1)}, | |||
| 57 | + | {net.IPv4(192, 0, 2, 7), net.IPv4zero}, | |||
| 58 | + | {net.IPv6loopback, net.IPv6loopback}, | |||
| 59 | + | {net.ParseIP("2001:db8::7"), net.IPv6unspecified}, | |||
| 60 | + | } { | |||
| 61 | + | t.Run(tt.server.String(), func(t *testing.T) { | |||
| 62 | + | require.True(t, upstreamBindAddr(tt.server).IP.Equal(tt.want)) | |||
| 45 | }) | 63 | }) | ||
| 46 | } | 64 | } | ||
| 47 | } | 65 | } | ||
M internal/adapters/quicnettest/proxy.go +18 -11
| @@ -13,8 +13,24 @@ import ( | |||
| 13 | 13 | // ErrClosed is returned by operations on a closed proxy. | |
| 14 | 14 | var ErrClosed = errors.New("quicnettest: proxy is closed") | |
| 15 | 15 | ||
| 16 | + | // upstreamBindAddr returns the ephemeral upstream bind address in the server's | |
| 17 | + | // address family: loopback for a loopback server, unspecified otherwise. | |
| 18 | + | func upstreamBindAddr(server net.IP) *net.UDPAddr { | |
| 19 | + | switch { | |
| 20 | + | case server.To4() != nil && server.IsLoopback(): | |
| 21 | + | return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} | |
| 22 | + | case server.To4() != nil: | |
| 23 | + | return &net.UDPAddr{IP: net.IPv4zero} | |
| 24 | + | case server.IsLoopback(): | |
| 25 | + | return &net.UDPAddr{IP: net.IPv6loopback} | |
| 26 | + | default: | |
| 27 | + | return &net.UDPAddr{IP: net.IPv6unspecified} | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 16 | 31 | // Proxy is a bounded, deterministic UDP relay with per-direction impairment. It | |
| 17 | - | // listens on an ephemeral loopback port for clients and forwards to | |
| 32 | + | // listens on Config.ListenAddr (default: ephemeral IPv4 loopback) for clients | |
| 33 | + | // and forwards to | |
| 18 | 34 | // Config.ServerAddr from a separate upstream socket that can be rebound to a | |
| 19 | 35 | // new source address at any time. | |
| 20 | 36 | type Proxy struct { | |
| @@ -136,16 +152,7 @@ func New(cfg Config) (*Proxy, error) { | |||
| 136 | 152 | if err != nil { | |
| 137 | 153 | return nil, fmt.Errorf("quicnettest: listen client socket: %w", err) | |
| 138 | 154 | } | |
| 139 | - | upstreamAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} | |
| 140 | - | if !cfg.ServerAddr.IP.IsLoopback() { | |
| 141 | - | upstreamAddr.IP = net.IPv4zero | |
| 142 | - | } | |
| 143 | - | if cfg.ServerAddr.IP.To4() == nil { | |
| 144 | - | upstreamAddr.IP = net.IPv6unspecified | |
| 145 | - | if cfg.ServerAddr.IP.IsLoopback() { | |
| 146 | - | upstreamAddr.IP = net.IPv6loopback | |
| 147 | - | } | |
| 148 | - | } | |
| 155 | + | upstreamAddr := upstreamBindAddr(cfg.ServerAddr.IP) | |
| 149 | 156 | upstream, err := net.ListenUDP("udp", upstreamAddr) | |
| 150 | 157 | if err != nil { | |
| 151 | 158 | _ = clientConn.Close() | |
| @@ -13,8 +13,24 @@ import ( | |||||
| 13 | // ErrClosed is returned by operations on a closed proxy. | 13 | // ErrClosed is returned by operations on a closed proxy. | ||
| 14 | var ErrClosed = errors.New("quicnettest: proxy is closed") | 14 | var ErrClosed = errors.New("quicnettest: proxy is closed") | ||
| 15 | 15 | ||||
| 16 | + | // upstreamBindAddr returns the ephemeral upstream bind address in the server's | |||
| 17 | + | // address family: loopback for a loopback server, unspecified otherwise. | |||
| 18 | + | func upstreamBindAddr(server net.IP) *net.UDPAddr { | |||
| 19 | + | switch { | |||
| 20 | + | case server.To4() != nil && server.IsLoopback(): | |||
| 21 | + | return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} | |||
| 22 | + | case server.To4() != nil: | |||
| 23 | + | return &net.UDPAddr{IP: net.IPv4zero} | |||
| 24 | + | case server.IsLoopback(): | |||
| 25 | + | return &net.UDPAddr{IP: net.IPv6loopback} | |||
| 26 | + | default: | |||
| 27 | + | return &net.UDPAddr{IP: net.IPv6unspecified} | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 16 | // Proxy is a bounded, deterministic UDP relay with per-direction impairment. It | 31 | // Proxy is a bounded, deterministic UDP relay with per-direction impairment. It | ||
| 17 | - | // listens on an ephemeral loopback port for clients and forwards to | 32 | + | // listens on Config.ListenAddr (default: ephemeral IPv4 loopback) for clients |
| 33 | + | // and forwards to | |||
| 18 | // Config.ServerAddr from a separate upstream socket that can be rebound to a | 34 | // Config.ServerAddr from a separate upstream socket that can be rebound to a | ||
| 19 | // new source address at any time. | 35 | // new source address at any time. | ||
| 20 | type Proxy struct { | 36 | type Proxy struct { | ||
| @@ -136,16 +152,7 @@ func New(cfg Config) (*Proxy, error) { | |||||
| 136 | if err != nil { | 152 | if err != nil { | ||
| 137 | return nil, fmt.Errorf("quicnettest: listen client socket: %w", err) | 153 | return nil, fmt.Errorf("quicnettest: listen client socket: %w", err) | ||
| 138 | } | 154 | } | ||
| 139 | - | upstreamAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} | 155 | + | upstreamAddr := upstreamBindAddr(cfg.ServerAddr.IP) |
| 140 | - | if !cfg.ServerAddr.IP.IsLoopback() { | |||
| 141 | - | upstreamAddr.IP = net.IPv4zero | |||
| 142 | - | } | |||
| 143 | - | if cfg.ServerAddr.IP.To4() == nil { | |||
| 144 | - | upstreamAddr.IP = net.IPv6unspecified | |||
| 145 | - | if cfg.ServerAddr.IP.IsLoopback() { | |||
| 146 | - | upstreamAddr.IP = net.IPv6loopback | |||
| 147 | - | } | |||
| 148 | - | } | |||
| 149 | upstream, err := net.ListenUDP("udp", upstreamAddr) | 156 | upstream, err := net.ListenUDP("udp", upstreamAddr) | ||
| 150 | if err != nil { | 157 | if err != nil { | ||
| 151 | _ = clientConn.Close() | 158 | _ = clientConn.Close() | ||