From 73b3c8fc8c085a508ec3130c6d616c8d2a545593 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 28 Aug 2024 08:06:17 -0700 Subject: [PATCH] tstest/natlab/vnet: add IPv6 all-nodes support This adds support for sending packets to 33:33:00:00:01 at IPv6 multicast address ff02::1 to send to all nodes. Nothing in Tailscale depends on this (yet?), but it makes debugging in VMs behind natlab easier (e.g. you can ping all nodes), and other things might depend on this in the future. Mostly I'm trying to flesh out the IPv6 support in natlab now that we can write vnet tests. Updates #13038 Change-Id: If590031fcf075690ca35c7b230a38c3e72e621eb Signed-off-by: Brad Fitzpatrick --- tstest/natlab/vnet/conf.go | 11 +++++++ tstest/natlab/vnet/vnet.go | 34 ++++++++++++++------- tstest/natlab/vnet/vnet_test.go | 52 ++++++++++++++++++++++++++------- 3 files changed, 77 insertions(+), 20 deletions(-) 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) - 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)) - } + return mkEth(macAllRouters, srcMAC, layers.EthernetTypeIPv6, mkPacket(ip, icmp, ra)) +} - return mkEth(macAllRouters, srcMAC, layers.EthernetTypeIPv6, buf.Bytes()) +func mkPacket(layers ...gopacket.SerializableLayer) []byte { + buf := gopacket.NewSerializeBuffer() + 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() +} + +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")) } }