You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
tailscale/feature/conn25/datapath.go

242 lines
10 KiB

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package conn25
import (
"errors"
"net/netip"
"tailscale.com/envknob"
"tailscale.com/net/flowtrack"
"tailscale.com/net/packet"
"tailscale.com/net/packet/checksum"
"tailscale.com/types/ipproto"
"tailscale.com/types/logger"
"tailscale.com/wgengine/filter"
)
var (
ErrUnmappedMagicIP = errors.New("unmapped magic IP")
ErrUnmappedSrcAndTransitIP = errors.New("unmapped src and transit IP")
)
// IPMapper provides methods for mapping special app connector IPs to each other
// in aid of performing DNAT and SNAT on app connector packets.
type IPMapper interface {
// ClientTransitIPForMagicIP returns a Transit IP for the given magicIP on a client.
// If the magicIP is within a configured Magic IP range for an app on the client,
// but not mapped to an active Transit IP, implementations should return [ErrUnmappedMagicIP].
// If magicIP is not within a configured Magic IP range, i.e. it is not actually a Magic IP,
// implementations should return a nil error, and a zero-value [netip.Addr] to indicate
// this potentially valid, non-app-connector traffic.
ClientTransitIPForMagicIP(magicIP netip.Addr) (netip.Addr, error)
// ConnectorRealIPForTransitIPConnection returns a real destination IP for the given
// srcIP and transitIP on a connector. If the transitIP is within a configured Transit IP
// range for an app on the connector, but not mapped to the client at srcIP, implementations
// should return [ErrUnmappedSrcAndTransitIP]. If the transitIP is not within a configured
// Transit IP range, i.e. it is not actually a Transit IP, implementations should return
// a nil error, a zero-value [netip.Addr] to indicate this is potentially valid, non-app-connector
// traffic.
ConnectorRealIPForTransitIPConnection(srcIP netip.Addr, transitIP netip.Addr) (netip.Addr, error)
}
// datapathHandler handles packets from the datapath,
// performing appropriate NAT operations to support Connectors 2025.
// It maintains [FlowTable] caches for fast lookups of established flows.
//
// When hooked into the main datapath filter chain in [tstun], the datapathHandler
// will see every packet on the node, regardless of whether it is relevant to
// app connector operations. In the common case of non-connector traffic, it
// passes the packet through unmodified.
//
// It classifies each packet based on the presence of special Magic IPs or
// Transit IPs, and determines whether the packet is flowing through a "client"
// (the node with the application that starts the connection), or a "connector"
// (the node that connects to the internet-hosted destination). On the client,
// outbound connections are DNATed from Magic IP to Transit IP, and return
// traffic is SNATed from Transit IP to Magic IP. On the connector, outbound
// connections are DNATed from Transit IP to real IP, and return traffic is
// SNATed from real IP to Transit IP.
//
// There are two exposed methods, one for handling packets from the tun device,
// and one for handling packets from WireGuard, but through the use of flow tables,
// we can handle four cases: client outbound, client return, connector outbound,
// connector return. The first packet goes through IPMapper, which is where Connectors
// 2025 authoritative state is stored. For valid packets relevant to connectors,
// a bidirectional flow entry is installed, so that subsequent packets (and all return traffic)
// hit that cache. Only outbound (towards internet) packets create new flows; return (from internet)
// packets either match a cached entry or pass through.
//
// We check the cache before IPMapper both for performance, and so that existing flows stay alive
// even if address mappings change mid-flow.
type datapathHandler struct {
ipMapper IPMapper
// Flow caches. One for the client, and one for the connector.
clientFlowTable *FlowTable
connectorFlowTable *FlowTable
logf logger.Logf
debugLogging bool
}
func newDatapathHandler(ipMapper IPMapper, logf logger.Logf) *datapathHandler {
return &datapathHandler{
ipMapper: ipMapper,
// TODO(mzb): Figure out sensible default max size for flow tables.
// Don't do any LRU eviction until we figure out deletion and expiration.
clientFlowTable: NewFlowTable(0),
connectorFlowTable: NewFlowTable(0),
logf: logf,
debugLogging: envknob.Bool("TS_CONN25_DATAPATH_DEBUG"),
}
}
// HandlePacketFromWireGuard inspects packets coming from WireGuard, and performs
// appropriate DNAT or SNAT actions for Connectors 2025. Returning [filter.Accept] signals
// that the packet should pass through subsequent stages of the datapath pipeline.
// Returning [filter.Drop] signals the packet should be dropped. This method handles all
// packets coming from WireGuard, on both connectors, and clients of connectors.
func (dh *datapathHandler) HandlePacketFromWireGuard(p *packet.Parsed) filter.Response {
// TODO(tailscale/corp#38764): Support other protocols, like ICMP for error messages.
if p.IPProto != ipproto.TCP && p.IPProto != ipproto.UDP {
return filter.Accept
}
// Check if this is an existing (return) flow on a client.
// If found, perform the action for the existing client flow and return.
existing, ok := dh.clientFlowTable.LookupFromWireGuard(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
return filter.Accept
}
// Check if this is an existing connector outbound flow.
// If found, perform the action for the existing connector outbound flow and return.
existing, ok = dh.connectorFlowTable.LookupFromWireGuard(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
return filter.Accept
}
// The flow was not found in either flow table. Since the packet came in
// from WireGuard, it can only be a new flow on the connector,
// other (non-app-connector) traffic, or broken app-connector traffic
// that needs to be re-established by a new outbound packet.
transitIP := p.Dst.Addr()
realIP, err := dh.ipMapper.ConnectorRealIPForTransitIPConnection(p.Src.Addr(), transitIP)
if err != nil {
if errors.Is(err, ErrUnmappedSrcAndTransitIP) {
// TODO(tailscale/corp#34256): This path should deliver an ICMP error to the client.
return filter.Drop
}
dh.debugLogf("error mapping src and transit IP, passing packet unmodified: %v", err)
return filter.Accept
}
// If this is normal non-app-connector traffic, forward it along unmodified.
if !realIP.IsValid() {
return filter.Accept
}
// This is a new outbound flow on a connector. Install a DNAT TransitIP-to-RealIP action
// for the outgoing direction, and an SNAT RealIP-to-TransitIP action for the
// return direction.
outgoing := FlowData{
Tuple: flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst),
Action: dh.dnatAction(realIP),
}
incoming := FlowData{
Tuple: flowtrack.MakeTuple(p.IPProto, netip.AddrPortFrom(realIP, p.Dst.Port()), p.Src),
Action: dh.snatAction(transitIP),
}
if err := dh.connectorFlowTable.NewFlowFromWireGuard(outgoing, incoming); err != nil {
dh.debugLogf("error installing flow, passing packet unmodified: %v", err)
return filter.Accept
}
outgoing.Action(p)
return filter.Accept
}
// HandlePacketFromTunDevice inspects packets coming from the tun device, and performs
// appropriate DNAT or SNAT actions for Connectors 2025. Returning [filter.Accept] signals
// that the packet should pass through subsequent stages of the datapath pipeline.
// Returning [filter.Drop] signals the packet should be dropped. This method handles all
// packets coming from the tun device, on both connectors, and clients of connectors.
func (dh *datapathHandler) HandlePacketFromTunDevice(p *packet.Parsed) filter.Response {
// TODO(tailscale/corp#38764): Support other protocols, like ICMP for error messages.
if p.IPProto != ipproto.TCP && p.IPProto != ipproto.UDP {
return filter.Accept
}
// Check if this is an existing client outbound flow.
// If found, perform the action for the existing client flow and return.
existing, ok := dh.clientFlowTable.LookupFromTunDevice(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
return filter.Accept
}
// Check if this is an existing connector return flow.
// If found, perform the action for the existing connector return flow and return.
existing, ok = dh.connectorFlowTable.LookupFromTunDevice(flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst))
if ok {
existing.Action(p)
return filter.Accept
}
// The flow was not found in either flow table. Since the packet came in on the
// tun device, it can only be a new client flow, other (non-app-connector) traffic,
// or broken return app-connector traffic on a connector, which needs to be re-established
// with a new outbound packet.
magicIP := p.Dst.Addr()
transitIP, err := dh.ipMapper.ClientTransitIPForMagicIP(magicIP)
if err != nil {
if errors.Is(err, ErrUnmappedMagicIP) {
// TODO(tailscale/corp#34257): This path should deliver an ICMP error to the client.
return filter.Drop
}
dh.debugLogf("error mapping magic IP, passing packet unmodified: %v", err)
return filter.Accept
}
// If this is normal non-app-connector traffic, forward it along unmodified.
if !transitIP.IsValid() {
return filter.Accept
}
// This is a new outbound client flow. Install a DNAT MagicIP-to-TransitIP action
// for the outgoing direction, and an SNAT TransitIP-to-MagicIP action for the
// return direction.
outgoing := FlowData{
Tuple: flowtrack.MakeTuple(p.IPProto, p.Src, p.Dst),
Action: dh.dnatAction(transitIP),
}
incoming := FlowData{
Tuple: flowtrack.MakeTuple(p.IPProto, netip.AddrPortFrom(transitIP, p.Dst.Port()), p.Src),
Action: dh.snatAction(magicIP),
}
if err := dh.clientFlowTable.NewFlowFromTunDevice(outgoing, incoming); err != nil {
dh.debugLogf("error installing flow from tun device, passing packet unmodified: %v", err)
return filter.Accept
}
outgoing.Action(p)
return filter.Accept
}
func (dh *datapathHandler) dnatAction(to netip.Addr) PacketAction {
return PacketAction(func(p *packet.Parsed) { checksum.UpdateDstAddr(p, to) })
}
func (dh *datapathHandler) snatAction(to netip.Addr) PacketAction {
return PacketAction(func(p *packet.Parsed) { checksum.UpdateSrcAddr(p, to) })
}
func (dh *datapathHandler) debugLogf(msg string, args ...any) {
if dh.debugLogging {
dh.logf(msg, args...)
}
}