Skip to content

Commit f013ce9

Browse files
committed
test: improve test coverage for IPv6 routing and packets
1 parent d1ee9eb commit f013ce9

6 files changed

Lines changed: 244 additions & 5 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ jobs:
129129
./librespeed-cli --local-json config_librespeed.json --server 2 --json --share --telemetry-level disabled | python3 -m json.tool
130130
131131
ping 10.66.0.2 -w 20 -c 10
132+
ping6 fd00:66:0::2 -w 20 -c 10 || true
132133
# TODO: remove this temporal hack for linux
133134
ping awl-tester.awl -w 20 -c 10 || true
134135

application_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,52 @@ func TestUpdatePeerSettingsIPAddr(t *testing.T) {
794794
})
795795
ts.NoError(err)
796796
})
797+
798+
t.Run("IPv6TunnelPackets", func(t *testing.T) {
799+
const packetSize = 1500
800+
const packetsCount = 10
801+
802+
peer2Config, err := peer1.api.KnownPeerConfig(peer2.PeerID())
803+
ts.NoError(err)
804+
805+
peer2IPv4 := net.ParseIP(peer2Config.IPAddr).To4()
806+
ts.NotNil(peer2IPv4)
807+
808+
awlSubnet4, err := netip.ParsePrefix(peer1.app.Conf.VPNConfig.IPNet)
809+
ts.NoError(err)
810+
awlSubnet6, err := netip.ParsePrefix(peer1.app.Conf.VPNConfig.IPNetV6)
811+
ts.NoError(err)
812+
813+
// Map IPv4 to IPv6 using the same logic as peerIPv6FromIPv4
814+
peer2IPv6 := make(net.IP, net.IPv6len)
815+
copy(peer2IPv6, awlSubnet6.Addr().AsSlice())
816+
v4Mask := net.CIDRMask(awlSubnet4.Bits(), 32)
817+
for i := 0; i < net.IPv4len; i++ {
818+
peer2IPv6[12+i] |= peer2IPv4[i] &^ v4Mask[i]
819+
}
820+
821+
// Configure tunnel for packet testing
822+
peer1.tun.SetInboundCapture(packetSize, nil)
823+
peer2.tun.SetInboundCapture(packetSize, nil)
824+
peer1.tun.ClearInboundCount()
825+
peer2.tun.ClearInboundCount()
826+
827+
// Wait for IP map to be ready
828+
time.Sleep(100 * time.Millisecond)
829+
830+
// Send IPv6 packets from peer1 to peer2
831+
packet := testPacketWithDestV6(packetSize, peer2IPv6.String())
832+
for i := 0; i < packetsCount; i++ {
833+
peer1.tun.Outbound <- [][]byte{packet}
834+
}
835+
836+
// Wait for packet processing
837+
time.Sleep(500 * time.Millisecond)
838+
839+
// Verify packet reception
840+
received := peer2.tun.InboundCount()
841+
ts.EqualValues(packetsCount, received)
842+
})
797843
}
798844

799845
func TestDisableVPNInterface(t *testing.T) {

service/tunnel_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package service
2+
3+
import (
4+
"net"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestPeerIPv6FromIPv4(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
peerIPv4 string
14+
awlSubnet4 string
15+
awlSubnet6 string
16+
expected string
17+
}{
18+
{
19+
name: "valid conversion /16 and /112",
20+
peerIPv4: "10.66.0.5",
21+
awlSubnet4: "10.66.0.0/16",
22+
awlSubnet6: "fd00:66::/112",
23+
expected: "fd00:66::5",
24+
},
25+
{
26+
name: "valid conversion /16 and /48",
27+
peerIPv4: "10.66.0.5",
28+
awlSubnet4: "10.66.0.0/16",
29+
awlSubnet6: "fd00:66:0::/48",
30+
expected: "fd00:66:0::5",
31+
},
32+
{
33+
name: "valid conversion with larger IPv4 offset",
34+
peerIPv4: "10.66.255.5",
35+
awlSubnet4: "10.66.0.0/16",
36+
awlSubnet6: "fd00:66:0::/48",
37+
expected: "fd00:66:0::ff05",
38+
},
39+
{
40+
name: "out of bounds IPv4",
41+
peerIPv4: "10.67.0.5",
42+
awlSubnet4: "10.66.0.0/16",
43+
awlSubnet6: "fd00:66:0::/48",
44+
expected: "", // expected nil
45+
},
46+
{
47+
name: "capacity mismatch v4 host bits > v6 host bits",
48+
peerIPv4: "10.66.0.5",
49+
awlSubnet4: "10.66.0.0/16", // 16 host bits
50+
awlSubnet6: "fd00:66::/120", // 8 host bits
51+
expected: "", // expected nil
52+
},
53+
{
54+
name: "invalid mask lengths (v4)",
55+
peerIPv4: "10.66.0.5",
56+
awlSubnet4: "10.66.0.0/16",
57+
awlSubnet6: "fd00:66::/112",
58+
expected: "",
59+
},
60+
}
61+
62+
for _, tc := range tests {
63+
t.Run(tc.name, func(t *testing.T) {
64+
peerIP := net.ParseIP(tc.peerIPv4)
65+
var sub4, sub6 *net.IPNet
66+
67+
if tc.awlSubnet4 != "" {
68+
_, sub4, _ = net.ParseCIDR(tc.awlSubnet4)
69+
if tc.name == "invalid mask lengths (v4)" {
70+
sub4.Mask = net.CIDRMask(16, 128)
71+
}
72+
}
73+
if tc.awlSubnet6 != "" {
74+
_, sub6, _ = net.ParseCIDR(tc.awlSubnet6)
75+
}
76+
77+
result := peerIPv6FromIPv4(peerIP, sub4, sub6)
78+
79+
if tc.expected == "" {
80+
assert.Nil(t, result)
81+
} else {
82+
assert.NotNil(t, result)
83+
assert.Equal(t, net.ParseIP(tc.expected).To16(), result)
84+
}
85+
})
86+
}
87+
88+
t.Run("nil inputs", func(t *testing.T) {
89+
assert.Nil(t, peerIPv6FromIPv4(nil, nil, nil))
90+
91+
_, sub4, _ := net.ParseCIDR("10.66.0.0/16")
92+
_, sub6, _ := net.ParseCIDR("fd00:66:0::/48")
93+
assert.Nil(t, peerIPv6FromIPv4(net.ParseIP("10.66.0.5"), nil, sub6))
94+
assert.Nil(t, peerIPv6FromIPv4(net.ParseIP("10.66.0.5"), sub4, nil))
95+
})
96+
}

test_suite_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,50 @@ func testPacketWithDest(length int, destIP string) []byte {
593593
return testPacketWithSrcDest(length, "10.66.0.1", destIP)
594594
}
595595

596+
func testPacketWithSrcDestV6(length int, srcIP, destIP string) []byte {
597+
data, err := hex.DecodeString("6000000000141140fd000000000000000000000000000001fd00000000000000000000000000000204d2162e0014000068656c6c6f20776f726c6421")
598+
if err != nil {
599+
panic(err)
600+
}
601+
602+
packet := data
603+
if length > len(data) {
604+
packet = make([]byte, length)
605+
copy(packet, data)
606+
_, err = rand.Read(packet[len(data):])
607+
if err != nil {
608+
panic(err)
609+
}
610+
}
611+
612+
vpnPacket := vpn.Packet{}
613+
_, err = vpnPacket.ReadFrom(bytes.NewReader(packet))
614+
if err != nil {
615+
panic(err)
616+
}
617+
vpnPacket.Parse()
618+
619+
srcIPParsed := net.ParseIP(srcIP).To16()
620+
if srcIPParsed == nil {
621+
panic(fmt.Sprintf("invalid source IPv6: %s", srcIP))
622+
}
623+
copy(vpnPacket.Src, srcIPParsed)
624+
625+
destIPParsed := net.ParseIP(destIP).To16()
626+
if destIPParsed == nil {
627+
panic(fmt.Sprintf("invalid destination IPv6: %s", destIP))
628+
}
629+
copy(vpnPacket.Dst, destIPParsed)
630+
631+
vpnPacket.RecalculateChecksum()
632+
633+
return vpnPacket.Packet
634+
}
635+
636+
func testPacketWithDestV6(length int, destIP string) []byte {
637+
return testPacketWithSrcDestV6(length, "fd00:66:0::1", destIP)
638+
}
639+
596640
// parsePacketIPs extracts src and dst IPs from a raw IPv4 packet.
597641
func parsePacketIPs(rawPacket []byte) (src, dst net.IP) {
598642
pkt := vpn.Packet{}

vpn/netstate/vpn_hostnet_windows_integration_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ func TestGatewayHostNetNATLifecycle(t *testing.T) {
281281
require.False(t, awlNATInstalled(t), "pre-existing awl-gateway NetNat; clean the host before running")
282282

283283
mgr := NewManager()
284-
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, nicGUID))
284+
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, "", nicGUID))
285285
require.True(t, mgr.ServerNATActive())
286286

287287
require.True(t, awlNATInstalled(t), "NetNat must exist while NAT is up")
@@ -317,7 +317,7 @@ func TestGatewayHostNetNATPreservesExistingForwarding(t *testing.T) {
317317
}
318318

319319
mgr := NewManager()
320-
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, nicGUID))
320+
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, "", nicGUID))
321321
require.True(t, forwardingEnabled(t, nicLUID))
322322

323323
require.NoError(t, mgr.DisableServerNAT())
@@ -338,7 +338,7 @@ func TestGatewayHostNetNATStaleRecovery(t *testing.T) {
338338
require.True(t, awlNATInstalled(t))
339339

340340
mgr := NewManager()
341-
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, nicGUID),
341+
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, "", nicGUID),
342342
"EnableServerNAT must recover from a stale awl-gateway NetNat")
343343
require.True(t, awlNATInstalled(t))
344344

@@ -370,7 +370,7 @@ func TestGatewayHostNetNATRollback(t *testing.T) {
370370
})
371371

372372
mgr := NewManager()
373-
err = mgr.EnableServerNAT(testAwlSubnet, nicGUID)
373+
err = mgr.EnableServerNAT(testAwlSubnet, "", nicGUID)
374374
if err == nil {
375375
// If even overlapping-prefix instances are tolerated, there is no
376376
// failure to roll back from. Clean up and skip rather than fail.
@@ -603,7 +603,7 @@ func TestGatewayHostNetClientFenceAllowsClientServerCoexist(t *testing.T) {
603603
_, nicGUID := pickServerTestNIC(t)
604604

605605
require.NoError(t, mgr.EnableClientRoutes(tunGUID))
606-
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, nicGUID))
606+
require.NoError(t, mgr.EnableServerNAT(testAwlSubnet, "", nicGUID))
607607
t.Cleanup(func() { _ = mgr.DisableServerNAT() })
608608

609609
require.Equal(t, 8, clientFenceRuleCount(t), "client fence rules present with both roles on")

vpn/packet_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ func TestPacket_RecalculateChecksum(t *testing.T) {
2121
a.Equal(rawData, packet.Packet)
2222
}
2323

24+
func TestPacket_RecalculateChecksum_IPv6(t *testing.T) {
25+
a := require.New(t)
26+
packet, rawData := testUDPPacketIPv6()
27+
// testUDPPacketIPv6 has 0000 for checksum, calling RecalculateChecksum will update it
28+
packet.RecalculateChecksum()
29+
// Just verify it doesn't crash and actually modifies the checksum if it was 0000
30+
a.NotEqual(rawData, packet.Packet)
31+
32+
// Test idempotency
33+
firstRecalculate := append([]byte(nil), packet.Packet...)
34+
packet.RecalculateChecksum()
35+
a.Equal(firstRecalculate, packet.Packet)
36+
}
37+
2438
// TODO: bench with bigger packet
2539
func BenchmarkPacket_RecalculateChecksum(b *testing.B) {
2640
packet, _ := testUDPPacket()
@@ -76,6 +90,31 @@ func TestPacket_Parse_RejectsMalformedIPv4(t *testing.T) {
7690
}
7791
}
7892

93+
func TestPacket_Parse_RejectsMalformedIPv6(t *testing.T) {
94+
newRawIPv6 := func(size int, nextHeader byte) []byte {
95+
raw := make([]byte, size)
96+
raw[0] = 0x60 // version 6
97+
if len(raw) > 6 {
98+
raw[6] = nextHeader
99+
}
100+
return raw
101+
}
102+
103+
cases := []struct {
104+
name string
105+
raw []byte
106+
}{
107+
{"length less than ipv6 header", newRawIPv6(20, 17)}, // ipv6 header is 40
108+
}
109+
for _, tc := range cases {
110+
t.Run(tc.name, func(t *testing.T) {
111+
p := new(Packet)
112+
_, _ = p.ReadFrom(bytes.NewReader(tc.raw))
113+
require.False(t, p.Parse(), "malformed IPv6 packet must be rejected by Parse")
114+
})
115+
}
116+
}
117+
79118
// defense in depth: RecalculateChecksum must be panic-safe on malformed or
80119
// truncated packets, including when called without a prior successful Parse.
81120
func TestPacket_RecalculateChecksum_MalformedNoPanic(t *testing.T) {
@@ -186,6 +225,19 @@ func testUDPPacket() (*Packet, []byte) {
186225
return packet, data
187226
}
188227

228+
func testUDPPacketIPv6() (*Packet, []byte) {
229+
data, err := hex.DecodeString("6000000000141140fd000000000000000000000000000001fd00000000000000000000000000000204d2162e0014000068656c6c6f20776f726c6421")
230+
if err != nil {
231+
panic(err)
232+
}
233+
234+
packet := new(Packet)
235+
_, _ = packet.ReadFrom(bytes.NewReader(data))
236+
packet.Parse()
237+
238+
return packet, data
239+
}
240+
189241
func TestGetIPv4BroadcastAddress(t *testing.T) {
190242
tests := []struct {
191243
name string

0 commit comments

Comments
 (0)