diff options
author | 2025-03-28 16:02:13 +0100 | |
---|---|---|
committer | 2025-05-22 09:29:05 +0200 | |
commit | 3d9b833f47b61a962dfb83548fa3b64814fca362 (patch) | |
tree | c1366e30aca362f48cdb84ab5a9b396d243be625 | |
parent | mod: bump golang.org/x/... (diff) | |
download | wireguard-windows-sr/for-jason.tar.xz wireguard-windows-sr/for-jason.zip |
tunnel: optional blocking for split-tunnelssr/for-jason
This is a TunnelVision mitigation for split-tunnels.
This is a work-in-progress commit. See TODOs in the change-set.
Signed-off-by: Simon Rozman <simon@rozman.si>
-rw-r--r-- | docs/netquirk.md | 5 | ||||
-rw-r--r-- | tunnel/addressconfig.go | 56 | ||||
-rw-r--r-- | tunnel/firewall/blocker.go | 6 | ||||
-rw-r--r-- | tunnel/firewall/rules.go | 98 |
4 files changed, 133 insertions, 32 deletions
diff --git a/docs/netquirk.md b/docs/netquirk.md index d53c8cdb..d2000c33 100644 --- a/docs/netquirk.md +++ b/docs/netquirk.md @@ -8,7 +8,7 @@ The tunnel service takes all the allowed IPs from each peer, deduplicates them, ### Firewall Considerations for `/0` Allowed IPs -If an interface has only one peer, and that peer contains an Allowed IP in `/0`, then WireGuard enables a so-called "kill-switch", which adds firewall rules to do the following: +If an interface has a peer that contains an Allowed IP in `/0`, then WireGuard enables a so-called "kill-switch", which adds firewall rules to do the following: - Packets from the tunnel service itself are permitted, so that WireGuard packets can flow successfully. - If the configuration specifies DNS servers, then packets sent to port `53` are only permitted if they are to one of those DNS servers. This is to prevent Windows' [ordinary multihomed DNS resolution behavior](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29), so that DNS queries only go to the DNS server specified, rather than multiple DNS servers. @@ -20,6 +20,9 @@ This prevents traffic from leaking outside the tunnel. If you'd like to use a default route _without_ having these restrictive kill-switch semantics, one may use the routes `0.0.0.0/1` and `128.0.0.0/1` in place of `0.0.0.0/0`, as well as `::/1` and `8000::/1` in place of `::/0`. This achieves nearly the same thing, but does not activate the above firewalling semantics. (The UI's editor has a checkbox that toggles this.) And users without the need for a `/0` route at all do not have to worry about this, and instead fall back to ordinary Windows routing and DNS behavior. +Should you require the same level of protection for a subrange of allowed IPs in split tunnel scenario, repeat the first address of a range on the list of allowed IPs (e.g. change `10.15.20.0/24, 2001:0db8:85a3:0000::/64` to `10.15.20.0/24, 10.15.20.0, 2001:0db8:85a3:0000::/64, 2001:0db8:85a3:0000::`). The single allowed IPs `/32` and `/128` are always protected by firewall rules. +> TODO: Revise the paragraph above and integrate it into this document better. + ### Considerations for non-`/0` Allowed IPs When the above conditions do not apply, routing and DNS information is handed to Windows in the typical way for Windows to manage. This includes its [ordinary multihomed DNS resolution behavior](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) as well as its ordinary routing table resolution. Users may make use of the normal Windows firewalling and network configuration capabilities to firewall this as needed. One firewall rule is added, however, which allows the tunnel service to send and receive WireGuard packets. diff --git a/tunnel/addressconfig.go b/tunnel/addressconfig.go index a3ce6295..1f817356 100644 --- a/tunnel/addressconfig.go +++ b/tunnel/addressconfig.go @@ -9,6 +9,7 @@ import ( "fmt" "log" "net/netip" + "strings" "time" "golang.org/x/sys/windows" @@ -143,16 +144,55 @@ startOver: return nil } -func enableFirewall(conf *conf.Config, luid winipcfg.LUID) error { - doNotRestrict := true - if len(conf.Peers) == 1 && !conf.Interface.TableOff { - for _, allowedip := range conf.Peers[0].AllowedIPs { - if allowedip.Bits() == 0 && allowedip == allowedip.Masked() { - doNotRestrict = false - break +func restrictedRoutes(conf *conf.Config) map[netip.Prefix]bool { + restrictedRoutes := make(map[netip.Prefix]bool, len(conf.Peers)*3) + if conf.Interface.TableOff { + return restrictedRoutes + } + for _, peer := range conf.Peers { + peerRoutes := make(map[netip.Addr]int, 3) + for _, allowedip := range peer.AllowedIPs { + network := allowedip.Masked() + prefix := network.Addr() + bits := allowedip.Bits() + if bits == 0 && allowedip == network { + restrictedRoutes[network] = true + } + if bits2, ok := peerRoutes[prefix]; !ok || bits < bits2 { + peerRoutes[prefix] = bits + } + } + for _, allowedip := range peer.AllowedIPs { + prefix := allowedip.Masked().Addr() + bits := allowedip.Bits() + if bits == prefix.BitLen() { + if bits2, ok := peerRoutes[prefix]; ok { + restrictedRoutes[netip.PrefixFrom(prefix, bits2)] = true + } else { + restrictedRoutes[netip.PrefixFrom(prefix, bits)] = true + } } } } + return restrictedRoutes +} + +func enableFirewall(conf *conf.Config, luid winipcfg.LUID) error { + restrictedRoutesMap := restrictedRoutes(conf) + restrictedRoutes := make([]netip.Prefix, 0, len(restrictedRoutesMap)) + for key := range restrictedRoutesMap { + restrictedRoutes = append(restrictedRoutes, key) + } + // TODO: Consult zx2c4 whether logging the routes from the config is acceptable data leakage. + // WireGuard routes may be seen using `route print` by non-priviledged user locally anyway. + // But, the problem might be sharing the log for troubleshooting. + if len(restrictedRoutes) > 0 { + addrStrings := make([]string, len(restrictedRoutes)) + for i, address := range restrictedRoutes { + addrStrings[i] = address.String() + } + log.Printf("Restricted routes: %s", strings.Join(addrStrings, ", ")) + } log.Println("Enabling firewall rules") - return firewall.EnableFirewall(uint64(luid), doNotRestrict, conf.Interface.DNS) + return firewall.EnableFirewall(uint64(luid), restrictedRoutes, conf.Interface.DNS) } diff --git a/tunnel/firewall/blocker.go b/tunnel/firewall/blocker.go index 8a4967ba..6f5746ea 100644 --- a/tunnel/firewall/blocker.go +++ b/tunnel/firewall/blocker.go @@ -101,7 +101,7 @@ func registerBaseObjects(session uintptr) (*baseObjects, error) { return bo, nil } -func EnableFirewall(luid uint64, doNotRestrict bool, restrictToDNSServers []netip.Addr) error { +func EnableFirewall(luid uint64, restrictedRoutes []netip.Prefix, restrictToDNSServers []netip.Addr) error { if wfpSession != 0 { return errors.New("The firewall has already been enabled") } @@ -122,7 +122,7 @@ func EnableFirewall(luid uint64, doNotRestrict bool, restrictToDNSServers []neti return wrapErr(err) } - if !doNotRestrict { + if len(restrictedRoutes) > 0 { if len(restrictToDNSServers) > 0 { err = blockDNS(restrictToDNSServers, session, baseObjects, 15, 14) if err != nil { @@ -163,7 +163,7 @@ func EnableFirewall(luid uint64, doNotRestrict bool, restrictToDNSServers []neti } */ - err = blockAll(session, baseObjects, 0) + err = blockRoutes(restrictedRoutes, session, baseObjects, 0) if err != nil { return wrapErr(err) } diff --git a/tunnel/firewall/rules.go b/tunnel/firewall/rules.go index 41632f98..4788b0e2 100644 --- a/tunnel/firewall/rules.go +++ b/tunnel/firewall/rules.go @@ -581,6 +581,19 @@ func permitDHCPIPv6(session uintptr, baseObjects *baseObjects, weight uint8) err return nil } +func setFilterConditions(filter *wtFwpmFilter0, conditions []wtFwpmFilterCondition0) { + if uint(len(conditions)) > uint(^uint32(0)) { + panic("number of filter conditions out of bounds") + } + if len(conditions) > 0 { + filter.numFilterConditions = uint32(len(conditions)) + filter.filterCondition = (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions[0])) + return + } + filter.numFilterConditions = 0 + filter.filterCondition = nil +} + func permitNdp(session uintptr, baseObjects *baseObjects, weight uint8) error { /* TODO: actually handle the hop limit somehow! The rules should vaguely be: * - icmpv6 133: must be outgoing, dst must be FF02::2/128, hop limit must be 255 @@ -809,8 +822,7 @@ func permitNdp(session uintptr, baseObjects *baseObjects, weight uint8) error { for _, definition := range defs { filter.displayData = *definition.displayData filter.layerKey = definition.layer - filter.numFilterConditions = uint32(len(definition.conditions)) - filter.filterCondition = (*wtFwpmFilterCondition0)(unsafe.Pointer(&definition.conditions[0])) + setFilterConditions(&filter, definition.conditions) err := fwpmFilterAdd0(session, &filter, 0, &filterID) if err != nil { @@ -895,8 +907,51 @@ func permitHyperV(session uintptr, baseObjects *baseObjects, weight uint8) error return nil } -// Block all traffic except what is explicitly permitted by other rules. -func blockAll(session uintptr, baseObjects *baseObjects, weight uint8) error { +// Block all traffic on restritced routes except what is explicitly permitted by other rules. +func blockRoutes(routes []netip.Prefix, session uintptr, baseObjects *baseObjects, weight uint8) error { + allOnesV4 := netip.AddrFrom4([4]byte{0xff, 0xff, 0xff, 0xff}) + storedPointersV4 := make([]*wtFwpV4AddrAndMask, 0, len(routes)) + denyConditionsV4 := make([]wtFwpmFilterCondition0, 0, len(routes)) + for _, a := range routes { + if !a.Addr().Is4() { + continue + } + address := wtFwpV4AddrAndMask{ + addr: binary.BigEndian.Uint32(a.Addr().AsSlice()), + mask: binary.BigEndian.Uint32(netip.PrefixFrom(allOnesV4, a.Bits()).Masked().Addr().AsSlice()), + } + denyConditionsV4 = append(denyConditionsV4, wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_IP_REMOTE_ADDRESS, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_V4_ADDR_MASK, + value: uintptr(unsafe.Pointer(&address)), + }, + }) + storedPointersV4 = append(storedPointersV4, &address) + } + + storedPointersV6 := make([]*wtFwpV6AddrAndMask, 0, len(routes)) + denyConditionsV6 := make([]wtFwpmFilterCondition0, 0, len(routes)) + for _, a := range routes { + if !a.Addr().Is6() { + continue + } + address := wtFwpV6AddrAndMask{ + addr: a.Addr().As16(), + prefixLength: uint8(a.Bits()), + } + denyConditionsV6 = append(denyConditionsV6, wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_IP_REMOTE_ADDRESS, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_V6_ADDR_MASK, + value: uintptr(unsafe.Pointer(&address)), + }, + }) + storedPointersV6 = append(storedPointersV6, &address) + } + filter := wtFwpmFilter0{ providerKey: &baseObjects.provider, subLayerKey: baseObjects.filters, @@ -907,12 +962,13 @@ func blockAll(session uintptr, baseObjects *baseObjects, weight uint8) error { } filterID := uint64(0) + setFilterConditions(&filter, denyConditionsV4) // // #1 Block outbound traffic on IPv4. // { - displayData, err := createWtFwpmDisplayData0("Block all outbound (IPv4)", "") + displayData, err := createWtFwpmDisplayData0("Block restricted routes outbound (IPv4)", "") if err != nil { return wrapErr(err) } @@ -930,7 +986,7 @@ func blockAll(session uintptr, baseObjects *baseObjects, weight uint8) error { // #2 Block inbound traffic on IPv4. // { - displayData, err := createWtFwpmDisplayData0("Block all inbound (IPv4)", "") + displayData, err := createWtFwpmDisplayData0("Block restricted routes inbound (IPv4)", "") if err != nil { return wrapErr(err) } @@ -944,11 +1000,13 @@ func blockAll(session uintptr, baseObjects *baseObjects, weight uint8) error { } } + setFilterConditions(&filter, denyConditionsV6) + // // #3 Block outbound traffic on IPv6. // { - displayData, err := createWtFwpmDisplayData0("Block all outbound (IPv6)", "") + displayData, err := createWtFwpmDisplayData0("Block restricted routes outbound (IPv6)", "") if err != nil { return wrapErr(err) } @@ -966,7 +1024,7 @@ func blockAll(session uintptr, baseObjects *baseObjects, weight uint8) error { // #4 Block inbound traffic on IPv6. // { - displayData, err := createWtFwpmDisplayData0("Block all inbound (IPv6)", "") + displayData, err := createWtFwpmDisplayData0("Block restricted routes inbound (IPv6)", "") if err != nil { return wrapErr(err) } @@ -980,6 +1038,9 @@ func blockAll(session uintptr, baseObjects *baseObjects, weight uint8) error { } } + runtime.KeepAlive(storedPointersV4) + runtime.KeepAlive(storedPointersV6) + return nil } @@ -1018,15 +1079,14 @@ func blockDNS(except []netip.Addr, session uintptr, baseObjects *baseObjects, we } filter := wtFwpmFilter0{ - providerKey: &baseObjects.provider, - subLayerKey: baseObjects.filters, - weight: filterWeight(weightDeny), - numFilterConditions: uint32(len(denyConditions)), - filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&denyConditions[0])), + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weightDeny), action: wtFwpmAction0{ _type: cFWP_ACTION_BLOCK, }, } + setFilterConditions(&filter, denyConditions) filterID := uint64(0) @@ -1138,15 +1198,14 @@ func blockDNS(except []netip.Addr, session uintptr, baseObjects *baseObjects, we } filter = wtFwpmFilter0{ - providerKey: &baseObjects.provider, - subLayerKey: baseObjects.filters, - weight: filterWeight(weightAllow), - numFilterConditions: uint32(len(allowConditionsV4)), - filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&allowConditionsV4[0])), + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weightAllow), action: wtFwpmAction0{ _type: cFWP_ACTION_PERMIT, }, } + setFilterConditions(&filter, allowConditionsV4) filterID = uint64(0) @@ -1186,8 +1245,7 @@ func blockDNS(except []netip.Addr, session uintptr, baseObjects *baseObjects, we } } - filter.filterCondition = (*wtFwpmFilterCondition0)(unsafe.Pointer(&allowConditionsV6[0])) - filter.numFilterConditions = uint32(len(allowConditionsV6)) + setFilterConditions(&filter, allowConditionsV6) // // #7 Allow IPv6 outbound DNS. |