tstest/natlab/vnet: add start of IPv6 support

Updates #13038

Change-Id: Ic3d095f167daf6c7129463e881b18f2e0d5693f5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/11249/merge
Brad Fitzpatrick 1 month ago committed by Brad Fitzpatrick
parent 31b5239a2f
commit b78df4d48a

@ -28,6 +28,7 @@ import (
"sync"
"time"
"tailscale.com/atomicfile"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/util/must"
@ -85,13 +86,25 @@ func main() {
flag.Parse()
if distro.Get() == distro.Gokrazy {
nsRx := regexp.MustCompile(`(?m)^nameserver (.*)`)
for t := time.Now(); time.Since(t) < 10*time.Second; time.Sleep(10 * time.Millisecond) {
all, _ := os.ReadFile("/etc/resolv.conf")
if nsRx.Match(all) {
cmdLine, _ := os.ReadFile("/proc/cmdline")
explicitNS := false
for _, s := range strings.Fields(string(cmdLine)) {
if ns, ok := strings.CutPrefix(s, "tta.nameserver="); ok {
err := atomicfile.WriteFile("/tmp/resolv.conf", []byte("nameserver "+ns+"\n"), 0644)
log.Printf("Wrote /tmp/resolv.conf: %v", err)
explicitNS = true
break
}
}
if !explicitNS {
nsRx := regexp.MustCompile(`(?m)^nameserver (.*)`)
for t := time.Now(); time.Since(t) < 10*time.Second; time.Sleep(10 * time.Millisecond) {
all, _ := os.ReadFile("/etc/resolv.conf")
if nsRx.Match(all) {
break
}
}
}
}
log.Printf("Tailscale Test Agent running.")

@ -2,12 +2,18 @@
echo "Type 'C-a c' to enter monitor; q to quit."
# If the USE_V6 environment is set to 1, set the nameserver explicitly to.
EXTRA_ARG=""
if [ "$USE_V6" = "1" ]; then
EXTRA_ARG="tta.nameserver=2411::411"
fi
set -eux
qemu-system-x86_64 -M microvm,isa-serial=off \
-m 1G \
-nodefaults -no-user-config -nographic \
-kernel $HOME/src/github.com/tailscale/gokrazy-kernel/vmlinuz \
-append "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-76baa2d60001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet tailscale-tta=1 tailscaled.env=TS_DEBUG_RAW_DISCO=1" \
-append "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-76baa2d60001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet tailscale-tta=1 tailscaled.env=TS_DEBUG_RAW_DISCO=1 ${EXTRA_ARG}" \
-drive id=blk0,file=$HOME/src/tailscale.com/gokrazy/natlabapp.img,format=raw \
-device virtio-blk-device,drive=blk0 \
-device virtio-rng-device \

@ -25,10 +25,12 @@ var (
listen = flag.String("listen", "/tmp/qemu.sock", "path to listen on")
nat = flag.String("nat", "easy", "type of NAT to use")
nat2 = flag.String("nat2", "hard", "type of NAT to use for second network")
portmap = flag.Bool("portmap", false, "enable portmapping")
portmap = flag.Bool("portmap", false, "enable portmapping; requires --v4")
dgram = flag.Bool("dgram", false, "enable datagram mode; for use with macOS Hypervisor.Framework and VZFileHandleNetworkDeviceAttachment")
blend = flag.Bool("blend", true, "blend reality (controlplane.tailscale.com and DERPs) into the virtual network")
pcapFile = flag.String("pcap", "", "if non-empty, filename to write pcap")
v4 = flag.Bool("v4", true, "enable IPv4")
v6 = flag.Bool("v6", true, "enable IPv6")
)
func main() {
@ -61,9 +63,18 @@ func main() {
var c vnet.Config
c.SetPCAPFile(*pcapFile)
c.SetBlendReality(*blend)
node1 := c.AddNode(c.AddNetwork("2.1.1.1", "192.168.1.1/24", vnet.NAT(*nat)))
var net1opt = []any{vnet.NAT(*nat)}
if *v4 {
net1opt = append(net1opt, "2.1.1.1", "192.168.1.1/24")
}
if *v6 {
net1opt = append(net1opt, "2000:52::1/64")
}
node1 := c.AddNode(c.AddNetwork(net1opt...))
c.AddNode(c.AddNetwork("2.2.2.2", "10.2.0.1/16", vnet.NAT(*nat2)))
if *portmap {
if *portmap && *v4 {
node1.Network().AddService(vnet.NATPMP)
}
@ -72,8 +83,10 @@ func main() {
log.Fatalf("newServer: %v", err)
}
if err := s.PopulateDERPMapIPs(); err != nil {
log.Printf("warning: ignoring failure to populate DERP map: %v", err)
if *blend {
if err := s.PopulateDERPMapIPs(); err != nil {
log.Printf("warning: ignoring failure to populate DERP map: %v", err)
}
}
s.WriteStartingBanner(os.Stdout)

@ -95,6 +95,10 @@ func findKernelPath(goMod string) (string, error) {
type addNodeFunc func(c *vnet.Config) *vnet.Node // returns nil to omit test
func v6cidr(n int) string {
return fmt.Sprintf("2000:%d::1/64", n)
}
func easy(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(c.AddNetwork(
@ -102,6 +106,20 @@ func easy(c *vnet.Config) *vnet.Node {
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT))
}
func easyAnd6(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(c.AddNetwork(
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
fmt.Sprintf("192.168.%d.1/24", n),
v6cidr(n),
vnet.EasyNAT))
}
func just6(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(c.AddNetwork(v6cidr(n))) // public IPv6 prefix
}
// easy + host firewall
func easyFW(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
@ -255,6 +273,11 @@ func (nt *natTest) runTest(node1, node2 addNodeFunc) pingRoute {
for _, e := range node.Env() {
fmt.Fprintf(&envBuf, " tailscaled.env=%s=%s", e.Key, e.Value)
}
sysLogAddr := net.JoinHostPort(vnet.FakeSyslogIPv4().String(), "995")
if node.IsV6Only() {
fmt.Fprintf(&envBuf, " tta.nameserver=%s", vnet.FakeDNSIPv6())
sysLogAddr = net.JoinHostPort(vnet.FakeSyslogIPv6().String(), "995")
}
envStr := envBuf.String()
cmd := exec.Command("qemu-system-x86_64",
@ -262,7 +285,7 @@ func (nt *natTest) runTest(node1, node2 addNodeFunc) pingRoute {
"-m", "384M",
"-nodefaults", "-no-user-config", "-nographic",
"-kernel", nt.kernel,
"-append", "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-76baa2d60001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet gokrazy.remote_syslog.target=52.52.0.9:995 tailscale-tta=1"+envStr,
"-append", "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-76baa2d60001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet gokrazy.remote_syslog.target="+sysLogAddr+" tailscale-tta=1"+envStr,
"-drive", "id=blk0,file="+disk+",format=qcow2",
"-device", "virtio-blk-device,drive=blk0",
"-netdev", "stream,id=net0,addr.type=unix,addr.path="+sockAddr,
@ -445,6 +468,20 @@ func TestEasyEasy(t *testing.T) {
nt.want(routeDirect)
}
func TestJustIPv6(t *testing.T) {
t.Skip("TODO: get this working")
nt := newNatTest(t)
nt.runTest(just6, just6)
nt.want(routeDirect)
}
func TestEasy4AndJust6(t *testing.T) {
t.Skip("TODO: get this working")
nt := newNatTest(t)
nt.runTest(easyAnd6, just6)
nt.want(routeDirect)
}
func TestSameLAN(t *testing.T) {
nt := newNatTest(t)
nt.runTest(easy, sameLAN)

@ -132,6 +132,7 @@ type TailscaledEnv struct {
func (c *Config) AddNetwork(opts ...any) *Network {
num := len(c.networks)
n := &Network{
num: num + 1,
mac: MAC{0x52, 0xee, 0xee, 0xee, 0xee, byte(num) + 1}, // 52=TS then 0xee for 'etwork
}
c.networks = append(c.networks, n)
@ -139,9 +140,15 @@ func (c *Config) AddNetwork(opts ...any) *Network {
switch o := o.(type) {
case string:
if ip, err := netip.ParseAddr(o); err == nil {
n.wanIP = ip
n.wanIP4 = ip
} else if ip, err := netip.ParsePrefix(o); err == nil {
n.lanIP = ip
// If the prefix is IPv4, treat it as the router's internal IPv4 address + CIDR.
// If the prefix is IPv6, treat it as the router's WAN IPv6 + CIDR (typically a /64).
if ip.Addr().Is4() {
n.lanIP4 = ip
} else if ip.Addr().Is6() {
n.wanIP6 = ip
}
} else {
if n.err == nil {
n.err = fmt.Errorf("unknown string option %q", o)
@ -208,6 +215,21 @@ func (n *Node) SetVerboseSyslog(v bool) {
n.verboseSyslog = v
}
// IsV6Only reports whether this node is only connected to IPv6 networks.
func (n *Node) IsV6Only() bool {
for _, net := range n.nets {
if net.CanV4() {
return false
}
}
for _, net := range n.nets {
if net.CanV6() {
return true
}
}
return false
}
// Network returns the first network this node is connected to,
// or nil if none.
func (n *Node) Network() *Network {
@ -219,12 +241,15 @@ func (n *Node) Network() *Network {
// Network is the configuration of a network in the virtual network.
type Network struct {
num int // 1-based
mac MAC // MAC address of the router/gateway
natType NAT
wanIP netip.Addr
lanIP netip.Prefix
nodes []*Node
wanIP6 netip.Prefix // global unicast router in host bits; CIDR is /64 delegated to LAN
wanIP4 netip.Addr // IPv4 WAN IP, if any
lanIP4 netip.Prefix
nodes []*Node
svcs set.Set[NetworkService]
@ -232,6 +257,14 @@ type Network struct {
err error // carried error
}
func (n *Network) CanV4() bool {
return n.lanIP4.IsValid() || n.wanIP4.IsValid()
}
func (n *Network) CanV6() bool {
return n.wanIP6.IsValid()
}
func (n *Network) CanTakeMoreNodes() bool {
if n.natType == One2OneNAT {
return len(n.nodes) == 0
@ -282,24 +315,43 @@ func (s *Server) initFromConfig(c *Config) error {
if conf.err != nil {
return conf.err
}
if !conf.lanIP.IsValid() {
conf.lanIP = netip.MustParsePrefix("192.168.0.0/24")
if !conf.lanIP4.IsValid() && !conf.wanIP6.IsValid() {
conf.lanIP4 = netip.MustParsePrefix("192.168.0.0/24")
}
n := &network{
s: s,
mac: conf.mac,
portmap: conf.svcs.Contains(NATPMP), // TODO: expand network.portmap
wanIP: conf.wanIP,
lanIP: conf.lanIP,
nodesByIP: map[netip.Addr]*node{},
logf: logger.WithPrefix(log.Printf, fmt.Sprintf("[net-%v] ", conf.mac)),
num: conf.num,
s: s,
mac: conf.mac,
portmap: conf.svcs.Contains(NATPMP), // TODO: expand network.portmap
wanIP6: conf.wanIP6,
v4: conf.lanIP4.IsValid(),
v6: conf.wanIP6.IsValid(),
wanIP4: conf.wanIP4,
lanIP4: conf.lanIP4,
nodesByIP: map[netip.Addr]*node{},
nodesByMAC: map[MAC]*node{},
logf: logger.WithPrefix(log.Printf, fmt.Sprintf("[net-%v] ", conf.mac)),
}
netOfConf[conf] = n
s.networks.Add(n)
if _, ok := s.networkByWAN[conf.wanIP]; ok {
return fmt.Errorf("two networks have the same WAN IP %v; Anycast not (yet?) supported", conf.wanIP)
if conf.wanIP4.IsValid() {
if conf.wanIP4.Is6() {
return fmt.Errorf("invalid IPv6 address in wanIP")
}
if _, ok := s.networkByWAN.Lookup(conf.wanIP4); ok {
return fmt.Errorf("two networks have the same WAN IP %v; Anycast not (yet?) supported", conf.wanIP4)
}
s.networkByWAN.Insert(netip.PrefixFrom(conf.wanIP4, 32), n)
}
if conf.wanIP6.IsValid() {
if conf.wanIP6.Addr().Is4() {
return fmt.Errorf("invalid IPv4 address in wanIP6")
}
if _, ok := s.networkByWAN.LookupPrefix(conf.wanIP6); ok {
return fmt.Errorf("two networks have the same WAN IPv6 %v; Anycast not (yet?) supported", conf.wanIP6)
}
s.networkByWAN.Insert(conf.wanIP6, n)
}
s.networkByWAN[conf.wanIP] = n
n.lanInterfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
Name: fmt.Sprintf("network%d-lan", i+1),
LinkType: layers.LinkTypeIPv4,
@ -330,13 +382,16 @@ func (s *Server) initFromConfig(c *Config) error {
s.nodes = append(s.nodes, n)
s.nodeByMAC[n.mac] = n
// Allocate a lanIP for the node. Use the network's CIDR and use final
// octet 101 (for first node), 102, etc. The node number comes from the
// last octent of the MAC address (0-based)
ip4 := n.net.lanIP.Addr().As4()
ip4[3] = 100 + n.mac[5]
n.lanIP = netip.AddrFrom4(ip4)
n.net.nodesByIP[n.lanIP] = n
if n.net.v4 {
// Allocate a lanIP for the node. Use the network's CIDR and use final
// octet 101 (for first node), 102, etc. The node number comes from the
// last octent of the MAC address (0-based)
ip4 := n.net.lanIP4.Addr().As4()
ip4[3] = 100 + n.mac[5]
n.lanIP = netip.AddrFrom4(ip4)
n.net.nodesByIP[n.lanIP] = n
}
n.net.nodesByMAC[n.mac] = n
}
// Now that nodes are populated, set up NAT:

@ -11,7 +11,7 @@ import (
var vips = map[string]virtualIP{} // DNS name => details
var (
fakeDNS = newVIP("dns", "4.11.4.11", "2000:4:11::4:11")
fakeDNS = newVIP("dns", "4.11.4.11", "2411::411")
fakeProxyControlplane = newVIP("controlplane.tailscale.com", 1)
fakeTestAgent = newVIP("test-driver.tailscale", 2)
fakeControl = newVIP("control.tailscale", 3)
@ -31,6 +31,18 @@ func (v virtualIP) Match(a netip.Addr) bool {
return v.v4 == a.Unmap() || v.v6 == a
}
// FakeDNSIPv4 returns the fake DNS IPv4 address.
func FakeDNSIPv4() netip.Addr { return fakeDNS.v4 }
// FakeDNSIPv6 returns the fake DNS IPv6 address.
func FakeDNSIPv6() netip.Addr { return fakeDNS.v6 }
// FakeSyslogIPv4 returns the fake syslog IPv4 address.
func FakeSyslogIPv4() netip.Addr { return fakeSyslog.v4 }
// FakeSyslogIPv6 returns the fake syslog IPv6 address.
func FakeSyslogIPv6() netip.Addr { return fakeSyslog.v6 }
// newVIP returns a new virtual IP.
//
// opts may be an IPv4 an IPv6 (in string form) or an int (bounded by uint8) to
@ -67,8 +79,14 @@ func newVIP(name string, opts ...any) (v virtualIP) {
}
if !v.v6.IsValid() && v.v4.IsValid() {
// Map 1.2.3.4 to 2052::0102:0304
a := [16]byte{0: 2, 2: 5, 3: 2} // 2052::
copy(a[12:], v.v4.AsSlice())
// But make 52.52.0.x map to 2052::x
a := [16]byte{0: 0x20, 1: 0x52} // 2052::
v4 := v.v4.As4()
if v4[0] == 52 && v4[1] == 52 && v4[2] == 0 {
a[15] = v4[3]
} else {
copy(a[12:], v.v4.AsSlice())
}
v.v6 = netip.AddrFrom16(a)
}
for _, b := range vips {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save