tstest/natlab: make a new virtualIP type in prep for IPv6 support

All the magic service names with virtual IPs will need IPv6 variants.

Pull this out in prep.

Updates #13038

Change-Id: I53b5eebd0679f9fa43dc0674805049258c83a0de
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2024-08-23 12:49:51 -07:00 committed by Brad Fitzpatrick
parent 5a99940dfa
commit aa42ae9058
2 changed files with 120 additions and 59 deletions

81
tstest/natlab/vnet/vip.go Normal file
View File

@ -0,0 +1,81 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package vnet
import (
"fmt"
"net/netip"
)
var vips = map[string]virtualIP{} // DNS name => details
var (
fakeDNS = newVIP("dns", "4.11.4.11", "2000:4:11::4:11")
fakeProxyControlplane = newVIP("controlplane.tailscale.com", 1)
fakeTestAgent = newVIP("test-driver.tailscale", 2)
fakeControl = newVIP("control.tailscale", 3)
fakeDERP1 = newVIP("derp1.tailscale", "33.4.0.1") // 3340=DERP; 1=derp 1
fakeDERP2 = newVIP("derp2.tailscale", "33.4.0.2") // 3340=DERP; 2=derp 2
fakeLogCatcher = newVIP("log.tailscale.io", 4)
fakeSyslog = newVIP("syslog.tailscale", 9)
)
type virtualIP struct {
name string // for DNS
v4 netip.Addr
v6 netip.Addr
}
func (v virtualIP) Match(a netip.Addr) bool {
return v.v4 == a.Unmap() || v.v6 == a
}
// newVIP returns a new virtual IP.
//
// opts may be an IPv4 an IPv6 (in string form) or an int (bounded by uint8) to
// use IPv4 of 52.52.0.x.
//
// If the IPv6 is omitted, one is derived from the IPv4.
//
// If an opt is invalid or the DNS name is already used, it panics.
func newVIP(name string, opts ...any) (v virtualIP) {
if _, ok := vips[name]; ok {
panic(fmt.Sprintf("duplicate VIP %q", name))
}
v.name = name
for _, o := range opts {
switch o := o.(type) {
case string:
if ip, err := netip.ParseAddr(o); err == nil {
if ip.Is4() {
v.v4 = ip
} else if ip.Is6() {
v.v6 = ip
}
} else {
panic(fmt.Sprintf("unsupported string option %q", o))
}
case int:
if o <= 0 || o > 255 {
panic(fmt.Sprintf("bad octet %d", o))
}
v.v4 = netip.AddrFrom4([4]byte{52, 52, 0, byte(o)})
default:
panic(fmt.Sprintf("unknown option type %T", o))
}
}
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())
v.v6 = netip.AddrFrom16(a)
}
for _, b := range vips {
if b.Match(v.v4) || b.Match(v.v6) {
panic(fmt.Sprintf("VIP %q collides with %q", name, v.name))
}
}
vips[name] = v
return v
}

View File

@ -281,7 +281,7 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
return return
} }
if destPort == 8008 && destIP == fakeTestAgentIP { if destPort == 8008 && fakeTestAgent.Match(destIP) {
r.Complete(false) r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep) tc := gonet.NewTCPConn(&wq, ep)
node := n.nodesByIP[clientRemoteIP] node := n.nodesByIP[clientRemoteIP]
@ -290,7 +290,7 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
return return
} }
if destPort == 80 && destIP == fakeControlIP { if destPort == 80 && fakeControl.Match(destIP) {
r.Complete(false) r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep) tc := gonet.NewTCPConn(&wq, ep)
hs := &http.Server{Handler: n.s.control} hs := &http.Server{Handler: n.s.control}
@ -298,28 +298,29 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
return return
} }
if destPort == 443 && (destIP == fakeDERP1IP || destIP == fakeDERP2IP) { if fakeDERP1.Match(destIP) || fakeDERP2.Match(destIP) {
ds := n.s.derps[0] if destPort == 443 {
if destIP == fakeDERP2IP { ds := n.s.derps[0]
ds = n.s.derps[1] if fakeDERP2.Match(destIP) {
ds = n.s.derps[1]
}
r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep)
tlsConn := tls.Server(tc, ds.tlsConfig)
hs := &http.Server{Handler: ds.handler}
go hs.Serve(netutil.NewOneConnListener(tlsConn, nil))
return
}
if destPort == 80 {
r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep)
hs := &http.Server{Handler: n.s.derps[0].handler}
go hs.Serve(netutil.NewOneConnListener(tc, nil))
return
} }
r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep)
tlsConn := tls.Server(tc, ds.tlsConfig)
hs := &http.Server{Handler: ds.handler}
go hs.Serve(netutil.NewOneConnListener(tlsConn, nil))
return
} }
if destPort == 80 && (destIP == fakeDERP1IP || destIP == fakeDERP2IP) { if destPort == 443 && fakeLogCatcher.Match(destIP) {
r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep)
hs := &http.Server{Handler: n.s.derps[0].handler}
go hs.Serve(netutil.NewOneConnListener(tc, nil))
return
}
if destPort == 443 && destIP == fakeLogCatcherIP {
r.Complete(false) r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep) tc := gonet.NewTCPConn(&wq, ep)
go n.serveLogCatcherConn(clientRemoteIP, tc) go n.serveLogCatcherConn(clientRemoteIP, tc)
@ -331,7 +332,7 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
var targetDial string var targetDial string
if n.s.derpIPs.Contains(destIP) { if n.s.derpIPs.Contains(destIP) {
targetDial = destIP.String() + ":" + strconv.Itoa(int(destPort)) targetDial = destIP.String() + ":" + strconv.Itoa(int(destPort))
} else if destIP == fakeProxyControlplaneIP { } else if fakeProxyControlplane.Match(destIP) {
targetDial = "controlplane.tailscale.com:" + strconv.Itoa(int(destPort)) targetDial = "controlplane.tailscale.com:" + strconv.Itoa(int(destPort))
} }
if targetDial != "" { if targetDial != "" {
@ -399,17 +400,6 @@ func (n *network) serveLogCatcherConn(clientRemoteIP netip.Addr, c net.Conn) {
hs.Serve(netutil.NewOneConnListener(tlsConn, nil)) hs.Serve(netutil.NewOneConnListener(tlsConn, nil))
} }
var (
fakeDNSIP = netip.AddrFrom4([4]byte{4, 11, 4, 11})
fakeProxyControlplaneIP = netip.AddrFrom4([4]byte{52, 52, 0, 1}) // real controlplane.tailscale.com proxy
fakeTestAgentIP = netip.AddrFrom4([4]byte{52, 52, 0, 2})
fakeControlIP = netip.AddrFrom4([4]byte{52, 52, 0, 3}) // 3=C for "Control"
fakeDERP1IP = netip.AddrFrom4([4]byte{33, 4, 0, 1}) // 3340=DERP; 1=derp 1
fakeDERP2IP = netip.AddrFrom4([4]byte{33, 4, 0, 2}) // 3340=DERP; 1=derp 1
fakeLogCatcherIP = netip.AddrFrom4([4]byte{52, 52, 0, 4})
fakeSyslogIP = netip.AddrFrom4([4]byte{52, 52, 0, 9})
)
type EthernetPacket struct { type EthernetPacket struct {
le *layers.Ethernet le *layers.Ethernet
gp gopacket.Packet gp gopacket.Packet
@ -594,7 +584,8 @@ type Server struct {
Name: "1a", Name: "1a",
RegionID: 1, RegionID: 1,
HostName: "derp1.tailscale", HostName: "derp1.tailscale",
IPv4: fakeDERP1IP.String(), IPv4: fakeDERP1.v4.String(),
IPv6: fakeDERP1.v6.String(),
InsecureForTests: true, InsecureForTests: true,
CanPort80: true, CanPort80: true,
}, },
@ -609,7 +600,8 @@ type Server struct {
Name: "2a", Name: "2a",
RegionID: 2, RegionID: 2,
HostName: "derp2.tailscale", HostName: "derp2.tailscale",
IPv4: fakeDERP2IP.String(), IPv4: fakeDERP2.v4.String(),
IPv6: fakeDERP2.v6.String(),
InsecureForTests: true, InsecureForTests: true,
CanPort80: true, CanPort80: true,
}, },
@ -666,21 +658,8 @@ func (s *Server) HWAddr(mac MAC) net.HardwareAddr {
// IPv4ForDNS returns the IP address for the given DNS query name (for IPv4 A // IPv4ForDNS returns the IP address for the given DNS query name (for IPv4 A
// queries only). // queries only).
func (s *Server) IPv4ForDNS(qname string) (netip.Addr, bool) { func (s *Server) IPv4ForDNS(qname string) (netip.Addr, bool) {
switch qname { if v, ok := vips[qname]; ok {
case "dns": return v.v4, v.v4.IsValid()
return fakeDNSIP, true
case "log.tailscale.io":
return fakeLogCatcherIP, true
case "test-driver.tailscale":
return fakeTestAgentIP, true
case "controlplane.tailscale.com":
return fakeProxyControlplaneIP, true
case "control.tailscale":
return fakeControlIP, true
case "derp1.tailscale":
return fakeDERP1IP, true
case "derp2.tailscale":
return fakeDERP2IP, true
} }
return netip.Addr{}, false return netip.Addr{}, false
} }
@ -1049,7 +1028,7 @@ func (n *network) HandleEthernetIPv4PacketForRouter(ep EthernetPacket) {
return return
} }
if isUDP && dstIP == fakeSyslogIP { if isUDP && fakeSyslog.Match(dstIP) {
node, ok := n.nodesByIP[srcIP] node, ok := n.nodesByIP[srcIP]
if !ok { if !ok {
return return
@ -1196,7 +1175,7 @@ func (s *Server) createDHCPResponse(request gopacket.Packet) ([]byte, error) {
}, },
layers.DHCPOption{ layers.DHCPOption{
Type: layers.DHCPOptDNS, Type: layers.DHCPOptDNS,
Data: fakeDNSIP.AsSlice(), Data: fakeDNS.v4.AsSlice(),
Length: 4, Length: 4,
}, },
layers.DHCPOption{ layers.DHCPOption{
@ -1276,18 +1255,19 @@ func (s *Server) shouldInterceptTCP(pkt gopacket.Packet) bool {
} }
dstIP, _ := netip.AddrFromSlice(ipv4.DstIP.To4()) dstIP, _ := netip.AddrFromSlice(ipv4.DstIP.To4())
if tcp.DstPort == 80 || tcp.DstPort == 443 { if tcp.DstPort == 80 || tcp.DstPort == 443 {
switch dstIP { for _, v := range []virtualIP{fakeControl, fakeDERP1, fakeDERP2, fakeLogCatcher} {
case fakeControlIP, fakeDERP1IP, fakeDERP2IP, fakeLogCatcherIP: if v.Match(dstIP) {
return true return true
}
} }
if dstIP == fakeProxyControlplaneIP { if fakeProxyControlplane.Match(dstIP) {
return s.blendReality return s.blendReality
} }
if s.derpIPs.Contains(dstIP) { if s.derpIPs.Contains(dstIP) {
return true return true
} }
} }
if tcp.DstPort == 8008 && dstIP == fakeTestAgentIP { if tcp.DstPort == 8008 && fakeTestAgent.Match(dstIP) {
// Connection from cmd/tta. // Connection from cmd/tta.
return true return true
} }
@ -1305,7 +1285,7 @@ func isDNSRequest(pkt gopacket.Packet) bool {
return false return false
} }
dstIP, ok := netip.AddrFromSlice(ip.DstIP) dstIP, ok := netip.AddrFromSlice(ip.DstIP)
if !ok || dstIP != fakeDNSIP { if !ok || !fakeDNS.Match(dstIP) {
return false return false
} }
dns, ok := pkt.Layer(layers.LayerTypeDNS).(*layers.DNS) dns, ok := pkt.Layer(layers.LayerTypeDNS).(*layers.DNS)