From 45c4d4b4b01e1a0c540fa7a347b159a4e58a61cd Mon Sep 17 00:00:00 2001 From: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:58:13 -0500 Subject: [PATCH] test Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> --- ipn/ipnlocal/local.go | 28 ++++++++- net/tstun/wrap.go | 96 ++++++++++++++++++++++++++++- wgengine/netstack/link_endpoint.go | 65 ++++++++++++++++++++ wgengine/netstack/netstack.go | 99 +++++++++++++++++++++++++++++- wgengine/userspace.go | 8 ++- 5 files changed, 291 insertions(+), 5 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index ef89af5af..0cb29db8a 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -913,6 +913,12 @@ func (b *LocalBackend) setStateLocked(state ipn.State) { } } +func (b *LocalBackend) IPServiceMappings() netmap.IPServiceMappings { + b.mu.Lock() + defer b.mu.Unlock() + return b.ipVIPServiceMap +} + // setConfigLocked uses the provided config to update the backend's prefs // and other state. func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error { @@ -5110,7 +5116,7 @@ func (b *LocalBackend) authReconfigLocked() { } oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.NetMon.Get(), b.sys.ControlKnobs(), version.OS()) - rcfg := b.routerConfigLocked(cfg, prefs, oneCGNATRoute) + rcfg := b.routerConfigLocked(cfg, prefs, nm, oneCGNATRoute) err = b.e.Reconfig(cfg, rcfg, dcfg) if err == wgengine.ErrNoChanges { @@ -5426,7 +5432,7 @@ func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int) (route // routerConfig produces a router.Config from a wireguard config and IPN prefs. // // b.mu must be held. -func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView, oneCGNATRoute bool) *router.Config { +func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView, nm *netmap.NetworkMap, oneCGNATRoute bool) *router.Config { singleRouteThreshold := 10_000 if oneCGNATRoute { singleRouteThreshold = 1 @@ -5511,13 +5517,31 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView } } + // Get the VIPs for VIP services this node hosts. We will add all locally served VIPs to routes then + // we terminate these connection locally in netstack instead of routing to peer. + VIPServiceIPs := nm.GetIPVIPServiceMap() + if slices.ContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs4) { rs.Routes = append(rs.Routes, netip.PrefixFrom(tsaddr.TailscaleServiceIP(), 32)) + for vip := range VIPServiceIPs { + if vip.Is4() { + rs.Routes = append(rs.Routes, netip.PrefixFrom(vip, 32)) + } + } } if slices.ContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs6) { rs.Routes = append(rs.Routes, netip.PrefixFrom(tsaddr.TailscaleServiceIPv6(), 128)) + for vip := range VIPServiceIPs { + if vip.Is6() { + rs.Routes = append(rs.Routes, netip.PrefixFrom(vip, 128)) + } + } } + fmt.Println("LocalAddrs are: ", rs.LocalAddrs) + fmt.Println("SubnetRoutes are: ", rs.SubnetRoutes) + fmt.Println("Routes are: ", rs.Routes) + return rs } diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index fe1bc31b8..8d9a56720 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -22,6 +22,7 @@ "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "go4.org/mem" + "gvisor.dev/gvisor/pkg/tcpip/header" "tailscale.com/disco" "tailscale.com/feature/buildfeatures" "tailscale.com/net/packet" @@ -828,6 +829,13 @@ func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) { ) func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConfigTable, gro *gro.GRO) (filter.Response, *gro.GRO) { + if p.IPProto == ipproto.TCP { + if p.TCPFlags&packet.TCPSyn != 0 { + t.logf("Kevin_check: out->wg enter: SYN %v:%d -> %v:%d", + p.Src.Addr(), p.Src.Port(), + p.Dst.Addr(), p.Dst.Port()) + } + } // Fake ICMP echo responses to MagicDNS (100.100.100.100). if p.IsEchoRequest() { switch p.Dst { @@ -862,11 +870,25 @@ func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConf res, gro = t.PreFilterPacketOutboundToWireGuardNetstackIntercept(p, t, gro) if res.IsDrop() { // Handled by netstack.Impl.handleLocalPackets (quad-100 DNS primarily) + if p.IPProto == ipproto.TCP { + if p.TCPFlags&packet.TCPSyn != 0 { + t.logf("Kevin_check: out->wg: SYN intercepted by netstack %v:%d -> %v:%d", + p.Src.Addr(), p.Src.Port(), + p.Dst.Addr(), p.Dst.Port()) + } + } return res, gro } } if t.PreFilterPacketOutboundToWireGuardEngineIntercept != nil { if res := t.PreFilterPacketOutboundToWireGuardEngineIntercept(p, t); res.IsDrop() { + if p.IPProto == ipproto.TCP { + if p.TCPFlags&packet.TCPSyn != 0 { + t.logf("Kevin_check: out->wg: SYN intercepted by engine %v:%d -> %v:%d", + p.Src.Addr(), p.Src.Port(), + p.Dst.Addr(), p.Dst.Port()) + } + } // Handled by userspaceEngine.handleLocalPackets (primarily handles // quad-100 if netstack is not installed). return res, gro @@ -892,6 +914,13 @@ func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConf Reason: reason, }, 1) } + if p.IPProto == ipproto.TCP { + if p.TCPFlags&packet.TCPSyn != 0 { + t.logf("Kevin_check: out->wg: SYN dropped by filter %v:%d -> %v:%d reason=%q", + p.Src.Addr(), p.Src.Port(), + p.Dst.Addr(), p.Dst.Port()) + } + } return filter.Drop, gro } @@ -942,7 +971,12 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) { if res.err != nil && len(res.data) == 0 { return 0, res.err } + if res.data == nil { + // Kevin_check: confirm injected packets get dequeued by Read(). + if tlog := t.logf; tlog != nil { + logInjectedSYN(tlog, res.injected) // helper below + } return t.injectedRead(res.injected, buffs, sizes, offset) } @@ -956,7 +990,13 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) { var buffsGRO *gro.GRO for _, data := range res.data { p.Decode(data[res.dataOffset:]) - + if p.IPProto == ipproto.TCP { + if p.TCPFlags&packet.TCPSyn != 0 { + t.logf("Kevin_check: tun->ts: SYN %v:%d -> %v:%d", + p.Src.Addr(), p.Src.Port(), + p.Dst.Addr(), p.Dst.Port()) + } + } if buildfeatures.HasLazyWG { if m := t.destIPActivity.Load(); m != nil { if fn := m[p.Dst.Addr()]; fn != nil { @@ -1007,6 +1047,60 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) { return buffsPos, res.err } +func logInjectedSYN(logf func(string, ...any), inj tunInjectedRead) { + // Prefer PacketBuffer if present (netstack-generated). + if inj.packet != nil { + // Assumption: PacketBuffer has NetworkHeader/TransportHeader accessors similar to gVisor stack.PacketBuffer. + // If your netstack_PacketBuffer wrapper differs, paste its methods and I’ll adjust. + nh := inj.packet.NetworkHeader().Slice() + th := inj.packet.TransportHeader().Slice() + + var src, dst netip.Addr + var sp, dp uint16 + var syn bool + + if len(nh) > 0 { + switch inj.packet.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + ip := header.IPv4(nh) + src = netip.AddrFrom4(ip.SourceAddress().As4()) + dst = netip.AddrFrom4(ip.DestinationAddress().As4()) + case header.IPv6ProtocolNumber: + ip := header.IPv6(nh) + src = netip.AddrFrom16(ip.SourceAddress().As16()) + dst = netip.AddrFrom16(ip.DestinationAddress().As16()) + } + } + + if inj.packet.TransportProtocolNumber == header.TCPProtocolNumber && len(th) >= header.TCPMinimumSize { + tcp := header.TCP(th) + sp = tcp.SourcePort() + dp = tcp.DestinationPort() + f := tcp.Flags() + syn = f&header.TCPFlagSyn != 0 + } + + if syn { + logf("Kevin_check: Read(): dequeued injected(PacketBuffer) SYN %v:%d -> %v:%d", src, sp, dst, dp) + } + return + } + + // Else parse raw bytes if present. + if len(inj.data) > 0 { + p := parsedPacketPool.Get().(*packet.Parsed) + defer parsedPacketPool.Put(p) + + p.Decode(inj.data) + + if p.IPProto == ipproto.TCP && (p.TCPFlags&packet.TCPSyn) != 0 { + logf("Kevin_check: Read(): dequeued injected(bytes) SYN %v:%d -> %v:%d", + p.Src.Addr(), p.Src.Port(), p.Dst.Addr(), p.Dst.Port()) + } + return + } +} + const ( minTCPHeaderSize = 20 ) diff --git a/wgengine/netstack/link_endpoint.go b/wgengine/netstack/link_endpoint.go index c5a9dbcbc..301a6990e 100644 --- a/wgengine/netstack/link_endpoint.go +++ b/wgengine/netstack/link_endpoint.go @@ -5,6 +5,8 @@ import ( "context" + "log" + "net/netip" "sync" "gvisor.dev/gvisor/pkg/tcpip" @@ -278,6 +280,7 @@ func (ep *linkEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Er // control MTU (and by effect TCP MSS in gVisor) we *shouldn't* expect to // ever overflow 128 slots (see wireguard-go/tun.ErrTooManySegments usage). for _, pkt := range pkts.AsSlice() { + logLinkEPOut(pkt) if err := ep.q.Write(pkt); err != nil { if _, ok := err.(*tcpip.ErrNoBufferSpace); !ok && n == 0 { return 0, err @@ -290,6 +293,68 @@ func (ep *linkEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Er return n, nil } +func logLinkEPOut(pkt *stack.PacketBuffer) { + nh := pkt.NetworkHeader().Slice() + th := pkt.TransportHeader().Slice() + if len(nh) == 0 || len(th) == 0 { + return + } + if pkt.TransportProtocolNumber != header.TCPProtocolNumber || len(th) < header.TCPMinimumSize { + return + } + + tcp := header.TCP(th) + flags := tcp.Flags() + + // Only log SYN/SYN-ACK/RST (SYN=0x02, ACK=0x10, RST=0x04) + syn := flags&header.TCPFlagSyn != 0 + rst := flags&header.TCPFlagRst != 0 + if !syn && !rst { + return + } + + sp := tcp.SourcePort() + dp := tcp.DestinationPort() + + var srcIP, dstIP netip.Addr + switch pkt.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + if len(nh) < header.IPv4MinimumSize { + return + } + ip := header.IPv4(nh) + srcIP = addrToNetip(ip.SourceAddress()) + dstIP = addrToNetip(ip.DestinationAddress()) + + case header.IPv6ProtocolNumber: + if len(nh) < header.IPv6MinimumSize { + return + } + ip := header.IPv6(nh) + srcIP = addrToNetip(ip.SourceAddress()) + dstIP = addrToNetip(ip.DestinationAddress()) + + default: + return + } + + // If parsing failed, don't log noisy junk. + if !srcIP.IsValid() || !dstIP.IsValid() { + return + } + + log.Printf("linkEP out TCP flags=%#x %s:%d -> %s:%d seq=%d ack=%d", + flags, srcIP, sp, dstIP, dp, tcp.SequenceNumber(), tcp.AckNumber()) +} + +func addrToNetip(a tcpip.Address) netip.Addr { + ip, ok := netip.AddrFromSlice(a.AsSlice()) + if !ok { + return netip.Addr{} + } + return ip.Unmap() +} + // Wait implements stack.LinkEndpoint.Wait. func (*linkEndpoint) Wait() {} diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index c2b5d8a32..a07217770 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -771,6 +771,11 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper, gro *gro. // Determine if we care about this local packet. dst := p.Dst.Addr() + var IPServiceMappings netmap.IPServiceMappings + if ns.lb != nil { + IPServiceMappings = ns.lb.IPServiceMappings() + } + serviceName, hasIP := IPServiceMappings[dst] switch { case dst == serviceIP || dst == serviceIPv6: // We want to intercept some traffic to the "service IP" (e.g. @@ -787,6 +792,30 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper, gro *gro. return filter.Accept, gro } } + case hasIP: + if p.IPProto != ipproto.TCP { + return filter.Accept, gro + } + // returns all configured VIP services, since the IPServiceMappings contains + // inactive service IPs when node hosts the service, we need to check the + // service is active or not before dropping the packet. + VIPServices := ns.lb.VIPServices() + serviceActive := false + for _, svc := range VIPServices { + // Even though control only send service IP down when there is a config + // for the service, we want to still check that the config still exists + // before passing the packet to netstack. + if svc.Name == serviceName { + serviceActive = svc.Active + } + } + if !serviceActive { + return filter.Accept, gro + } + if debugNetstack() { + ns.logf("Kevin_check: netstack: intercepting local VIP service packet: proto=%v dst=%v src=%v", + p.IPProto, p.Dst, p.Src) + } case viaRange.Contains(dst): // We need to handle 4via6 packets leaving the host if the via // route is for this host; otherwise the packet will be dropped @@ -946,6 +975,55 @@ func (ns *Impl) inject() { inboundBuffs, inboundBuffsSizes := ns.getInjectInboundBuffsSizes() for { pkt := ns.linkEP.ReadContext(ns.ctx) + nh := pkt.NetworkHeader().Slice() + th := pkt.TransportHeader().Slice() + + var src, dst netip.Addr + var sp, dp uint16 + var syn, ack, rst bool + + if len(nh) > 0 { + switch pkt.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + ip := header.IPv4(nh) + + sa := ip.SourceAddress() + da := ip.DestinationAddress() + + if s, ok := netip.AddrFromSlice(sa.AsSlice()); ok { + src = s.Unmap() + } + if d, ok := netip.AddrFromSlice(da.AsSlice()); ok { + dst = d.Unmap() + } + + case header.IPv6ProtocolNumber: + ip := header.IPv6(nh) + + sa := ip.SourceAddress() + da := ip.DestinationAddress() + + if s, ok := netip.AddrFromSlice(sa.AsSlice()); ok { + src = s.Unmap() + } + if d, ok := netip.AddrFromSlice(da.AsSlice()); ok { + dst = d.Unmap() + } + } + } + + if pkt.TransportProtocolNumber == header.TCPProtocolNumber && len(th) >= header.TCPMinimumSize { + tcp := header.TCP(th) + sp = tcp.SourcePort() + dp = tcp.DestinationPort() + f := tcp.Flags() + syn = f&header.TCPFlagSyn != 0 + ack = f&header.TCPFlagAck != 0 + rst = f&header.TCPFlagRst != 0 + } + + ns.logf("Kevin_check: inject: dequeued TCP syn=%v ack=%v rst=%v %v:%d -> %v:%d", + syn, ack, rst, src, sp, dst, dp) if pkt == nil { if ns.ctx.Err() != nil { // Return without logging. @@ -965,15 +1043,18 @@ func (ns *Impl) inject() { // send traffic destined for the local device, hence must // be injected 'inbound'. sendToHost := ns.shouldSendToHost(pkt) + ns.logf("Kevin_check: inject: shouldSendToHost=%v", sendToHost) // pkt has a non-zero refcount, so injection methods takes // ownership of one count and will decrement on completion. if sendToHost { + ns.logf("Kevin_check: inject: InjectInboundPacketBuffer") if err := ns.tundev.InjectInboundPacketBuffer(pkt, inboundBuffs, inboundBuffsSizes); err != nil { ns.logf("netstack inject inbound: %v", err) return } } else { + ns.logf("Kevin_check: inject: InjectOutboundPacketBuffer") if err := ns.tundev.InjectOutboundPacketBuffer(pkt); err != nil { ns.logf("netstack inject outbound: %v", err) return @@ -998,12 +1079,28 @@ func (ns *Impl) shouldSendToHost(pkt *stack.PacketBuffer) bool { return true } + if ns.isVIPServiceIP(srcIP) { + dstIP := netip.AddrFrom4(v.DestinationAddress().As4()) + if ns.isLocalIP(dstIP) { + ns.logf("Kevin_check: netstack: sending VIP service packet to host: src=%v dst=%v", srcIP, dstIP) + return true + } + } + case header.IPv6: srcIP := netip.AddrFrom16(v.SourceAddress().As16()) if srcIP == serviceIPv6 { return true } + if ns.isVIPServiceIP(srcIP) { + dstIP := netip.AddrFrom16(v.DestinationAddress().As16()) + if ns.isLocalIP(dstIP) { + ns.logf("Kevin_check: netstack: sending VIP service packet to host: src=%v dst=%v", srcIP, dstIP) + return true + } + } + if viaRange.Contains(srcIP) { // Only send to the host if this 4via6 route is // something this node handles. @@ -1349,7 +1446,7 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) { getConnOrReset := func(opts ...tcpip.SettableSocketOption) *gonet.TCPConn { ep, err := r.CreateEndpoint(&wq) if err != nil { - ns.logf("CreateEndpoint error for %s: %v", stringifyTEI(reqDetails), err) + ns.logf("CreateEndpoint error for %s: (%T) %v", stringifyTEI(reqDetails), err, err) r.Complete(true) // sends a RST return nil } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 875011a9c..87451a505 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -596,6 +596,7 @@ func echoRespondToAll(p *packet.Parsed, t *tstun.Wrapper, gro *gro.GRO) (filter. // tailscaled directly. Other packets are allowed to proceed into the // main ACL filter. func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Response { + isTCPSYN := p.IPProto == ipproto.TCP && (p.TCPFlags&packet.TCPSyn) != 0 if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { isLocalAddr, ok := e.isLocalAddr.LoadOk() if !ok { @@ -606,6 +607,9 @@ func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) // looping back within the kernel network stack. We have to // notice that an outbound packet is actually destined for // ourselves, and loop it back into macOS. + if isTCPSYN { + e.logf("Kevin_check: e.handleLocalPackets: reflecting TCP SYN to local Tailscale IP %v back to OS", p.Dst.Addr()) + } t.InjectInboundCopy(p.Buffer()) metricReflectToOS.Add(1) return filter.Drop @@ -622,7 +626,9 @@ func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) } } } - + if isTCPSYN { + e.logf("Kevin_check: e.handleLocalPackets: outbound TCP SYN to %v", p.Dst) + } return filter.Accept }