WIP: rebase for 2026-05-18 #7
@@ -436,6 +436,14 @@ func (n *Node) LanIP(net *vnet.Network) netip.Addr {
|
|||||||
return n.vnetNode.LanIP(net)
|
return n.vnetNode.LanIP(net)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DropControlTraffic sets up a blackhole for control traffic for just this
|
||||||
|
// node on all the networks belonging to the node.
|
||||||
|
func (n *Node) DropControlTraffic() {
|
||||||
|
for _, network := range n.nets {
|
||||||
|
network.BlackholeControlForAddr(n.LanIP(network))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NodeOption types for configuring nodes.
|
// NodeOption types for configuring nodes.
|
||||||
|
|
||||||
type nodeOptOS OSImage
|
type nodeOptOS OSImage
|
||||||
@@ -1669,3 +1677,68 @@ func findKernelPath(goMod string) (string, error) {
|
|||||||
}
|
}
|
||||||
return "", fmt.Errorf("gokrazy-kernel not found in %s", goMod)
|
return "", fmt.Errorf("gokrazy-kernel not found in %s", goMod)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PingRoute describes what connection type was used to transfer a Disco ping.
|
||||||
|
type PingRoute string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PingRouteDirect PingRoute = "direct"
|
||||||
|
PingRouteDERP PingRoute = "derp"
|
||||||
|
PingRouteLocal PingRoute = "local"
|
||||||
|
PingRouteNil PingRoute = "nil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// classifyPing finds what kind of route has been used on a ping path.
|
||||||
|
// It is only really relevant for DiscoPings.
|
||||||
|
func classifyPing(pr *ipnstate.PingResult) PingRoute {
|
||||||
|
if pr == nil {
|
||||||
|
return PingRouteNil
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr.Endpoint == "" {
|
||||||
|
return PingRouteDERP
|
||||||
|
}
|
||||||
|
|
||||||
|
ap, err := netip.ParseAddrPort(pr.Endpoint)
|
||||||
|
if err == nil && ap.Addr().IsPrivate() {
|
||||||
|
return PingRouteLocal
|
||||||
|
}
|
||||||
|
return PingRouteDirect
|
||||||
|
}
|
||||||
|
|
||||||
|
// PingExpect retries disco pings until the result matches wantRoute or the
|
||||||
|
// timeout is reached. It is using DiscoPings as this is the only ping type
|
||||||
|
// that can classify the connection type.
|
||||||
|
func (e *Env) PingExpect(from, to *Node, wantRoute PingRoute, timeout time.Duration) error {
|
||||||
|
e.t.Helper()
|
||||||
|
ctx, cancel := context.WithTimeout(e.t.Context(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
var lastRoute PingRoute
|
||||||
|
toSt, err := to.agent.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ping: can't get %s status: %w", to.name, err)
|
||||||
|
}
|
||||||
|
if len(toSt.Self.TailscaleIPs) == 0 {
|
||||||
|
return fmt.Errorf("ping: %s has no Tailscale IPs", to.name)
|
||||||
|
}
|
||||||
|
targetIP := toSt.Self.TailscaleIPs[0]
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
pingCtx, pingCancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
pr, err := from.agent.PingWithOpts(pingCtx, targetIP, tailcfg.PingDisco, local.PingOpts{})
|
||||||
|
pingCancel()
|
||||||
|
if err == nil && pr.Err == "" {
|
||||||
|
if got := classifyPing(pr); got == wantRoute {
|
||||||
|
e.t.Logf("Saw ping type %q", got)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
e.t.Logf("Saw ping type %q", got)
|
||||||
|
lastRoute = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ping route = %q, want %q (after %v)", lastRoute, wantRoute, timeout)
|
||||||
|
}
|
||||||
|
|||||||
@@ -919,9 +919,6 @@ func TestCachedNetmapAfterRestart(t *testing.T) {
|
|||||||
aNet := env.AddNetwork("1.0.0.1", "192.168.1.1/24", vnet.EasyNAT)
|
aNet := env.AddNetwork("1.0.0.1", "192.168.1.1/24", vnet.EasyNAT)
|
||||||
bNet := env.AddNetwork("2.0.0.1", "192.168.2.1/24", vnet.EasyNAT)
|
bNet := env.AddNetwork("2.0.0.1", "192.168.2.1/24", vnet.EasyNAT)
|
||||||
|
|
||||||
aNet.SetPostConnectControlBlackhole(true)
|
|
||||||
bNet.SetPostConnectControlBlackhole(true)
|
|
||||||
|
|
||||||
a := env.AddNode("a", aNet,
|
a := env.AddNode("a", aNet,
|
||||||
vmtest.OS(vmtest.Gokrazy),
|
vmtest.OS(vmtest.Gokrazy),
|
||||||
tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil})
|
tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil})
|
||||||
@@ -945,8 +942,9 @@ func TestCachedNetmapAfterRestart(t *testing.T) {
|
|||||||
connectStep.End(nil)
|
connectStep.End(nil)
|
||||||
|
|
||||||
cutControlStep.Begin()
|
cutControlStep.Begin()
|
||||||
aNet.PostConnectedToControl()
|
// Both nodes lose connection to control
|
||||||
bNet.PostConnectedToControl()
|
a.DropControlTraffic()
|
||||||
|
b.DropControlTraffic()
|
||||||
env.ControlServer().SetOnMapRequest(func(nk key.NodePublic) {
|
env.ControlServer().SetOnMapRequest(func(nk key.NodePublic) {
|
||||||
panic(fmt.Sprintf("got connection from %v", nk))
|
panic(fmt.Sprintf("got connection from %v", nk))
|
||||||
})
|
})
|
||||||
@@ -978,3 +976,56 @@ func TestCachedNetmapAfterRestart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
pingStep.End(nil)
|
pingStep.End(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDirectConnectionWithCachedNetmap verifies that two nodes with netmap
|
||||||
|
// caching enabled (NodeAttrCacheNetworkMaps) can re-establish a direct
|
||||||
|
// WireGuard tunnel after one is restarted while the control server is
|
||||||
|
// unreachable. After restart the node must use only its on-disk cached
|
||||||
|
// netmaps to re-connect and ping the other (still online) node.
|
||||||
|
func TestDirectConnectionWithCachedNetmapOnOneNode(t *testing.T) {
|
||||||
|
env := vmtest.New(t)
|
||||||
|
|
||||||
|
aNet := env.AddNetwork("1.0.0.1", "192.168.1.1/24", vnet.EasyNAT)
|
||||||
|
bNet := env.AddNetwork("2.0.0.1", "192.168.2.1/24", vnet.EasyNAT)
|
||||||
|
|
||||||
|
a := env.AddNode("a", aNet,
|
||||||
|
vmtest.OS(vmtest.Gokrazy),
|
||||||
|
tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil})
|
||||||
|
b := env.AddNode("b", bNet,
|
||||||
|
vmtest.OS(vmtest.Gokrazy),
|
||||||
|
tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil})
|
||||||
|
|
||||||
|
cutControlStep := env.AddStep("Cut control server access")
|
||||||
|
restartStep := env.AddStep("Restart tailscaled on a")
|
||||||
|
tsmpPingStep := env.AddStep("Ping a → b TSMP (cached netmap, no control)")
|
||||||
|
DiscoPingStep := env.AddStep("Ping a → b Disco (want Direct)")
|
||||||
|
|
||||||
|
env.Start()
|
||||||
|
|
||||||
|
cutControlStep.Begin()
|
||||||
|
a.DropControlTraffic()
|
||||||
|
env.ControlServer().SetOnMapRequest(func(nk key.NodePublic) {
|
||||||
|
if env.ControlServer().Node(nk).Name == a.Name() {
|
||||||
|
panic(fmt.Sprintf("got connection from %v", a.Name()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cutControlStep.End(nil)
|
||||||
|
|
||||||
|
restartStep.Begin()
|
||||||
|
env.RestartTailscaled(a)
|
||||||
|
restartStep.End(nil)
|
||||||
|
|
||||||
|
tsmpPingStep.Begin()
|
||||||
|
if err := env.Ping(a, b, tailcfg.PingTSMP, 30*time.Second); err != nil {
|
||||||
|
tsmpPingStep.End(err)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tsmpPingStep.End(nil)
|
||||||
|
|
||||||
|
DiscoPingStep.Begin()
|
||||||
|
if err := env.PingExpect(a, b, vmtest.PingRouteDirect, 30*time.Second); err != nil {
|
||||||
|
DiscoPingStep.End(err)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
DiscoPingStep.End(nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -445,6 +445,12 @@ func (n *Network) PostConnectedToControl() {
|
|||||||
n.network.SetControlBlackholed(n.postConnectBlackholeControl)
|
n.network.SetControlBlackholed(n.postConnectBlackholeControl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BlackholeControlForAddr sets weither the network should drop all control
|
||||||
|
// traffic for the specified addr starting immediately.
|
||||||
|
func (n *Network) BlackholeControlForAddr(addr netip.Addr) {
|
||||||
|
n.network.BlackholeControlForAddr(addr)
|
||||||
|
}
|
||||||
|
|
||||||
// NetworkService is a service that can be added to a network.
|
// NetworkService is a service that can be added to a network.
|
||||||
type NetworkService string
|
type NetworkService string
|
||||||
|
|
||||||
|
|||||||
@@ -606,6 +606,9 @@ type network struct {
|
|||||||
// writers is a map of MAC -> networkWriters to write packets to that MAC.
|
// writers is a map of MAC -> networkWriters to write packets to that MAC.
|
||||||
// It contains entries for connected nodes only.
|
// It contains entries for connected nodes only.
|
||||||
writers syncs.Map[MAC, networkWriter] // MAC -> to networkWriter for that MAC
|
writers syncs.Map[MAC, networkWriter] // MAC -> to networkWriter for that MAC
|
||||||
|
|
||||||
|
blackholeMu sync.Mutex
|
||||||
|
blackholeMap map[netip.Addr]netip.Addr // blackholeMap contains address pairs for dropping traffic (in either direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerWriter registers a client address with a MAC address.
|
// registerWriter registers a client address with a MAC address.
|
||||||
@@ -653,6 +656,19 @@ func (n *network) SetControlBlackholed(v bool) {
|
|||||||
n.blackholeControl = v
|
n.blackholeControl = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BlackholeControlForAddr sets up a map entry, ensuring that traffic to or from
|
||||||
|
// control from the addr is dropped.
|
||||||
|
func (n *network) BlackholeControlForAddr(addr netip.Addr) {
|
||||||
|
n.blackholeMu.Lock()
|
||||||
|
defer n.blackholeMu.Unlock()
|
||||||
|
|
||||||
|
if addr.Is6() {
|
||||||
|
mak.Set(&n.blackholeMap, addr, fakeControl.v6)
|
||||||
|
} else {
|
||||||
|
mak.Set(&n.blackholeMap, addr, fakeControl.v4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// nodeNIC represents a single network interface on a node.
|
// nodeNIC represents a single network interface on a node.
|
||||||
// For multi-homed nodes, additional NICs beyond the primary are stored in node.extraNICs.
|
// For multi-homed nodes, additional NICs beyond the primary are stored in node.extraNICs.
|
||||||
type nodeNIC struct {
|
type nodeNIC struct {
|
||||||
@@ -1621,6 +1637,17 @@ func (n *network) HandleEthernetPacketForRouter(ep EthernetPacket) {
|
|||||||
// Blackhole the packet.
|
// Blackhole the packet.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop traffic to/from address pairs in the blackholeMap.
|
||||||
|
n.blackholeMu.Lock()
|
||||||
|
defer n.blackholeMu.Unlock()
|
||||||
|
if src, ok := n.blackholeMap[flow.dst]; ok && flow.src == src {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dst, ok := n.blackholeMap[flow.src]; ok && flow.dst == dst {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var base *layers.BaseLayer
|
var base *layers.BaseLayer
|
||||||
proto := header.IPv4ProtocolNumber
|
proto := header.IPv4ProtocolNumber
|
||||||
if v4, ok := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4); ok {
|
if v4, ok := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4); ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user