diff --git a/tstest/natlab/vnet/conf.go b/tstest/natlab/vnet/conf.go index 42629c34e..bb7c258f8 100644 --- a/tstest/natlab/vnet/conf.go +++ b/tstest/natlab/vnet/conf.go @@ -65,6 +65,16 @@ func nodeMac(n int) MAC { return MAC{0x52, 0xcc, 0xcc, 0xcc, 0xcc, byte(n)} } +var lanSLAACBase = netip.MustParseAddr("fe80::50cc:ccff:fecc:cc01") + +// nodeLANIP6 returns a node number's Link Local SLAAC IPv6 address, +// such as fe80::50cc:ccff:fecc:cc03 for node 3. +func nodeLANIP6(n int) netip.Addr { + a := lanSLAACBase.As16() + a[15] = byte(n) + return netip.AddrFrom16(a) +} + // AddNode creates a new node in the world. // // The opts may be of the following types: @@ -128,6 +138,7 @@ type TailscaledEnv struct { // The opts may be of the following types: // - string IP address, for the network's WAN IP (if any) // - string netip.Prefix, for the network's LAN IP (defaults to 192.168.0.0/24) +// if IPv4, or its WAN IPv6 + CIDR (e.g. "2000:52::1/64") // - NAT, the type of NAT to use // - NetworkService, a service to add to the network // diff --git a/tstest/natlab/vnet/vnet.go b/tstest/natlab/vnet/vnet.go index dbf855cc0..d88040ab7 100644 --- a/tstest/natlab/vnet/vnet.go +++ b/tstest/natlab/vnet/vnet.go @@ -820,7 +820,20 @@ func (c vmClient) proto() Protocol { return ProtocolUnixDGRAM } -const ethernetHeaderLen = 14 +func parseEthernet(pkt []byte) (dst, src MAC, ethType layers.EthernetType, payload []byte, ok bool) { + // headerLen is the length of an Ethernet header: + // 6 bytes of destination MAC, 6 bytes of source MAC, 2 bytes of EtherType. + const headerLen = 14 + if len(pkt) < headerLen { + return + } + dst = MAC(pkt[0:6]) + src = MAC(pkt[6:12]) + ethType = layers.EthernetType(binary.BigEndian.Uint16(pkt[12:14])) + payload = pkt[headerLen:] + ok = true + return +} // Handles a single connection from a QEMU-style client or muxd connections for dgram mode func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) { @@ -878,10 +891,10 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) { c := vmClient{uc, raddr} // For the first packet from a MAC, register a writerFunc to write to the VM. - if len(packetRaw) < ethernetHeaderLen { + _, srcMAC, _, _, ok := parseEthernet(packetRaw) + if !ok { continue } - srcMAC := MAC(packetRaw[6:12]) srcNode, ok := s.nodeByMAC[srcMAC] if !ok { s.logf("[conn %p] got frame from unknown MAC %v", c.uc, srcMAC) @@ -961,12 +974,12 @@ func (s *Server) routeUDPPacket(up UDPPacket) { // // It reports whether a packet was written to any clients. func (n *network) writeEth(res []byte) bool { - if len(res) < 12 { + dstMAC, srcMAC, etherType, _, ok := parseEthernet(res) + if !ok { return false } - dstMAC := MAC(res[0:6]) - srcMAC := MAC(res[6:12]) - if dstMAC.IsBroadcast() { + + if dstMAC.IsBroadcast() || (n.v6 && etherType == layers.EthernetTypeIPv6 && dstMAC == macAllNodes) { num := 0 n.writers.Range(func(mac MAC, nw networkWriter) bool { if mac != srcMAC { @@ -996,6 +1009,7 @@ func (n *network) writeEth(res []byte) bool { } var ( + macAllNodes = MAC{0: 0x33, 1: 0x33, 5: 0x01} macAllRouters = MAC{0: 0x33, 1: 0x33, 5: 0x02} macBroadcast = MAC{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} ) @@ -1007,7 +1021,7 @@ const ( func (n *network) HandleEthernetPacket(ep EthernetPacket) { packet := ep.gp dstMAC := ep.DstMAC() - isBroadcast := dstMAC.IsBroadcast() + isBroadcast := dstMAC.IsBroadcast() || (n.v6 && ep.le.EthernetType == layers.EthernetTypeIPv6 && dstMAC == macAllNodes) isV6SpecialMAC := dstMAC[0] == 0x33 && dstMAC[1] == 0x33 // forRouter is whether the packet is destined for the router itself @@ -1016,7 +1030,7 @@ func (n *network) HandleEthernetPacket(ep EthernetPacket) { const debug = false if debug { - n.logf("HandleEthernetPacket: %v => %v; type %v, forRouter=%v", ep.SrcMAC(), ep.DstMAC(), ep.le.EthernetType, forRouter) + n.logf("HandleEthernetPacket: %v => %v; type %v, bcast=%v, forRouter=%v", ep.SrcMAC(), ep.DstMAC(), ep.le.EthernetType, isBroadcast, forRouter) } switch ep.le.EthernetType { @@ -1058,7 +1072,7 @@ func (n *network) HandleEthernetPacket(ep EthernetPacket) { // log spam when verbose logging is enabled. return } - if isMcast { + if isMcast && !isBroadcast { return } } diff --git a/tstest/natlab/vnet/vnet_test.go b/tstest/natlab/vnet/vnet_test.go index e816dbdba..4995a603b 100644 --- a/tstest/natlab/vnet/vnet_test.go +++ b/tstest/natlab/vnet/vnet_test.go @@ -69,13 +69,15 @@ func TestPacketSideEffects(t *testing.T) { netName: "v6", setup: func() (*Server, error) { var c Config - c.AddNode(c.AddNetwork("2000:52::1/64")) + nw := c.AddNetwork("2000:52::1/64") + c.AddNode(nw) + c.AddNode(nw) return New(&c) }, tests: []netTest{ { name: "router-solicit", - pkt: mkIPv6RouterSolicit(nodeMac(1), netip.MustParseAddr("fe80::50cc:ccff:fecc:cc01")), + pkt: mkIPv6RouterSolicit(nodeMac(1), nodeLANIP6(1)), check: all( logSubstr("sending IPv6 router advertisement to 52:cc:cc:cc:cc:01 from 52:ee:ee:ee:ee:01"), numPkts(1), @@ -84,6 +86,16 @@ func TestPacketSideEffects(t *testing.T) { pktSubstr("SrcMAC=52:ee:ee:ee:ee:01 DstMAC=52:cc:cc:cc:cc:01 EthernetType=IPv6"), ), }, + { + name: "all-nodes", + pkt: mkAllNodesPing(nodeMac(1), nodeLANIP6(1)), + check: all( + numPkts(1), + pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=33:33:00:00:00:01"), + pktSubstr("SrcIP=fe80::50cc:ccff:fecc:cc01 DstIP=ff02::1"), + pktSubstr("TypeCode=EchoRequest"), + ), + }, }, }, } @@ -105,7 +117,9 @@ func TestPacketSideEffects(t *testing.T) { }) } - s.handleEthernetFrameFromVM(tt.pkt) + if err := s.handleEthernetFrameFromVM(tt.pkt); err != nil { + t.Fatal(err) + } if tt.check != nil { if err := tt.check(se); err != nil { t.Fatal(err) @@ -156,13 +170,31 @@ func mkIPv6RouterSolicit(srcMAC MAC, srcIP netip.Addr) []byte { }}, } icmp.SetNetworkLayerForChecksum(ip) + return mkEth(macAllRouters, srcMAC, layers.EthernetTypeIPv6, mkPacket(ip, icmp, ra)) +} + +func mkPacket(layers ...gopacket.SerializableLayer) []byte { buf := gopacket.NewSerializeBuffer() - options := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true} - if err := gopacket.SerializeLayers(buf, options, ip, icmp, ra); err != nil { - panic(fmt.Sprintf("serializing ICMPv6 RA: %v", err)) + opts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true} + if err := gopacket.SerializeLayers(buf, opts, layers...); err != nil { + panic(fmt.Sprintf("serializing packet: %v", err)) } + return buf.Bytes() +} - return mkEth(macAllRouters, srcMAC, layers.EthernetTypeIPv6, buf.Bytes()) +func mkAllNodesPing(srcMAC MAC, srcIP netip.Addr) []byte { + ip := &layers.IPv6{ + Version: 6, + HopLimit: 255, + NextHeader: layers.IPProtocolICMPv6, + SrcIP: srcIP.AsSlice(), + DstIP: net.ParseIP("ff02::1"), // all nodes + } + icmp := &layers.ICMPv6{ + TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeEchoRequest, 0), + } + icmp.SetNetworkLayerForChecksum(ip) + return mkEth(macAllNodes, srcMAC, layers.EthernetTypeIPv6, mkPacket(ip, icmp)) } // sideEffects gathers side effects as a result of sending a packet and tests @@ -198,7 +230,7 @@ func logSubstr(sub string) func(*sideEffects) error { return nil } } - return fmt.Errorf("expected log substring %q not found; log statements were %q", sub, se.logs) + return fmt.Errorf("expected log substring %q not found; log statements were:\n%s", sub, strings.Join(se.logs, "\n")) } }