Fix windows bridge forwarding to connected subnets

This commit is contained in:
世界
2026-07-08 19:45:13 +08:00
parent 24607d331b
commit 5fee7552e7
2 changed files with 184 additions and 44 deletions
+53 -23
View File
@@ -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
}
+131 -21
View File
@@ -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 {