diff --git a/appc/conn25.go b/appc/conn25.go index b4890c26c..6da77cacf 100644 --- a/appc/conn25.go +++ b/appc/conn25.go @@ -7,7 +7,10 @@ import ( "net/netip" "sync" + "tailscale.com/net/packet" + "tailscale.com/net/packet/checksum" "tailscale.com/tailcfg" + "tailscale.com/wgengine/filter" ) // Conn25 holds the developing state for the as yet nascent next generation app connector. @@ -108,3 +111,127 @@ type ConnectorTransitIPResponse struct { // correspond to the order of [ConnectorTransitIPRequest.TransitIPs]. TransitIPs []TransitIPResponse `json:"transitIPs,omitempty"` } + +// DatapathHandler provides methods to intercept, mangle, and filter packets +// in the datapath for app connector purposes. +type DatapathHandler interface { + // HandleLocalTraffic intercepts traffic from the local network stack, e.g. the tun device, and + // determines if the traffic is app connector traffic that should be forwarded to a connector, + // or is return traffic that should be forwarded back to the originating client. Valid packets may + // be altered, e.g. NAT, and invalid packets may be dropped. + HandleLocalTraffic(*packet.Parsed) filter.Response + + // HandleTunnelTraffic intercepts traffic from the wireguard tunnel and determines if the traffic + // is app connector traffic that should be forwarded to an application destination or back to the + // local network stack. Valid packets may be altered, e.g. NAT, and invalid packets may be dropped. + HandleTunnelTraffic(*packet.Parsed) filter.Response +} + +// datapathHandler is the main implementation of DatapathHandler. +type datapathHandler struct { + // conn25 *Conn25 perhaps + // flowTable Flowtable perhaps +} + +func NewDatpathHandler() DatapathHandler { + return &datapathHandler{} +} + +func (dh *datapathHandler) HandleLocalTraffic(p *packet.Parsed) filter.Response { + // Connector-bound traffic. + if dh.dstIPIsMagicIP(p) { + return dh.processClientToConnector(p) + } + + // Return traffic from external application. + if dh.selfIsConnector() && dh.isConnectorReturnTraffic(p) { + return dh.processConnectorToClient(p) + } + // if controller client with flow in flow table, find address for source nat. If not, forward along. + + return filter.Accept +} + +func (dh *datapathHandler) HandleTunnelTraffic(p *packet.Parsed) filter.Response { + // Return traffic from connector, source is a Transit IP. + if dh.srcIsTransitIP(p) { + return dh.processClientFromConnector(p) + } + + // Outgoing traffic for an external application. Destination is Transit IP. + if dh.selfIsConnector() && dh.dstIPIsTransitIP(p) { + return dh.processConnectorFromClient(p) + } + return filter.Accept +} + +// processClientToConnector consults the flow table to determine which connector to send the packet to, +// and if this is a new flow, runs the connector selection algorithm, and installs a new flow. +// If the packet is valid, we DNAT from the Magic IP to the Transit IP. +// If there is no flow or the packet is otherwise invalid, we drop the packet. +func (dh *datapathHandler) processClientToConnector(p *packet.Parsed) filter.Response { + // TODO: implement + // TODO: we could do magic IP validation here as well + + // This is just an example of how to do the NAT, when we need it. + transitIP := netip.AddrFrom4([4]byte{169, 254, 100, 1}) + checksum.UpdateDstAddr(p, transitIP) + + return filter.Drop +} + +// processConnectorToClient consults the flow table on a connector to determine which client +// to send the return traffic to. +// If the packet is valid, we SNAT the external application IP to the Transit IP. +// If there is no flow or the packet is otherwise invalid, we drop the packet. +func (dh *datapathHandler) processConnectorToClient(p *packet.Parsed) filter.Response { + // TODO: implement + return filter.Drop +} + +// processClientFromConnector consults the flow table to validate that the packet should +// be forwarded back to the local network stack. +// We SNAT the Transit IP back to the Magic IP. +// If there is no flow or the packet is otherwise invalid, we drop the packet. +func (dh *datapathHandler) processClientFromConnector(p *packet.Parsed) filter.Response { + // TODO: implement + return filter.Drop +} + +// processConnectorFromClient consults the flow table to see if this packet is part of +// an existing outbound flow to an application, or a new flow should be installed. +// If the packet is valid, we DNAT from the Transit IP to the external application IP. +// If there is no flow or the packet is otherwise invalid, we drop the packet. +func (dh *datapathHandler) processConnectorFromClient(p *packet.Parsed) filter.Response { + // TODO: implement + return filter.Drop +} + +// dstIPIsMagicIP returns whether the destination IP address in p is Magic IP, +// which could indicate interesting traffic for outbound traffic from a client to a connector. +func (dh *datapathHandler) dstIPIsMagicIP(p *packet.Parsed) bool { + // TODO: implement + // TODO: we could do magic IP validation here as well + return false +} + +func (dh *datapathHandler) srcIsTransitIP(p *packet.Parsed) bool { + // TODO: implement + return false +} + +func (dh *datapathHandler) dstIPIsTransitIP(p *packet.Parsed) bool { + // TODO: implement + return false +} + +// selfIsConnector returns whether this client is running on an app connector. +func (dh *datapathHandler) selfIsConnector() bool { + // TODO: implement + return false +} + +func (dh *datapathHandler) isConnectorReturnTraffic(p *packet.Parsed) bool { + // TODO: implement + return false +} diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 647923775..de9552907 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -22,6 +22,7 @@ import ( "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" + "tailscale.com/appc" "tailscale.com/control/controlknobs" "tailscale.com/drive" "tailscale.com/envknob" @@ -164,6 +165,10 @@ type userspaceEngine struct { // networkLogger logs statistics about network connections. networkLogger netlog.Logger + // appcDatapathHander intercepts, and possibly mangles and filters packets + // for app connector operation. + appcDatapathHandler appc.DatapathHandler + // Lock ordering: magicsock.Conn.mu, wgLock, then mu. } @@ -430,6 +435,8 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) if conf.RespondToPing { e.tundev.PostFilterPacketInboundFromWireGuard = echoRespondToAll + } else { + e.tundev.PostFilterPacketInboundFromWireGuard = e.handleTunnelPackets } e.tundev.PreFilterPacketOutboundToWireGuardEngineIntercept = e.handleLocalPackets @@ -623,9 +630,22 @@ func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) } } + if e.appcDatapathHandler != nil { + return e.appcDatapathHandler.HandleLocalTraffic(p) + } + return filter.Accept } +// handleTunnelPackets inspects packets coming from the wireguard tunnel stack and have passed through +// the main filter. It intercepts and filters packets before delivering tun device wrapper.. +func (e *userspaceEngine) handleTunnelPackets(p *packet.Parsed, t *tstun.Wrapper, gro *gro.GRO) (filter.Response, *gro.GRO) { + if e.appcDatapathHandler != nil { + return e.appcDatapathHandler.HandleTunnelTraffic(p), gro + } + return filter.Accept, gro +} + var debugTrimWireguard = envknob.RegisterOptBool("TS_DEBUG_TRIM_WIREGUARD") // forceFullWireguardConfig reports whether we should give wireguard our full