WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -5,22 +5,46 @@ package vmtest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstest/natlab/vmtest"
|
||||
"tailscale.com/tstest/natlab/vnet"
|
||||
)
|
||||
|
||||
// hardDualNoEndpoints is hard NAT with both an IPv4 LAN and an IPv6 prefix
|
||||
// (so the DebugDERPRegion probe exercises both address families against the
|
||||
// test DERP server) and TS_DEBUG_STRIP_ENDPOINTS=1 set on tailscaled so it
|
||||
// doesn't announce any direct endpoints to peers. Combined on both nodes,
|
||||
// that leaves DERP as the only available path for the tailnet ping. The
|
||||
// home DERP itself is left alone so the sha256-raw verification path is
|
||||
// still exercised.
|
||||
func hardDualNoEndpoints(env *vmtest.Env) *vmtest.Node {
|
||||
n := env.NumNodes()
|
||||
return env.AddNode(fmt.Sprintf("node-%d", n),
|
||||
env.AddNetwork(
|
||||
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
|
||||
fmt.Sprintf("10.0.%d.1/24", n),
|
||||
v6cidr(n),
|
||||
vnet.HardNAT),
|
||||
vnet.TailscaledEnv{Key: "TS_DEBUG_STRIP_ENDPOINTS", Value: "1"},
|
||||
vmtest.OS(vmtest.Gokrazy))
|
||||
}
|
||||
|
||||
// TestSelfSignedDERPHashPinning exercises the sha256-raw DERP cert pinning
|
||||
// code path end-to-end: tailscaled connects to its home DERP whose cert is
|
||||
// self-signed and pinned via CertName="sha256-raw:<hex>" (no separate
|
||||
// fronting CertName), the two nodes communicate over the resulting tailnet,
|
||||
// and `tailscale debug derp` against the same region succeeds.
|
||||
//
|
||||
// Both nodes sit behind hard NATs with no port mapping available so disco
|
||||
// cannot punch a direct path and the tailnet ping must traverse DERP, making
|
||||
// the sha256-raw pinning of the tailscaled→DERP path part of the assertion.
|
||||
// Both nodes sit behind hard NATs and additionally strip their direct
|
||||
// endpoints (TS_DEBUG_STRIP_ENDPOINTS=1) so disco cannot find a direct path
|
||||
// and the tailnet ping must traverse DERP, making the sha256-raw pinning of
|
||||
// the tailscaled→DERP path part of the assertion. (Stripping endpoints — not
|
||||
// just relying on hard NAT — is needed because the dual-stack LAN provides a
|
||||
// non-NATted IPv6 path that the nodes would otherwise discover.)
|
||||
//
|
||||
// The debug-derp half is the regression test for the bug fixed in PR #19965:
|
||||
// before that change, [ipn/localapi.serveDebugDERPRegion] passed the raw
|
||||
@@ -28,8 +52,8 @@ import (
|
||||
// failed with a hostname mismatch.
|
||||
func TestSelfSignedDERPHashPinning(t *testing.T) {
|
||||
env := vmtest.New(t, vmtest.SelfSignedDERPCertPinning())
|
||||
n1 := hard(env)
|
||||
n2 := hard(env)
|
||||
n1 := hardDualNoEndpoints(env)
|
||||
n2 := hardDualNoEndpoints(env)
|
||||
env.Start()
|
||||
|
||||
if err := env.PingExpect(n1, n2, vmtest.PingRouteDERP, 60*time.Second); err != nil {
|
||||
@@ -46,15 +70,9 @@ func TestSelfSignedDERPHashPinning(t *testing.T) {
|
||||
}
|
||||
t.Logf("[%s] DebugDERPRegion(1): info=%v warnings=%v errors=%v",
|
||||
n.Name(), rep.Info, rep.Warnings, rep.Errors)
|
||||
for _, e := range rep.Errors {
|
||||
// The `hard` builder gives the node only an IPv4 LAN, so the
|
||||
// DebugDERPRegion IPv6 probe predictably fails with
|
||||
// "network is unreachable". That's orthogonal to the TLS
|
||||
// verification path this test exists to cover.
|
||||
if strings.Contains(e, "over IPv6") {
|
||||
continue
|
||||
}
|
||||
t.Errorf("[%s] DebugDERPRegion(1) error: %s", n.Name(), e)
|
||||
if len(rep.Errors) > 0 {
|
||||
t.Errorf("[%s] DebugDERPRegion(1) reported errors: %s",
|
||||
n.Name(), strings.Join(rep.Errors, "; "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,8 @@ func (n *network) initStack() error {
|
||||
Destination: ipv6Subnet,
|
||||
NIC: nicID,
|
||||
})
|
||||
|
||||
n.startUnsolicitedRAs()
|
||||
}
|
||||
|
||||
n.ns.SetRouteTable(routes)
|
||||
@@ -627,6 +629,17 @@ func (n *network) registerWriter(mac MAC, c vmClient) {
|
||||
nw.interfaceID = node.interfaceID
|
||||
}
|
||||
n.writers.Store(mac, nw)
|
||||
|
||||
// As soon as a host appears on the wire, hand it a Router Advertisement
|
||||
// so its kernel installs the prefix + default route. Without this, hosts
|
||||
// that never emit a Router Solicitation (e.g. gokrazy with DHCPv4 doing
|
||||
// link bringup) would have to wait for the next periodic RA, by which
|
||||
// point the test may have already failed.
|
||||
if n.v6 {
|
||||
if pkt, err := n.buildIPv6RouterAdvertisement(mac, ipv6AllNodes); err == nil {
|
||||
n.writeEth(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *network) unregisterWriter(mac MAC) {
|
||||
@@ -1926,21 +1939,36 @@ func (n *network) handleUDPPacketForRouter(ep EthernetPacket, udp *layers.UDP, t
|
||||
n.logf("router got unknown UDP packet: %v", packet)
|
||||
}
|
||||
|
||||
func (n *network) handleIPv6RouterSolicitation(ep EthernetPacket, rs *layers.ICMPv6RouterSolicitation) {
|
||||
v6 := ep.gp.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
|
||||
// ipv6AllNodes is the IPv6 link-local "all nodes" multicast address (ff02::1).
|
||||
// Unsolicited Router Advertisements are sent here so that every connected
|
||||
// host on the LAN sees them without the router having to know each host's
|
||||
// unicast address.
|
||||
var ipv6AllNodes = net.ParseIP("ff02::1")
|
||||
|
||||
// Send a router advertisement back.
|
||||
// unsolicitedRAInterval is how often vnet sends an unsolicited IPv6 Router
|
||||
// Advertisement on each v6-enabled network. Real routers default to 200s
|
||||
// (RFC 4861 §6.2.1, MaxRtrAdvInterval). We pick a much smaller value so
|
||||
// short-lived tests don't have to wait: the first RA goes out as soon as
|
||||
// a VM connects, and any subsequent gokrazy/Linux init paths that miss the
|
||||
// initial RA pick one up quickly.
|
||||
const unsolicitedRAInterval = 5 * time.Second
|
||||
|
||||
// buildIPv6RouterAdvertisement serializes a Router Advertisement frame
|
||||
// addressed to (dstMAC, dstIP), advertising n.wanIP6's /64 as on-link and
|
||||
// fe80::1 as a default router. dstMAC/dstIP are typically the soliciting
|
||||
// host (for a solicited reply) or the link-local all-nodes group (for an
|
||||
// unsolicited periodic RA).
|
||||
func (n *network) buildIPv6RouterAdvertisement(dstMAC MAC, dstIP net.IP) ([]byte, error) {
|
||||
eth := &layers.Ethernet{
|
||||
SrcMAC: n.mac.HWAddr(),
|
||||
DstMAC: ep.SrcMAC().HWAddr(),
|
||||
DstMAC: dstMAC.HWAddr(),
|
||||
EthernetType: layers.EthernetTypeIPv6,
|
||||
}
|
||||
n.logf("sending IPv6 router advertisement to %v from %v", eth.DstMAC, eth.SrcMAC)
|
||||
ip := &layers.IPv6{
|
||||
NextHeader: layers.IPProtocolICMPv6,
|
||||
HopLimit: 255, // per RFC 4861, 7.1.1 etc (all NDP messages); don't use mkPacket's default of 64
|
||||
SrcIP: net.ParseIP("fe80::1"),
|
||||
DstIP: v6.SrcIP,
|
||||
DstIP: dstIP,
|
||||
}
|
||||
icmp := &layers.ICMPv6{
|
||||
TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeRouterAdvertisement, 0),
|
||||
@@ -1963,7 +1991,13 @@ func (n *network) handleIPv6RouterSolicitation(ep EthernetPacket, rs *layers.ICM
|
||||
},
|
||||
},
|
||||
}
|
||||
pkt, err := mkPacket(eth, ip, icmp, ra)
|
||||
return mkPacket(eth, ip, icmp, ra)
|
||||
}
|
||||
|
||||
func (n *network) handleIPv6RouterSolicitation(ep EthernetPacket, _ *layers.ICMPv6RouterSolicitation) {
|
||||
v6 := ep.gp.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
|
||||
n.logf("sending IPv6 router advertisement to %v from %v", ep.SrcMAC(), n.mac)
|
||||
pkt, err := n.buildIPv6RouterAdvertisement(ep.SrcMAC(), v6.SrcIP)
|
||||
if err != nil {
|
||||
n.logf("serializing ICMPv6 RA: %v", err)
|
||||
return
|
||||
@@ -1971,6 +2005,37 @@ func (n *network) handleIPv6RouterSolicitation(ep EthernetPacket, rs *layers.ICM
|
||||
n.writeEth(pkt)
|
||||
}
|
||||
|
||||
// startUnsolicitedRAs sends an unsolicited Router Advertisement to the
|
||||
// link-local all-nodes group every unsolicitedRAInterval until the vnet
|
||||
// server shuts down. This ensures hosts on the LAN install vnet's default
|
||||
// IPv6 route even if their stack never emits a Router Solicitation, which
|
||||
// is what gokrazy's dual-stack init does in practice: it brings the link
|
||||
// up via DHCPv4 and then leaves IPv6 to the kernel, which under our
|
||||
// configuration never sends an RS.
|
||||
func (n *network) startUnsolicitedRAs() {
|
||||
n.s.wg.Go(func() {
|
||||
send := func() {
|
||||
pkt, err := n.buildIPv6RouterAdvertisement(macAllNodes, ipv6AllNodes)
|
||||
if err != nil {
|
||||
n.logf("building unsolicited RA: %v", err)
|
||||
return
|
||||
}
|
||||
n.writeEth(pkt)
|
||||
}
|
||||
send()
|
||||
t := time.NewTicker(unsolicitedRAInterval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-n.s.shutdownCtx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
send()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (n *network) handleIPv6NeighborSolicitation(ep EthernetPacket, ns *layers.ICMPv6NeighborSolicitation) {
|
||||
v6 := ep.gp.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
|
||||
|
||||
|
||||
@@ -637,15 +637,22 @@ func sendBetweenClients(t testing.TB, clientc [2]*net.UnixConn, s *Server, wrap
|
||||
t.Logf("writing % 02x", pkt)
|
||||
must.Get(clientc[0].Write(pkt))
|
||||
|
||||
buf := make([]byte, len(pkt))
|
||||
clientc[1].SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
n, err := clientc[1].Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := buf[:n]
|
||||
if !bytes.Equal(got, pkt) {
|
||||
t.Errorf("bad packet\n got: % 02x\nwant: % 02x", got, pkt)
|
||||
// vnet sends an unsolicited Router Advertisement at writer-register time
|
||||
// on v6-enabled networks; loop until we see the test packet, skipping any
|
||||
// noise that arrived first.
|
||||
buf := make([]byte, 2048)
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
clientc[1].SetReadDeadline(deadline)
|
||||
n, err := clientc[1].Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("did not receive test packet: %v", err)
|
||||
}
|
||||
got := buf[:n]
|
||||
if bytes.Equal(got, pkt) {
|
||||
return
|
||||
}
|
||||
t.Logf("ignoring unexpected packet (% 02x...)", got[:min(len(got), 16)])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user