Fix windows bridge forwarding to connected subnets
This commit is contained in:
+53
-23
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user