From 5fee7552e7f68e26b0d7101fce44a742852da28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Jul 2026 19:45:13 +0800 Subject: [PATCH] Fix windows bridge forwarding to connected subnets --- common/windivert/filter.go | 76 ++++++++++----- protocol/bridge/backend_windows.go | 152 +++++++++++++++++++++++++---- 2 files changed, 184 insertions(+), 44 deletions(-) diff --git a/common/windivert/filter.go b/common/windivert/filter.go index 4f5a60c2c..04cab2a0f 100644 --- a/common/windivert/filter.go +++ b/common/windivert/filter.go @@ -73,8 +73,9 @@ type filterInst struct { // // Zero value = "reject all" (match nothing), suitable for send-only handles. type Filter struct { - insts []filterInst - flags uint64 // filter flags for STARTUP ioctl + insts []filterInst + anyInsts []filterInst // trailing OR block: any match accepts + flags uint64 // filter flags for STARTUP ioctl } // reject returns a filter that matches no packet. The empty insts slice @@ -116,28 +117,41 @@ func OutboundTCP(src, dst netip.AddrPort) (*Filter, error) { return f, nil } -func inboundTo(destination netip.Addr) (*Filter, error) { - if !destination.IsValid() { - return nil, E.New("windivert: filter: invalid address") +func inboundTo(destinations []netip.Addr) (*Filter, error) { + if len(destinations) == 0 { + return nil, E.New("windivert: filter: no destination address") + } + isV6 := destinations[0].Is6() + for _, destination := range destinations { + if !destination.IsValid() { + return nil, E.New("windivert: filter: invalid address") + } + if destination.Is6() != isV6 { + return nil, E.New("windivert: filter: mixed IPv4/IPv6") + } } f := &Filter{ flags: filterFlagInbound, } f.add(fieldInbound, testEQ, argUint32(1)) - if destination.Is4() { + if !isV6 { f.flags |= filterFlagIP f.add(fieldIP, testEQ, argUint32(1)) - f.add(fieldIPDstAddr, testEQ, argIPv4(destination)) + for _, destination := range destinations { + f.addAny(fieldIPDstAddr, testEQ, argIPv4(destination)) + } } else { f.flags |= filterFlagIPv6 f.add(fieldIPv6, testEQ, argUint32(1)) - f.add(fieldIPv6DstAddr, testEQ, argIPv6(destination)) + for _, destination := range destinations { + f.addAny(fieldIPv6DstAddr, testEQ, argIPv6(destination)) + } } return f, nil } -func InboundTCPPortRange(destination netip.Addr, portLow, portHigh uint16) (*Filter, error) { - f, err := inboundTo(destination) +func InboundTCPPortRange(destinations []netip.Addr, portLow, portHigh uint16) (*Filter, error) { + f, err := inboundTo(destinations) if err != nil { return nil, err } @@ -147,8 +161,8 @@ func InboundTCPPortRange(destination netip.Addr, portLow, portHigh uint16) (*Fil return f, nil } -func InboundUDPPortRange(destination netip.Addr, portLow, portHigh uint16) (*Filter, error) { - f, err := inboundTo(destination) +func InboundUDPPortRange(destinations []netip.Addr, portLow, portHigh uint16) (*Filter, error) { + f, err := inboundTo(destinations) if err != nil { return nil, err } @@ -158,12 +172,12 @@ func InboundUDPPortRange(destination netip.Addr, portLow, portHigh uint16) (*Fil return f, nil } -func InboundICMPEchoReply(destination netip.Addr) (*Filter, error) { - f, err := inboundTo(destination) +func InboundICMPEchoReply(destinations []netip.Addr) (*Filter, error) { + f, err := inboundTo(destinations) if err != nil { return nil, err } - if destination.Is4() { + if destinations[0].Is4() { f.add(fieldICMP, testEQ, argUint32(1)) f.add(fieldICMPType, testEQ, argUint32(0)) } else { @@ -173,12 +187,12 @@ func InboundICMPEchoReply(destination netip.Addr) (*Filter, error) { return f, nil } -func InboundICMPError(destination netip.Addr) (*Filter, error) { - f, err := inboundTo(destination) +func InboundICMPError(destinations []netip.Addr) (*Filter, error) { + f, err := inboundTo(destinations) if err != nil { return nil, err } - if destination.Is4() { + if destinations[0].Is4() { f.add(fieldICMP, testEQ, argUint32(1)) f.add(fieldICMPType, testGEQ, argUint32(3)) f.add(fieldICMPType, testLEQ, argUint32(12)) @@ -194,6 +208,10 @@ func (f *Filter) add(field uint16, test uint8, arg [4]uint32) { f.insts = append(f.insts, filterInst{field: field, test: test, arg: arg}) } +func (f *Filter) addAny(field uint16, test uint8, arg [4]uint32) { + f.anyInsts = append(f.anyInsts, filterInst{field: field, test: test, arg: arg}) +} + func argUint32(v uint32) [4]uint32 { return [4]uint32{v, 0, 0, 0} } // argIPv4 encodes an IPv4 address for IP_SRCADDR/IP_DSTADDR. The driver @@ -222,9 +240,12 @@ func argIPv6(addr netip.Addr) [4]uint32 { } // encode serializes the Filter to the on-wire WINDIVERT_FILTER[] format -// plus the filter_flags for STARTUP ioctl. +// plus the filter_flags for STARTUP ioctl. insts chain as AND (failure +// rejects); anyInsts follow as an OR block (success accepts, failure falls +// through to the next alternative). func (f *Filter) encode() ([]byte, uint64, error) { - if len(f.insts) == 0 { + total := len(f.insts) + len(f.anyInsts) + if total == 0 { // "Reject all" — one instruction, ZERO == 0 is always true, but we // invert by setting both success and failure to REJECT. return encodeInst(filterInst{ @@ -234,12 +255,12 @@ func (f *Filter) encode() ([]byte, uint64, error) { failure: resultReject, }), 0, nil } - if len(f.insts) > filterMaxInsts-1 { + if total > filterMaxInsts-1 { return nil, 0, E.New("windivert: filter too long") } - buf := make([]byte, 0, filterInstBytes*len(f.insts)) + buf := make([]byte, 0, filterInstBytes*total) for i, inst := range f.insts { - if i == len(f.insts)-1 { + if i == total-1 { inst.success = resultAccept } else { inst.success = uint16(i + 1) @@ -247,6 +268,15 @@ func (f *Filter) encode() ([]byte, uint64, error) { inst.failure = resultReject buf = append(buf, encodeInst(inst)...) } + for i, inst := range f.anyInsts { + inst.success = resultAccept + if len(f.insts)+i == total-1 { + inst.failure = resultReject + } else { + inst.failure = uint16(len(f.insts) + i + 1) + } + buf = append(buf, encodeInst(inst)...) + } return buf, f.flags, nil } diff --git a/protocol/bridge/backend_windows.go b/protocol/bridge/backend_windows.go index 12f858eaf..4ec1c3acb 100644 --- a/protocol/bridge/backend_windows.go +++ b/protocol/bridge/backend_windows.go @@ -6,7 +6,9 @@ import ( "context" "encoding/binary" "errors" + "net" "net/netip" + "slices" "sync" "sync/atomic" "time" @@ -16,6 +18,7 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun/gtcpip" "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -43,10 +46,62 @@ const ( divertICMPError ) +type localSegment struct { + prefix netip.Prefix + address netip.Addr +} + type egressState struct { - inet4 netip.Addr - inet6 netip.Addr - mtu uint32 + inet4 netip.Addr + inet6 netip.Addr + mtu uint32 + inet4Segments []localSegment + inet6Segments []localSegment +} + +func (s *egressState) equal(other *egressState) bool { + return s.inet4 == other.inet4 && s.inet6 == other.inet6 && s.mtu == other.mtu && + slices.Equal(s.inet4Segments, other.inet4Segments) && + slices.Equal(s.inet6Segments, other.inet6Segments) +} + +// sourceAddress picks the translated source for an outbound packet. Windows +// routes with the strong host model: the route lookup is constrained to the +// interface owning the source address, so choosing the source is what steers +// the packet. Destinations in a connected subnet take that subnet's own +// address; everything else takes the egress address. +func (s *egressState) sourceAddress(destination netip.Addr, isV6 bool) netip.Addr { + segments := s.inet4Segments + egressAddress := s.inet4 + if isV6 { + segments = s.inet6Segments + egressAddress = s.inet6 + } + for _, segment := range segments { + if segment.prefix.Contains(destination) { + return segment.address + } + } + return egressAddress +} + +func (s *egressState) divertAddresses(isV6 bool) []netip.Addr { + egressAddress := s.inet4 + segments := s.inet4Segments + if isV6 { + egressAddress = s.inet6 + segments = s.inet6Segments + } + if !egressAddress.IsValid() { + return nil + } + addresses := []netip.Addr{egressAddress} + for _, segment := range segments { + if !slices.Contains(addresses, segment.address) { + addresses = append(addresses, segment.address) + } + } + return addresses } type diverter struct { @@ -177,13 +232,13 @@ func (b *backendWindows) rebuildDivertersLocked(state *egressState) error { b.diverters = nil if b.inet4Port.IsValid() && state.inet4.IsValid() { - err := b.openFamilyDiverters(state.inet4, false) + err := b.openFamilyDiverters(state.divertAddresses(false), false) if err != nil { return err } } if b.inet6Port.IsValid() && state.inet6.IsValid() { - err := b.openFamilyDiverters(state.inet6, true) + err := b.openFamilyDiverters(state.divertAddresses(true), true) if err != nil { return err } @@ -191,7 +246,7 @@ func (b *backendWindows) rebuildDivertersLocked(state *egressState) error { return nil } -func (b *backendWindows) openFamilyDiverters(egressAddr netip.Addr, isV6 bool) error { +func (b *backendWindows) openFamilyDiverters(addresses []netip.Addr, isV6 bool) error { portHigh := uint16(uint32(b.reservedStart) + uint32(bridgeReservedPortCount) - 1) entries := []struct { what string @@ -199,16 +254,16 @@ func (b *backendWindows) openFamilyDiverters(egressAddr netip.Addr, isV6 bool) e build func() (*windivert.Filter, error) }{ {"TCP", divertTransport, func() (*windivert.Filter, error) { - return windivert.InboundTCPPortRange(egressAddr, b.reservedStart, portHigh) + return windivert.InboundTCPPortRange(addresses, b.reservedStart, portHigh) }}, {"UDP", divertTransport, func() (*windivert.Filter, error) { - return windivert.InboundUDPPortRange(egressAddr, b.reservedStart, portHigh) + return windivert.InboundUDPPortRange(addresses, b.reservedStart, portHigh) }}, {"ICMP echo", divertICMPEcho, func() (*windivert.Filter, error) { - return windivert.InboundICMPEchoReply(egressAddr) + return windivert.InboundICMPEchoReply(addresses) }}, {"ICMP error", divertICMPError, func() (*windivert.Filter, error) { - return windivert.InboundICMPError(egressAddr) + return windivert.InboundICMPError(addresses) }}, } for _, entry := range entries { @@ -577,28 +632,24 @@ func (b *backendWindows) flushOutboundLocked() { } func (b *backendWindows) prepareOutbound(packet []byte, state *egressState) bool { - var ( - isV6 bool - egressAddr netip.Addr - ) + var isV6 bool switch header.IPVersion(packet) { case header.IPv4Version: - egressAddr = state.inet4 case header.IPv6Version: isV6 = true - egressAddr = state.inet6 default: return false } - if !egressAddr.IsValid() { - return false - } // The batched injection ioctl walks the buffer by IP total length; a // packet with trailing bytes would desynchronize the walk and fail the // whole batch. if ipPacketLength(packet) != len(packet) { return false } + egressAddr := state.sourceAddress(packetRemoteAddress(packet, isV6, false), isV6) + if !egressAddr.IsValid() { + return false + } info, valid := parseTransport(packet, isV6) if !valid { return false @@ -642,8 +693,10 @@ func (b *backendWindows) syncEgress() { } state := b.currentEgressState() previous := b.egress.Load() - if previous != nil && previous.inet4 == state.inet4 && previous.inet6 == state.inet6 { - if *previous != *state { + if previous != nil && + slices.Equal(previous.divertAddresses(false), state.divertAddresses(false)) && + slices.Equal(previous.divertAddresses(true), state.divertAddresses(true)) { + if !previous.equal(state) { b.egress.Store(state) } return @@ -688,9 +741,66 @@ func (b *backendWindows) currentEgressState() *egressState { state.inet6 = address } } + b.collectLocalSegments(state, finder) return state } +// collectLocalSegments gathers the connected subnets whose destinations must +// bypass the egress pin so they leave on their own interface, mirroring the +// Linux backend (routing table) and the Darwin backend (pf pass-in rules). +// With a pinned egress only its own subnets are considered. +func (b *backendWindows) collectLocalSegments(state *egressState, finder control.InterfaceFinder) { + for _, localInterface := range finder.Interfaces() { + if b.boundInterface != "" && localInterface.Name != b.boundInterface { + continue + } + if localInterface.Flags&net.FlagUp == 0 || localInterface.Flags&net.FlagBroadcast == 0 || + localInterface.Flags&net.FlagLoopback != 0 || localInterface.Flags&net.FlagPointToPoint != 0 { + continue + } + for _, prefix := range localInterface.Addresses { + address := prefix.Addr().Unmap() + if !address.IsGlobalUnicast() { + continue + } + segment := localSegment{ + prefix: netip.PrefixFrom(address, prefix.Bits()).Masked(), + address: address, + } + if address.Is4() { + if state.inet4.IsValid() { + state.inet4Segments = appendSegment(state.inet4Segments, segment) + } + } else if state.inet6.IsValid() { + state.inet6Segments = appendSegment(state.inet6Segments, segment) + } + } + } + sortSegments(state.inet4Segments) + sortSegments(state.inet6Segments) +} + +func appendSegment(segments []localSegment, segment localSegment) []localSegment { + for _, existing := range segments { + if existing.prefix == segment.prefix { + return segments + } + } + return append(segments, segment) +} + +func sortSegments(segments []localSegment) { + slices.SortFunc(segments, func(a, b localSegment) int { + if a.prefix.Bits() != b.prefix.Bits() { + return b.prefix.Bits() - a.prefix.Bits() + } + if result := a.prefix.Addr().Compare(b.prefix.Addr()); result != 0 { + return result + } + return a.address.Compare(b.address) + }) +} + func (b *backendWindows) Close() error { b.closeOnce.Do(func() { if b.closed != nil {