From 4810e79a0027cf3bc782091fb4eeaf281bccb2a4 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 5 May 2026 18:11:06 +0200 Subject: [PATCH 1/7] Add Windows DNS firewall to block DNS leaks from non-netbird processes --- client/internal/dns/dnsfw/config.go | 65 +++ client/internal/dns/dnsfw/config_test.go | 38 ++ client/internal/dns/dnsfw/dnsfw.go | 16 + client/internal/dns/dnsfw/dnsfw_other.go | 15 + client/internal/dns/dnsfw/dnsfw_windows.go | 136 ++++++ client/internal/dns/dnsfw/helpers_windows.go | 51 +++ client/internal/dns/dnsfw/rules_windows.go | 249 +++++++++++ client/internal/dns/dnsfw/session_windows.go | 177 ++++++++ client/internal/dns/dnsfw/syscall_windows.go | 36 ++ client/internal/dns/dnsfw/types_windows.go | 412 ++++++++++++++++++ client/internal/dns/dnsfw/types_windows_32.go | 90 ++++ client/internal/dns/dnsfw/types_windows_64.go | 87 ++++ client/internal/dns/dnsfw/zsyscall_windows.go | 130 ++++++ client/internal/dns/host_windows.go | 29 +- 14 files changed, 1524 insertions(+), 7 deletions(-) create mode 100644 client/internal/dns/dnsfw/config.go create mode 100644 client/internal/dns/dnsfw/config_test.go create mode 100644 client/internal/dns/dnsfw/dnsfw.go create mode 100644 client/internal/dns/dnsfw/dnsfw_other.go create mode 100644 client/internal/dns/dnsfw/dnsfw_windows.go create mode 100644 client/internal/dns/dnsfw/helpers_windows.go create mode 100644 client/internal/dns/dnsfw/rules_windows.go create mode 100644 client/internal/dns/dnsfw/session_windows.go create mode 100644 client/internal/dns/dnsfw/syscall_windows.go create mode 100644 client/internal/dns/dnsfw/types_windows.go create mode 100644 client/internal/dns/dnsfw/types_windows_32.go create mode 100644 client/internal/dns/dnsfw/types_windows_64.go create mode 100644 client/internal/dns/dnsfw/zsyscall_windows.go diff --git a/client/internal/dns/dnsfw/config.go b/client/internal/dns/dnsfw/config.go new file mode 100644 index 000000000..6efa6b371 --- /dev/null +++ b/client/internal/dns/dnsfw/config.go @@ -0,0 +1,65 @@ +package dnsfw + +import ( + "os" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +const ( + // EnvDisable disables the DNS firewall entirely when set to a truthy value. + EnvDisable = "NB_DISABLE_DNS_FIREWALL" + // EnvPorts overrides the comma-separated list of remote ports to block. + // Empty disables the firewall. + EnvPorts = "NB_DNS_FIREWALL_PORTS" + // EnvStrict enables strict mode: permit DNS only to the virtual DNS IP + // and the netbird daemon. Default mode also permits anything on the + // netbird tunnel interface, which is safer if NRPT is silently ignored + // by Windows but lets apps reach custom DNS servers via the tunnel. + EnvStrict = "NB_DNS_FIREWALL_STRICT" +) + +// strictMode reports whether strict mode is enabled via env. +func strictMode() bool { + v, _ := strconv.ParseBool(os.Getenv(EnvStrict)) + return v +} + +// defaultBlockedPorts are the well-known DNS ports we block for non-netbird +// processes: 53 (plain DNS) and 853 (DNS-over-TLS). +var defaultBlockedPorts = []uint16{53, 853} + +// blockedPorts returns the effective port list, honoring env overrides. +// A nil return means the firewall should not be installed. +func blockedPorts() []uint16 { + if disabled, _ := strconv.ParseBool(os.Getenv(EnvDisable)); disabled { + log.Infof("dns firewall disabled via %s", EnvDisable) + return nil + } + + override, ok := os.LookupEnv(EnvPorts) + if !ok { + return defaultBlockedPorts + } + + var ports []uint16 + for _, raw := range strings.Split(override, ",") { + raw = strings.TrimSpace(raw) + if raw == "" { + continue + } + port, err := strconv.ParseUint(raw, 10, 16) + if err != nil { + log.Warnf("dns firewall: ignoring invalid port %q in %s: %v", raw, EnvPorts, err) + continue + } + ports = append(ports, uint16(port)) + } + if len(ports) == 0 { + log.Infof("dns firewall disabled via empty %s", EnvPorts) + return nil + } + return ports +} diff --git a/client/internal/dns/dnsfw/config_test.go b/client/internal/dns/dnsfw/config_test.go new file mode 100644 index 000000000..12b266cd4 --- /dev/null +++ b/client/internal/dns/dnsfw/config_test.go @@ -0,0 +1,38 @@ +package dnsfw + +import ( + "reflect" + "testing" +) + +func TestBlockedPorts(t *testing.T) { + tests := []struct { + name string + disable string + ports string + setPorts bool + want []uint16 + }{ + {name: "default", want: defaultBlockedPorts}, + {name: "disabled", disable: "true", want: nil}, + {name: "disabled false keeps default", disable: "false", want: defaultBlockedPorts}, + {name: "override single port", ports: "53", setPorts: true, want: []uint16{53}}, + {name: "override multi", ports: "53, 853 ,5353", setPorts: true, want: []uint16{53, 853, 5353}}, + {name: "override empty disables", ports: "", setPorts: true, want: nil}, + {name: "override invalid skipped", ports: "53,not-a-port,853", setPorts: true, want: []uint16{53, 853}}, + {name: "override only invalid disables", ports: "abc", setPorts: true, want: nil}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(EnvDisable, tc.disable) + if tc.setPorts { + t.Setenv(EnvPorts, tc.ports) + } + got := blockedPorts() + if !reflect.DeepEqual(got, tc.want) { + t.Fatalf("blockedPorts() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/client/internal/dns/dnsfw/dnsfw.go b/client/internal/dns/dnsfw/dnsfw.go new file mode 100644 index 000000000..1c5418c59 --- /dev/null +++ b/client/internal/dns/dnsfw/dnsfw.go @@ -0,0 +1,16 @@ +// Package dnsfw blocks DNS traffic from non-netbird processes when netbird is +// managing the host's DNS, so that resolvers running on apps or libraries +// outside netbird cannot bypass the configured DNS path. +// +// Implementation is Windows-only (uses WFP). On other platforms New returns +// a no-op manager. +package dnsfw + +import "net/netip" + +// Manager controls the per-tunnel DNS firewall. Both methods must be safe +// to call multiple times. +type Manager interface { + Enable(ifaceGUID string, virtualDNSIP netip.Addr) error + Disable() error +} diff --git a/client/internal/dns/dnsfw/dnsfw_other.go b/client/internal/dns/dnsfw/dnsfw_other.go new file mode 100644 index 000000000..458c443a1 --- /dev/null +++ b/client/internal/dns/dnsfw/dnsfw_other.go @@ -0,0 +1,15 @@ +//go:build !windows + +package dnsfw + +import "net/netip" + +// New returns a no-op manager on non-Windows platforms. +func New() Manager { + return noopManager{} +} + +type noopManager struct{} + +func (noopManager) Enable(string, netip.Addr) error { return nil } +func (noopManager) Disable() error { return nil } diff --git a/client/internal/dns/dnsfw/dnsfw_windows.go b/client/internal/dns/dnsfw/dnsfw_windows.go new file mode 100644 index 000000000..f9dc53734 --- /dev/null +++ b/client/internal/dns/dnsfw/dnsfw_windows.go @@ -0,0 +1,136 @@ +//go:build windows + +package dnsfw + +import ( + "fmt" + "net/netip" + "os" + "sync" + "unsafe" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +// New returns a Windows DNS firewall manager backed by WFP. +func New() Manager { + return &windowsManager{} +} + +type windowsManager struct { + mu sync.Mutex + // session is the WFP engine handle. Zero when disabled. + session uintptr +} + +// Enable installs the dns firewall. Strict mode propagates failures; +// non-strict mode logs and returns nil so partial protection is preserved. +func (m *windowsManager) Enable(ifaceGUID string, virtualDNSIP netip.Addr) error { + m.mu.Lock() + defer m.mu.Unlock() + + ports := blockedPorts() + if len(ports) == 0 { + return nil + } + + if m.session != 0 { + if err := m.disableLocked(); err != nil { + return fmt.Errorf("reset existing dns firewall session: %w", err) + } + } + + strict := strictMode() + + luid, err := luidFromGUID(ifaceGUID) + if err != nil { + return m.failOrLog(strict, fmt.Errorf("resolve tun luid from guid %s: %w", ifaceGUID, err)) + } + + exe, err := os.Executable() + if err != nil { + return m.failOrLog(strict, fmt.Errorf("resolve daemon executable path: %w", err)) + } + + cfg := installConfig{ + tunLUID: luid, + daemonExe: exe, + blockedPorts: ports, + strict: strict, + virtualDNSIP: virtualDNSIP, + } + // session==0 signals a hard failure; non-zero with non-nil err is a partial install. + session, installErr := installFilters(cfg) + if session == 0 { + return m.failOrLog(strict, fmt.Errorf("install dns firewall filters: %w", installErr)) + } + + if installErr != nil && strict { + _ = closeSession(session) + return fmt.Errorf("strict dns firewall: partial install: %w", installErr) + } + + m.session = session + log.Infof("dns firewall installed: iface=%s daemon=%s ports=%v strict=%v virtual_dns=%s", + ifaceGUID, exe, ports, strict, virtualDNSIP) + if installErr != nil { + log.Warnf("dns firewall partially installed (some filters failed): %v", installErr) + } + return nil +} + +// failOrLog returns err unchanged in strict mode. In non-strict mode the +// error is logged and nil is returned. +func (m *windowsManager) failOrLog(strict bool, err error) error { + if strict { + return err + } + log.Errorf("dns firewall: %v", err) + return nil +} + +// luidFromGUID converts a Windows interface GUID string to its LUID. +func luidFromGUID(ifaceGUID string) (luid uint64, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic in luidFromGUID: %v", r) + } + }() + + guid, err := windows.GUIDFromString(ifaceGUID) + if err != nil { + return 0, fmt.Errorf("parse guid: %w", err) + } + rc, _, _ := procConvertInterfaceGuidToLuid.Call( + uintptr(unsafe.Pointer(&guid)), + uintptr(unsafe.Pointer(&luid)), + ) + if rc != 0 { + return 0, fmt.Errorf("ConvertInterfaceGuidToLuid returned %d", rc) + } + return luid, nil +} + +var ( + modIphlpapi = windows.NewLazyDLL("iphlpapi.dll") + procConvertInterfaceGuidToLuid = modIphlpapi.NewProc("ConvertInterfaceGuidToLuid") +) + +func (m *windowsManager) Disable() error { + m.mu.Lock() + defer m.mu.Unlock() + return m.disableLocked() +} + +func (m *windowsManager) disableLocked() error { + if m.session == 0 { + return nil + } + if err := closeSession(m.session); err != nil { + return fmt.Errorf("close wfp session: %w", err) + } + m.session = 0 + log.Info("dns firewall removed") + return nil +} diff --git a/client/internal/dns/dnsfw/helpers_windows.go b/client/internal/dns/dnsfw/helpers_windows.go new file mode 100644 index 000000000..d13dde9f9 --- /dev/null +++ b/client/internal/dns/dnsfw/helpers_windows.go @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + * + * Adapted from wireguard-windows tunnel/firewall/helpers.go. + */ + +package dnsfw + +import ( + "fmt" + "runtime" + "syscall" + + "golang.org/x/sys/windows" +) + +func createWtFwpmDisplayData0(name, description string) (*wtFwpmDisplayData0, error) { + namePtr, err := windows.UTF16PtrFromString(name) + if err != nil { + return nil, wrapErr(err) + } + + descriptionPtr, err := windows.UTF16PtrFromString(description) + if err != nil { + return nil, wrapErr(err) + } + + return &wtFwpmDisplayData0{ + name: namePtr, + description: descriptionPtr, + }, nil +} + +func filterWeight(weight uint8) wtFwpValue0 { + return wtFwpValue0{ + _type: cFWP_UINT8, + value: uintptr(weight), + } +} + +func wrapErr(err error) error { + if _, ok := err.(syscall.Errno); !ok { + return err + } + _, file, line, ok := runtime.Caller(1) + if !ok { + return fmt.Errorf("wfp error at unknown location: %w", err) + } + return fmt.Errorf("wfp error at %s:%d: %w", file, line, err) +} diff --git a/client/internal/dns/dnsfw/rules_windows.go b/client/internal/dns/dnsfw/rules_windows.go new file mode 100644 index 000000000..3287e3940 --- /dev/null +++ b/client/internal/dns/dnsfw/rules_windows.go @@ -0,0 +1,249 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2026 NetBird GmbH. All Rights Reserved. + * + * Filter installers adapted from wireguard-windows tunnel/firewall/rules.go. + * The block-DNS approach (port 53 + UDP/TCP) matches what wireguard-windows + * uses for its kill-switch DNS leak protection. We extend it with a + * configurable port set so we also cover :853 (DoT) and any future ports. + */ + +package dnsfw + +import ( + "encoding/binary" + "fmt" + "net/netip" + "unsafe" + + "github.com/hashicorp/go-multierror" + "golang.org/x/sys/windows" + + nberrors "github.com/netbirdio/netbird/client/errors" +) + +// Filters install at outbound ALE_AUTH_CONNECT layers only; inbound replies +// follow the authorized outbound flow. + +// permitTunInterface installs a permit filter for any traffic whose local +// interface is the netbird tunnel. +func permitTunInterface(session uintptr, base *baseObjects, weight uint8, ifLUID uint64) error { + cond := wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_IP_LOCAL_INTERFACE, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT64, + value: uintptr(unsafe.Pointer(&ifLUID)), + }, + } + + filter := wtFwpmFilter0{ + providerKey: &base.provider, + subLayerKey: base.filters, + weight: filterWeight(weight), + numFilterConditions: 1, + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&cond)), + action: wtFwpmAction0{_type: cFWP_ACTION_PERMIT}, + } + + return addOutboundFilters(session, &filter, "Permit netbird tunnel") +} + +// permitDaemonByAppID installs a permit filter matching the netbird daemon +// executable by App-ID. App-ID alone is sufficient because netbird.exe is a +// dedicated binary. +func permitDaemonByAppID(session uintptr, base *baseObjects, daemonExe string, weight uint8) error { + appID, err := daemonAppID(daemonExe) + if err != nil { + return err + } + defer fwpmFreeMemory0(unsafe.Pointer(&appID)) + + cond := wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_ALE_APP_ID, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_BYTE_BLOB_TYPE, + value: uintptr(unsafe.Pointer(appID)), + }, + } + + filter := wtFwpmFilter0{ + providerKey: &base.provider, + subLayerKey: base.filters, + weight: filterWeight(weight), + numFilterConditions: 1, + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&cond)), + action: wtFwpmAction0{_type: cFWP_ACTION_PERMIT}, + } + + return addOutboundFilters(session, &filter, "Permit netbird daemon") +} + +// permitVirtualDNSIP installs a permit filter for DNS-port traffic destined +// for the in-tunnel virtual DNS IP. Used in strict mode in lieu of +// permitTunInterface. +func permitVirtualDNSIP(session uintptr, base *baseObjects, ip netip.Addr, ports []uint16, weight uint8) error { + var merr *multierror.Error + for _, port := range ports { + if err := permitDNSToHost(session, base, ip, port, weight); err != nil { + merr = multierror.Append(merr, fmt.Errorf("permit %s:%d: %w", ip, port, err)) + } + } + return nberrors.FormatErrorOrNil(merr) +} + +func permitDNSToHost(session uintptr, base *baseObjects, ip netip.Addr, port uint16, weight uint8) error { + if !ip.IsValid() { + return fmt.Errorf("invalid address") + } + + var addrCond wtFwpmFilterCondition0 + var layer windows.GUID + // v6 backing must outlive fwpmFilterAdd0; keep it on this stack frame. + var v6 wtFwpByteArray16 + + if ip.Is4() { + v4 := ip.As4() + addrCond = wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_IP_REMOTE_ADDRESS, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT32, + value: uintptr(binary.BigEndian.Uint32(v4[:])), + }, + } + layer = cFWPM_LAYER_ALE_AUTH_CONNECT_V4 + } else { + v6 = wtFwpByteArray16{byteArray16: ip.As16()} + addrCond = wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_IP_REMOTE_ADDRESS, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_BYTE_ARRAY16_TYPE, + value: uintptr(unsafe.Pointer(&v6)), + }, + } + layer = cFWPM_LAYER_ALE_AUTH_CONNECT_V6 + } + + conditions := [2]wtFwpmFilterCondition0{ + addrCond, + { + fieldKey: cFWPM_CONDITION_IP_REMOTE_PORT, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT16, + value: uintptr(port), + }, + }, + } + filter := wtFwpmFilter0{ + providerKey: &base.provider, + subLayerKey: base.filters, + weight: filterWeight(weight), + numFilterConditions: uint32(len(conditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions[0])), + action: wtFwpmAction0{_type: cFWP_ACTION_PERMIT}, + } + + display, err := createWtFwpmDisplayData0(fmt.Sprintf("Permit DNS to %s:%d", ip, port), "") + if err != nil { + return wrapErr(err) + } + filter.displayData = *display + filter.layerKey = layer + + var filterID uint64 + if err := fwpmFilterAdd0(session, &filter, 0, &filterID); err != nil { + return wrapErr(err) + } + _ = v6 + return nil +} + +// blockDNSPorts installs a deny filter for outbound traffic to each of the +// given remote ports over UDP or TCP. Per-port and per-layer failures are +// accumulated; partial coverage is preferred over zero coverage. +func blockDNSPorts(session uintptr, base *baseObjects, ports []uint16, weight uint8) error { + var merr *multierror.Error + for _, port := range ports { + if err := blockDNSPort(session, base, port, weight); err != nil { + merr = multierror.Append(merr, fmt.Errorf("block port %d: %w", port, err)) + } + } + return nberrors.FormatErrorOrNil(merr) +} + +func blockDNSPort(session uintptr, base *baseObjects, port uint16, weight uint8) error { + conditions := [3]wtFwpmFilterCondition0{ + { + fieldKey: cFWPM_CONDITION_IP_REMOTE_PORT, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT16, + value: uintptr(port), + }, + }, + { + fieldKey: cFWPM_CONDITION_IP_PROTOCOL, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT8, + value: uintptr(cIPPROTO_UDP), + }, + }, + // Repeat the IP_PROTOCOL condition for logical OR with TCP. + { + fieldKey: cFWPM_CONDITION_IP_PROTOCOL, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT8, + value: uintptr(cIPPROTO_TCP), + }, + }, + } + + filter := wtFwpmFilter0{ + providerKey: &base.provider, + subLayerKey: base.filters, + weight: filterWeight(weight), + numFilterConditions: uint32(len(conditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions[0])), + action: wtFwpmAction0{_type: cFWP_ACTION_BLOCK}, + } + + return addOutboundFilters(session, &filter, "Block DNS port") +} + +// addOutboundFilters installs the same filter on the v4 and v6 outbound ALE +// connect layers. v4 and v6 are installed independently: failure on one +// layer does not abort the other, and the accumulated errors are returned. +// Partial coverage is preferred over zero coverage. +func addOutboundFilters(session uintptr, filter *wtFwpmFilter0, name string) error { + layers := [...]struct { + layer windows.GUID + label string + }{ + {cFWPM_LAYER_ALE_AUTH_CONNECT_V4, name + " (IPv4)"}, + {cFWPM_LAYER_ALE_AUTH_CONNECT_V6, name + " (IPv6)"}, + } + + var merr *multierror.Error + for _, l := range layers { + display, err := createWtFwpmDisplayData0(l.label, "") + if err != nil { + merr = multierror.Append(merr, fmt.Errorf("%s: %w", l.label, wrapErr(err))) + continue + } + filter.displayData = *display + filter.layerKey = l.layer + + var filterID uint64 + if err := fwpmFilterAdd0(session, filter, 0, &filterID); err != nil { + merr = multierror.Append(merr, fmt.Errorf("%s: %w", l.label, wrapErr(err))) + } + } + return nberrors.FormatErrorOrNil(merr) +} diff --git a/client/internal/dns/dnsfw/session_windows.go b/client/internal/dns/dnsfw/session_windows.go new file mode 100644 index 000000000..7efaac6e3 --- /dev/null +++ b/client/internal/dns/dnsfw/session_windows.go @@ -0,0 +1,177 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + * Copyright (C) 2026 NetBird GmbH. All Rights Reserved. + * + * Session lifecycle and the high-level Install/Close entry points adapted + * from wireguard-windows tunnel/firewall. + */ + +package dnsfw + +import ( + "errors" + "fmt" + "net/netip" + "unsafe" + + "github.com/hashicorp/go-multierror" + "golang.org/x/sys/windows" + + nberrors "github.com/netbirdio/netbird/client/errors" +) + +// installConfig is the input to installFilters. +type installConfig struct { + tunLUID uint64 + daemonExe string + blockedPorts []uint16 + // strict, when true, narrows the carve-out from "anything on tun" to + // "DNS only to virtualDNSIP". virtualDNSIP must be valid in this case. + strict bool + virtualDNSIP netip.Addr +} + +// baseObjects holds the GUIDs of the WFP provider and sublayer registered +// for our session. Both are randomly generated per session. +type baseObjects struct { + provider windows.GUID + filters windows.GUID +} + +// installFilters opens a dynamic WFP session and installs the netbird DNS +// firewall filters. Returns a zero session on hard failure (session create, +// base objects); a non-zero session with a non-nil error is a partial install +// (some per-filter installs failed) and is safe to close. +func installFilters(cfg installConfig) (session uintptr, err error) { + defer func() { + if r := recover(); r != nil { + // Dynamic session: kernel will clean up on process exit even + // if we leave the handle dangling here. + err = fmt.Errorf("panic in installFilters: %v", r) + } + }() + + if len(cfg.blockedPorts) == 0 { + return 0, errors.New("dns firewall: no blocked ports configured") + } + if cfg.strict && !cfg.virtualDNSIP.IsValid() { + return 0, errors.New("dns firewall: strict mode requires a valid virtual DNS IP") + } + + session, err = createSession() + if err != nil { + return 0, err + } + + base, err := registerBaseObjects(session) + if err != nil { + fwpmEngineClose0(session) + return 0, fmt.Errorf("register base objects: %w", err) + } + + var merr *multierror.Error + if cfg.strict { + if err := permitVirtualDNSIP(session, base, cfg.virtualDNSIP, cfg.blockedPorts, 15); err != nil { + merr = multierror.Append(merr, fmt.Errorf("permit virtual dns: %w", err)) + } + } else { + if err := permitTunInterface(session, base, 15, cfg.tunLUID); err != nil { + merr = multierror.Append(merr, fmt.Errorf("permit tun interface: %w", err)) + } + } + if err := permitDaemonByAppID(session, base, cfg.daemonExe, 14); err != nil { + merr = multierror.Append(merr, fmt.Errorf("permit netbird daemon: %w", err)) + } + if err := blockDNSPorts(session, base, cfg.blockedPorts, 10); err != nil { + merr = multierror.Append(merr, fmt.Errorf("block dns ports: %w", err)) + } + + return session, nberrors.FormatErrorOrNil(merr) +} + +// closeSession tears down a WFP session previously opened by installFilters. +// All filters owned by the session are removed. +func closeSession(session uintptr) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic in closeSession: %v", r) + } + }() + + if session == 0 { + return nil + } + if err := fwpmEngineClose0(session); err != nil { + return wrapErr(err) + } + return nil +} + +func createSession() (uintptr, error) { + displayData, err := createWtFwpmDisplayData0("NetBird DNS firewall", "NetBird DNS firewall dynamic session") + if err != nil { + return 0, wrapErr(err) + } + session := wtFwpmSession0{ + displayData: *displayData, + flags: cFWPM_SESSION_FLAG_DYNAMIC, + txnWaitTimeoutInMSec: windows.INFINITE, + } + var handle uintptr + if err := fwpmEngineOpen0(nil, cRPC_C_AUTHN_WINNT, nil, &session, unsafe.Pointer(&handle)); err != nil { + return 0, wrapErr(err) + } + return handle, nil +} + +func registerBaseObjects(session uintptr) (*baseObjects, error) { + bo := &baseObjects{} + var err error + if bo.provider, err = windows.GenerateGUID(); err != nil { + return nil, wrapErr(err) + } + if bo.filters, err = windows.GenerateGUID(); err != nil { + return nil, wrapErr(err) + } + + displayData, err := createWtFwpmDisplayData0("NetBird DNS firewall", "NetBird DNS firewall provider") + if err != nil { + return nil, wrapErr(err) + } + provider := wtFwpmProvider0{ + providerKey: bo.provider, + displayData: *displayData, + } + if err := fwpmProviderAdd0(session, &provider, 0); err != nil { + return nil, wrapErr(err) + } + + subDisplay, err := createWtFwpmDisplayData0("NetBird DNS firewall filters", "Permit and block filters") + if err != nil { + return nil, wrapErr(err) + } + sublayer := wtFwpmSublayer0{ + subLayerKey: bo.filters, + displayData: *subDisplay, + providerKey: &bo.provider, + weight: ^uint16(0), + } + if err := fwpmSubLayerAdd0(session, &sublayer, 0); err != nil { + return nil, wrapErr(err) + } + return bo, nil +} + +// daemonAppID returns the WFP App-ID byte blob for the given executable path. +func daemonAppID(path string) (*wtFwpByteBlob, error) { + pathPtr, err := windows.UTF16PtrFromString(path) + if err != nil { + return nil, wrapErr(err) + } + var appID *wtFwpByteBlob + if err := fwpmGetAppIdFromFileName0(pathPtr, unsafe.Pointer(&appID)); err != nil { + return nil, wrapErr(err) + } + return appID, nil +} diff --git a/client/internal/dns/dnsfw/syscall_windows.go b/client/internal/dns/dnsfw/syscall_windows.go new file mode 100644 index 000000000..56d33290a --- /dev/null +++ b/client/internal/dns/dnsfw/syscall_windows.go @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package dnsfw + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineopen0 +//sys fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmEngineOpen0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineclose0 +//sys fwpmEngineClose0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmEngineClose0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmsublayeradd0 +//sys fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmSubLayerAdd0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmgetappidfromfilename0 +//sys fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmGetAppIdFromFileName0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfreememory0 +//sys fwpmFreeMemory0(p unsafe.Pointer) = fwpuclnt.FwpmFreeMemory0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfilteradd0 +//sys fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) [failretval!=0] = fwpuclnt.FwpmFilterAdd0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/Fwpmu/nf-fwpmu-fwpmtransactionbegin0 +//sys fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionBegin0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactioncommit0 +//sys fwpmTransactionCommit0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionCommit0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactionabort0 +//sys fwpmTransactionAbort0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionAbort0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmprovideradd0 +//sys fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmProviderAdd0 diff --git a/client/internal/dns/dnsfw/types_windows.go b/client/internal/dns/dnsfw/types_windows.go new file mode 100644 index 000000000..aac39dbea --- /dev/null +++ b/client/internal/dns/dnsfw/types_windows.go @@ -0,0 +1,412 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package dnsfw + +import "golang.org/x/sys/windows" + +const ( + anysizeArray = 1 // ANYSIZE_ARRAY defined in winnt.h + + wtFwpBitmapArray64_Size = 8 + + wtFwpByteArray16_Size = 16 + + wtFwpByteArray6_Size = 6 + + wtFwpmAction0_Size = 20 + wtFwpmAction0_filterType_Offset = 4 + + wtFwpV4AddrAndMask_Size = 8 + wtFwpV4AddrAndMask_mask_Offset = 4 + + wtFwpV6AddrAndMask_Size = 17 + wtFwpV6AddrAndMask_prefixLength_Offset = 16 +) + +type wtFwpActionFlag uint32 + +const ( + cFWP_ACTION_FLAG_TERMINATING wtFwpActionFlag = 0x00001000 + cFWP_ACTION_FLAG_NON_TERMINATING wtFwpActionFlag = 0x00002000 + cFWP_ACTION_FLAG_CALLOUT wtFwpActionFlag = 0x00004000 +) + +// FWP_ACTION_TYPE defined in fwptypes.h +type wtFwpActionType uint32 + +const ( + cFWP_ACTION_BLOCK wtFwpActionType = wtFwpActionType(0x00000001 | cFWP_ACTION_FLAG_TERMINATING) + cFWP_ACTION_PERMIT wtFwpActionType = wtFwpActionType(0x00000002 | cFWP_ACTION_FLAG_TERMINATING) + cFWP_ACTION_CALLOUT_TERMINATING wtFwpActionType = wtFwpActionType(0x00000003 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_TERMINATING) + cFWP_ACTION_CALLOUT_INSPECTION wtFwpActionType = wtFwpActionType(0x00000004 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_NON_TERMINATING) + cFWP_ACTION_CALLOUT_UNKNOWN wtFwpActionType = wtFwpActionType(0x00000005 | cFWP_ACTION_FLAG_CALLOUT) + cFWP_ACTION_CONTINUE wtFwpActionType = wtFwpActionType(0x00000006 | cFWP_ACTION_FLAG_NON_TERMINATING) + cFWP_ACTION_NONE wtFwpActionType = 0x00000007 + cFWP_ACTION_NONE_NO_MATCH wtFwpActionType = 0x00000008 + cFWP_ACTION_BITMAP_INDEX_SET wtFwpActionType = 0x00000009 +) + +// FWP_BYTE_BLOB defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_blob_) +type wtFwpByteBlob struct { + size uint32 + data *uint8 +} + +// FWP_MATCH_TYPE defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_match_type_) +type wtFwpMatchType uint32 + +const ( + cFWP_MATCH_EQUAL wtFwpMatchType = 0 + cFWP_MATCH_GREATER wtFwpMatchType = cFWP_MATCH_EQUAL + 1 + cFWP_MATCH_LESS wtFwpMatchType = cFWP_MATCH_GREATER + 1 + cFWP_MATCH_GREATER_OR_EQUAL wtFwpMatchType = cFWP_MATCH_LESS + 1 + cFWP_MATCH_LESS_OR_EQUAL wtFwpMatchType = cFWP_MATCH_GREATER_OR_EQUAL + 1 + cFWP_MATCH_RANGE wtFwpMatchType = cFWP_MATCH_LESS_OR_EQUAL + 1 + cFWP_MATCH_FLAGS_ALL_SET wtFwpMatchType = cFWP_MATCH_RANGE + 1 + cFWP_MATCH_FLAGS_ANY_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ALL_SET + 1 + cFWP_MATCH_FLAGS_NONE_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ANY_SET + 1 + cFWP_MATCH_EQUAL_CASE_INSENSITIVE wtFwpMatchType = cFWP_MATCH_FLAGS_NONE_SET + 1 + cFWP_MATCH_NOT_EQUAL wtFwpMatchType = cFWP_MATCH_EQUAL_CASE_INSENSITIVE + 1 + cFWP_MATCH_PREFIX wtFwpMatchType = cFWP_MATCH_NOT_EQUAL + 1 + cFWP_MATCH_NOT_PREFIX wtFwpMatchType = cFWP_MATCH_PREFIX + 1 + cFWP_MATCH_TYPE_MAX wtFwpMatchType = cFWP_MATCH_NOT_PREFIX + 1 +) + +// FWPM_ACTION0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_action0_) +type wtFwpmAction0 struct { + _type wtFwpActionType + filterType windows.GUID // Windows type: GUID +} + +// Defined in fwpmu.h. 4cd62a49-59c3-4969-b7f3-bda5d32890a4 +var cFWPM_CONDITION_IP_LOCAL_INTERFACE = windows.GUID{ + Data1: 0x4cd62a49, + Data2: 0x59c3, + Data3: 0x4969, + Data4: [8]byte{0xb7, 0xf3, 0xbd, 0xa5, 0xd3, 0x28, 0x90, 0xa4}, +} + +// Defined in fwpmu.h. b235ae9a-1d64-49b8-a44c-5ff3d9095045 +var cFWPM_CONDITION_IP_REMOTE_ADDRESS = windows.GUID{ + Data1: 0xb235ae9a, + Data2: 0x1d64, + Data3: 0x49b8, + Data4: [8]byte{0xa4, 0x4c, 0x5f, 0xf3, 0xd9, 0x09, 0x50, 0x45}, +} + +// Defined in fwpmu.h. 3971ef2b-623e-4f9a-8cb1-6e79b806b9a7 +var cFWPM_CONDITION_IP_PROTOCOL = windows.GUID{ + Data1: 0x3971ef2b, + Data2: 0x623e, + Data3: 0x4f9a, + Data4: [8]byte{0x8c, 0xb1, 0x6e, 0x79, 0xb8, 0x06, 0xb9, 0xa7}, +} + +// Defined in fwpmu.h. 0c1ba1af-5765-453f-af22-a8f791ac775b +var cFWPM_CONDITION_IP_LOCAL_PORT = windows.GUID{ + Data1: 0x0c1ba1af, + Data2: 0x5765, + Data3: 0x453f, + Data4: [8]byte{0xaf, 0x22, 0xa8, 0xf7, 0x91, 0xac, 0x77, 0x5b}, +} + +// Defined in fwpmu.h. c35a604d-d22b-4e1a-91b4-68f674ee674b +var cFWPM_CONDITION_IP_REMOTE_PORT = windows.GUID{ + Data1: 0xc35a604d, + Data2: 0xd22b, + Data3: 0x4e1a, + Data4: [8]byte{0x91, 0xb4, 0x68, 0xf6, 0x74, 0xee, 0x67, 0x4b}, +} + +// Defined in fwpmu.h. d78e1e87-8644-4ea5-9437-d809ecefc971 +var cFWPM_CONDITION_ALE_APP_ID = windows.GUID{ + Data1: 0xd78e1e87, + Data2: 0x8644, + Data3: 0x4ea5, + Data4: [8]byte{0x94, 0x37, 0xd8, 0x09, 0xec, 0xef, 0xc9, 0x71}, +} + +// af043a0a-b34d-4f86-979c-c90371af6e66 +var cFWPM_CONDITION_ALE_USER_ID = windows.GUID{ + Data1: 0xaf043a0a, + Data2: 0xb34d, + Data3: 0x4f86, + Data4: [8]byte{0x97, 0x9c, 0xc9, 0x03, 0x71, 0xaf, 0x6e, 0x66}, +} + +// d9ee00de-c1ef-4617-bfe3-ffd8f5a08957 +var cFWPM_CONDITION_IP_LOCAL_ADDRESS = windows.GUID{ + Data1: 0xd9ee00de, + Data2: 0xc1ef, + Data3: 0x4617, + Data4: [8]byte{0xbf, 0xe3, 0xff, 0xd8, 0xf5, 0xa0, 0x89, 0x57}, +} + +var ( + cFWPM_CONDITION_ICMP_TYPE = cFWPM_CONDITION_IP_LOCAL_PORT + cFWPM_CONDITION_ICMP_CODE = cFWPM_CONDITION_IP_REMOTE_PORT +) + +// 7bc43cbf-37ba-45f1-b74a-82ff518eeb10 +var cFWPM_CONDITION_L2_FLAGS = windows.GUID{ + Data1: 0x7bc43cbf, + Data2: 0x37ba, + Data3: 0x45f1, + Data4: [8]byte{0xb7, 0x4a, 0x82, 0xff, 0x51, 0x8e, 0xeb, 0x10}, +} + +type wtFwpmL2Flags uint32 + +const cFWP_CONDITION_L2_IS_VM2VM wtFwpmL2Flags = 0x00000010 + +var cFWPM_CONDITION_FLAGS = windows.GUID{ + Data1: 0x632ce23b, + Data2: 0x5167, + Data3: 0x435c, + Data4: [8]byte{0x86, 0xd7, 0xe9, 0x03, 0x68, 0x4a, 0xa8, 0x0c}, +} + +type wtFwpmFlags uint32 + +const cFWP_CONDITION_FLAG_IS_LOOPBACK wtFwpmFlags = 0x00000001 + +// Defined in fwpmtypes.h +type wtFwpmFilterFlags uint32 + +const ( + cFWPM_FILTER_FLAG_NONE wtFwpmFilterFlags = 0x00000000 + cFWPM_FILTER_FLAG_PERSISTENT wtFwpmFilterFlags = 0x00000001 + cFWPM_FILTER_FLAG_BOOTTIME wtFwpmFilterFlags = 0x00000002 + cFWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000004 + cFWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT wtFwpmFilterFlags = 0x00000008 + cFWPM_FILTER_FLAG_PERMIT_IF_CALLOUT_UNREGISTERED wtFwpmFilterFlags = 0x00000010 + cFWPM_FILTER_FLAG_DISABLED wtFwpmFilterFlags = 0x00000020 + cFWPM_FILTER_FLAG_INDEXED wtFwpmFilterFlags = 0x00000040 + cFWPM_FILTER_FLAG_HAS_SECURITY_REALM_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000080 + cFWPM_FILTER_FLAG_SYSTEMOS_ONLY wtFwpmFilterFlags = 0x00000100 + cFWPM_FILTER_FLAG_GAMEOS_ONLY wtFwpmFilterFlags = 0x00000200 + cFWPM_FILTER_FLAG_SILENT_MODE wtFwpmFilterFlags = 0x00000400 + cFWPM_FILTER_FLAG_IPSEC_NO_ACQUIRE_INITIATE wtFwpmFilterFlags = 0x00000800 +) + +// FWPM_LAYER_ALE_AUTH_CONNECT_V4 (c38d57d1-05a7-4c33-904f-7fbceee60e82) defined in fwpmu.h +var cFWPM_LAYER_ALE_AUTH_CONNECT_V4 = windows.GUID{ + Data1: 0xc38d57d1, + Data2: 0x05a7, + Data3: 0x4c33, + Data4: [8]byte{0x90, 0x4f, 0x7f, 0xbc, 0xee, 0xe6, 0x0e, 0x82}, +} + +// e1cd9fe7-f4b5-4273-96c0-592e487b8650 +var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 = windows.GUID{ + Data1: 0xe1cd9fe7, + Data2: 0xf4b5, + Data3: 0x4273, + Data4: [8]byte{0x96, 0xc0, 0x59, 0x2e, 0x48, 0x7b, 0x86, 0x50}, +} + +// FWPM_LAYER_ALE_AUTH_CONNECT_V6 (4a72393b-319f-44bc-84c3-ba54dcb3b6b4) defined in fwpmu.h +var cFWPM_LAYER_ALE_AUTH_CONNECT_V6 = windows.GUID{ + Data1: 0x4a72393b, + Data2: 0x319f, + Data3: 0x44bc, + Data4: [8]byte{0x84, 0xc3, 0xba, 0x54, 0xdc, 0xb3, 0xb6, 0xb4}, +} + +// a3b42c97-9f04-4672-b87e-cee9c483257f +var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 = windows.GUID{ + Data1: 0xa3b42c97, + Data2: 0x9f04, + Data3: 0x4672, + Data4: [8]byte{0xb8, 0x7e, 0xce, 0xe9, 0xc4, 0x83, 0x25, 0x7f}, +} + +// 94c44912-9d6f-4ebf-b995-05ab8a088d1b +var cFWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE = windows.GUID{ + Data1: 0x94c44912, + Data2: 0x9d6f, + Data3: 0x4ebf, + Data4: [8]byte{0xb9, 0x95, 0x05, 0xab, 0x8a, 0x08, 0x8d, 0x1b}, +} + +// d4220bd3-62ce-4f08-ae88-b56e8526df50 +var cFWPM_LAYER_INBOUND_MAC_FRAME_NATIVE = windows.GUID{ + Data1: 0xd4220bd3, + Data2: 0x62ce, + Data3: 0x4f08, + Data4: [8]byte{0xae, 0x88, 0xb5, 0x6e, 0x85, 0x26, 0xdf, 0x50}, +} + +// FWP_BITMAP_ARRAY64 defined in fwtypes.h +type wtFwpBitmapArray64 struct { + bitmapArray64 [8]uint8 // Windows type: [8]UINT8 +} + +// FWP_BYTE_ARRAY6 defined in fwtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array6_) +type wtFwpByteArray6 struct { + byteArray6 [6]uint8 // Windows type: [6]UINT8 +} + +// FWP_BYTE_ARRAY16 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array16_) +type wtFwpByteArray16 struct { + byteArray16 [16]uint8 // Windows type [16]UINT8 +} + +// FWP_CONDITION_VALUE0 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_condition_value0). +type wtFwpConditionValue0 wtFwpValue0 + +// FWP_DATA_TYPE defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_data_type_) +type wtFwpDataType uint + +const ( + cFWP_EMPTY wtFwpDataType = 0 + cFWP_UINT8 wtFwpDataType = cFWP_EMPTY + 1 + cFWP_UINT16 wtFwpDataType = cFWP_UINT8 + 1 + cFWP_UINT32 wtFwpDataType = cFWP_UINT16 + 1 + cFWP_UINT64 wtFwpDataType = cFWP_UINT32 + 1 + cFWP_INT8 wtFwpDataType = cFWP_UINT64 + 1 + cFWP_INT16 wtFwpDataType = cFWP_INT8 + 1 + cFWP_INT32 wtFwpDataType = cFWP_INT16 + 1 + cFWP_INT64 wtFwpDataType = cFWP_INT32 + 1 + cFWP_FLOAT wtFwpDataType = cFWP_INT64 + 1 + cFWP_DOUBLE wtFwpDataType = cFWP_FLOAT + 1 + cFWP_BYTE_ARRAY16_TYPE wtFwpDataType = cFWP_DOUBLE + 1 + cFWP_BYTE_BLOB_TYPE wtFwpDataType = cFWP_BYTE_ARRAY16_TYPE + 1 + cFWP_SID wtFwpDataType = cFWP_BYTE_BLOB_TYPE + 1 + cFWP_SECURITY_DESCRIPTOR_TYPE wtFwpDataType = cFWP_SID + 1 + cFWP_TOKEN_INFORMATION_TYPE wtFwpDataType = cFWP_SECURITY_DESCRIPTOR_TYPE + 1 + cFWP_TOKEN_ACCESS_INFORMATION_TYPE wtFwpDataType = cFWP_TOKEN_INFORMATION_TYPE + 1 + cFWP_UNICODE_STRING_TYPE wtFwpDataType = cFWP_TOKEN_ACCESS_INFORMATION_TYPE + 1 + cFWP_BYTE_ARRAY6_TYPE wtFwpDataType = cFWP_UNICODE_STRING_TYPE + 1 + cFWP_BITMAP_INDEX_TYPE wtFwpDataType = cFWP_BYTE_ARRAY6_TYPE + 1 + cFWP_BITMAP_ARRAY64_TYPE wtFwpDataType = cFWP_BITMAP_INDEX_TYPE + 1 + cFWP_SINGLE_DATA_TYPE_MAX wtFwpDataType = 0xff + cFWP_V4_ADDR_MASK wtFwpDataType = cFWP_SINGLE_DATA_TYPE_MAX + 1 + cFWP_V6_ADDR_MASK wtFwpDataType = cFWP_V4_ADDR_MASK + 1 + cFWP_RANGE_TYPE wtFwpDataType = cFWP_V6_ADDR_MASK + 1 + cFWP_DATA_TYPE_MAX wtFwpDataType = cFWP_RANGE_TYPE + 1 +) + +// FWP_V4_ADDR_AND_MASK defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v4_addr_and_mask). +type wtFwpV4AddrAndMask struct { + addr uint32 + mask uint32 +} + +// FWP_V6_ADDR_AND_MASK defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v6_addr_and_mask). +type wtFwpV6AddrAndMask struct { + addr [16]uint8 + prefixLength uint8 +} + +// FWP_VALUE0 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_value0_) +type wtFwpValue0 struct { + _type wtFwpDataType + value uintptr +} + +// FWPM_DISPLAY_DATA0 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwpm_display_data0). +type wtFwpmDisplayData0 struct { + name *uint16 // Windows type: *wchar_t + description *uint16 // Windows type: *wchar_t +} + +// FWPM_FILTER_CONDITION0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter_condition0). +type wtFwpmFilterCondition0 struct { + fieldKey windows.GUID // Windows type: GUID + matchType wtFwpMatchType + conditionValue wtFwpConditionValue0 +} + +// FWPM_PROVIDER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0_) +type wtFwpProvider0 struct { + providerKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags uint32 + providerData wtFwpByteBlob + serviceName *uint16 // Windows type: *wchar_t +} + +type wtFwpmSessionFlagsValue uint32 + +const ( + cFWPM_SESSION_FLAG_DYNAMIC wtFwpmSessionFlagsValue = 0x00000001 // FWPM_SESSION_FLAG_DYNAMIC defined in fwpmtypes.h +) + +// FWPM_SESSION0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_session0). +type wtFwpmSession0 struct { + sessionKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmSessionFlagsValue // Windows type UINT32 + txnWaitTimeoutInMSec uint32 + processId uint32 // Windows type: DWORD + sid *windows.SID + username *uint16 // Windows type: *wchar_t + kernelMode uint8 // Windows type: BOOL +} + +type wtFwpmSublayerFlags uint32 + +const ( + cFWPM_SUBLAYER_FLAG_PERSISTENT wtFwpmSublayerFlags = 0x00000001 // FWPM_SUBLAYER_FLAG_PERSISTENT defined in fwpmtypes.h +) + +// FWPM_SUBLAYER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_sublayer0_) +type wtFwpmSublayer0 struct { + subLayerKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmSublayerFlags + providerKey *windows.GUID // Windows type: *GUID + providerData wtFwpByteBlob + weight uint16 +} + +// Defined in rpcdce.h +type wtRpcCAuthN uint32 + +const ( + cRPC_C_AUTHN_NONE wtRpcCAuthN = 0 + cRPC_C_AUTHN_WINNT wtRpcCAuthN = 10 + cRPC_C_AUTHN_DEFAULT wtRpcCAuthN = 0xFFFFFFFF +) + +// FWPM_PROVIDER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/sv-se/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0). +type wtFwpmProvider0 struct { + providerKey windows.GUID + displayData wtFwpmDisplayData0 + flags uint32 + providerData wtFwpByteBlob + serviceName *uint16 +} + +type wtIPProto uint32 + +const ( + cIPPROTO_ICMP wtIPProto = 1 + cIPPROTO_ICMPV6 wtIPProto = 58 + cIPPROTO_TCP wtIPProto = 6 + cIPPROTO_UDP wtIPProto = 17 +) + +const ( + cFWP_ACTRL_MATCH_FILTER = 1 +) diff --git a/client/internal/dns/dnsfw/types_windows_32.go b/client/internal/dns/dnsfw/types_windows_32.go new file mode 100644 index 000000000..af8a1951e --- /dev/null +++ b/client/internal/dns/dnsfw/types_windows_32.go @@ -0,0 +1,90 @@ +//go:build windows && (386 || arm) + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package dnsfw + +import "golang.org/x/sys/windows" + +const ( + wtFwpByteBlob_Size = 8 + wtFwpByteBlob_data_Offset = 4 + + wtFwpConditionValue0_Size = 8 + wtFwpConditionValue0_uint8_Offset = 4 + + wtFwpmDisplayData0_Size = 8 + wtFwpmDisplayData0_description_Offset = 4 + + wtFwpmFilter0_Size = 152 + wtFwpmFilter0_displayData_Offset = 16 + wtFwpmFilter0_flags_Offset = 24 + wtFwpmFilter0_providerKey_Offset = 28 + wtFwpmFilter0_providerData_Offset = 32 + wtFwpmFilter0_layerKey_Offset = 40 + wtFwpmFilter0_subLayerKey_Offset = 56 + wtFwpmFilter0_weight_Offset = 72 + wtFwpmFilter0_numFilterConditions_Offset = 80 + wtFwpmFilter0_filterCondition_Offset = 84 + wtFwpmFilter0_action_Offset = 88 + wtFwpmFilter0_providerContextKey_Offset = 112 + wtFwpmFilter0_reserved_Offset = 128 + wtFwpmFilter0_filterID_Offset = 136 + wtFwpmFilter0_effectiveWeight_Offset = 144 + + wtFwpmFilterCondition0_Size = 28 + wtFwpmFilterCondition0_matchType_Offset = 16 + wtFwpmFilterCondition0_conditionValue_Offset = 20 + + wtFwpmSession0_Size = 48 + wtFwpmSession0_displayData_Offset = 16 + wtFwpmSession0_flags_Offset = 24 + wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 28 + wtFwpmSession0_processId_Offset = 32 + wtFwpmSession0_sid_Offset = 36 + wtFwpmSession0_username_Offset = 40 + wtFwpmSession0_kernelMode_Offset = 44 + + wtFwpmSublayer0_Size = 44 + wtFwpmSublayer0_displayData_Offset = 16 + wtFwpmSublayer0_flags_Offset = 24 + wtFwpmSublayer0_providerKey_Offset = 28 + wtFwpmSublayer0_providerData_Offset = 32 + wtFwpmSublayer0_weight_Offset = 40 + + wtFwpProvider0_Size = 40 + wtFwpProvider0_displayData_Offset = 16 + wtFwpProvider0_flags_Offset = 24 + wtFwpProvider0_providerData_Offset = 28 + wtFwpProvider0_serviceName_Offset = 36 + + wtFwpTokenInformation_Size = 16 + + wtFwpValue0_Size = 8 + wtFwpValue0_value_Offset = 4 +) + +// FWPM_FILTER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0). +type wtFwpmFilter0 struct { + filterKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmFilterFlags + providerKey *windows.GUID // Windows type: *GUID + providerData wtFwpByteBlob + layerKey windows.GUID // Windows type: GUID + subLayerKey windows.GUID // Windows type: GUID + weight wtFwpValue0 + numFilterConditions uint32 + filterCondition *wtFwpmFilterCondition0 + action wtFwpmAction0 + offset1 [4]byte // Layout correction field + providerContextKey windows.GUID // Windows type: GUID + reserved *windows.GUID // Windows type: *GUID + offset2 [4]byte // Layout correction field + filterID uint64 + effectiveWeight wtFwpValue0 +} diff --git a/client/internal/dns/dnsfw/types_windows_64.go b/client/internal/dns/dnsfw/types_windows_64.go new file mode 100644 index 000000000..5ccdc428f --- /dev/null +++ b/client/internal/dns/dnsfw/types_windows_64.go @@ -0,0 +1,87 @@ +//go:build windows && (amd64 || arm64) + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package dnsfw + +import "golang.org/x/sys/windows" + +const ( + wtFwpByteBlob_Size = 16 + wtFwpByteBlob_data_Offset = 8 + + wtFwpConditionValue0_Size = 16 + wtFwpConditionValue0_uint8_Offset = 8 + + wtFwpmDisplayData0_Size = 16 + wtFwpmDisplayData0_description_Offset = 8 + + wtFwpmFilter0_Size = 200 + wtFwpmFilter0_displayData_Offset = 16 + wtFwpmFilter0_flags_Offset = 32 + wtFwpmFilter0_providerKey_Offset = 40 + wtFwpmFilter0_providerData_Offset = 48 + wtFwpmFilter0_layerKey_Offset = 64 + wtFwpmFilter0_subLayerKey_Offset = 80 + wtFwpmFilter0_weight_Offset = 96 + wtFwpmFilter0_numFilterConditions_Offset = 112 + wtFwpmFilter0_filterCondition_Offset = 120 + wtFwpmFilter0_action_Offset = 128 + wtFwpmFilter0_providerContextKey_Offset = 152 + wtFwpmFilter0_reserved_Offset = 168 + wtFwpmFilter0_filterID_Offset = 176 + wtFwpmFilter0_effectiveWeight_Offset = 184 + + wtFwpmFilterCondition0_Size = 40 + wtFwpmFilterCondition0_matchType_Offset = 16 + wtFwpmFilterCondition0_conditionValue_Offset = 24 + + wtFwpmSession0_Size = 72 + wtFwpmSession0_displayData_Offset = 16 + wtFwpmSession0_flags_Offset = 32 + wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 36 + wtFwpmSession0_processId_Offset = 40 + wtFwpmSession0_sid_Offset = 48 + wtFwpmSession0_username_Offset = 56 + wtFwpmSession0_kernelMode_Offset = 64 + + wtFwpmSublayer0_Size = 72 + wtFwpmSublayer0_displayData_Offset = 16 + wtFwpmSublayer0_flags_Offset = 32 + wtFwpmSublayer0_providerKey_Offset = 40 + wtFwpmSublayer0_providerData_Offset = 48 + wtFwpmSublayer0_weight_Offset = 64 + + wtFwpProvider0_Size = 64 + wtFwpProvider0_displayData_Offset = 16 + wtFwpProvider0_flags_Offset = 32 + wtFwpProvider0_providerData_Offset = 40 + wtFwpProvider0_serviceName_Offset = 56 + + wtFwpValue0_Size = 16 + wtFwpValue0_value_Offset = 8 +) + +// FWPM_FILTER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0). +type wtFwpmFilter0 struct { + filterKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmFilterFlags // Windows type: UINT32 + providerKey *windows.GUID // Windows type: *GUID + providerData wtFwpByteBlob + layerKey windows.GUID // Windows type: GUID + subLayerKey windows.GUID // Windows type: GUID + weight wtFwpValue0 + numFilterConditions uint32 + filterCondition *wtFwpmFilterCondition0 + action wtFwpmAction0 + offset1 [4]byte // Layout correction field + providerContextKey windows.GUID // Windows type: GUID + reserved *windows.GUID // Windows type: *GUID + filterID uint64 + effectiveWeight wtFwpValue0 +} diff --git a/client/internal/dns/dnsfw/zsyscall_windows.go b/client/internal/dns/dnsfw/zsyscall_windows.go new file mode 100644 index 000000000..663331753 --- /dev/null +++ b/client/internal/dns/dnsfw/zsyscall_windows.go @@ -0,0 +1,130 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package dnsfw + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modfwpuclnt = windows.NewLazySystemDLL("fwpuclnt.dll") + + procFwpmEngineClose0 = modfwpuclnt.NewProc("FwpmEngineClose0") + procFwpmEngineOpen0 = modfwpuclnt.NewProc("FwpmEngineOpen0") + procFwpmFilterAdd0 = modfwpuclnt.NewProc("FwpmFilterAdd0") + procFwpmFreeMemory0 = modfwpuclnt.NewProc("FwpmFreeMemory0") + procFwpmGetAppIdFromFileName0 = modfwpuclnt.NewProc("FwpmGetAppIdFromFileName0") + procFwpmProviderAdd0 = modfwpuclnt.NewProc("FwpmProviderAdd0") + procFwpmSubLayerAdd0 = modfwpuclnt.NewProc("FwpmSubLayerAdd0") + procFwpmTransactionAbort0 = modfwpuclnt.NewProc("FwpmTransactionAbort0") + procFwpmTransactionBegin0 = modfwpuclnt.NewProc("FwpmTransactionBegin0") + procFwpmTransactionCommit0 = modfwpuclnt.NewProc("FwpmTransactionCommit0") +) + +func fwpmEngineClose0(engineHandle uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmEngineClose0.Addr(), 1, uintptr(engineHandle), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) { + r1, _, e1 := syscall.Syscall6(procFwpmEngineOpen0.Addr(), 5, uintptr(unsafe.Pointer(serverName)), uintptr(authnService), uintptr(unsafe.Pointer(authIdentity)), uintptr(unsafe.Pointer(session)), uintptr(engineHandle), 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) { + r1, _, e1 := syscall.Syscall6(procFwpmFilterAdd0.Addr(), 4, uintptr(engineHandle), uintptr(unsafe.Pointer(filter)), uintptr(sd), uintptr(unsafe.Pointer(id)), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmFreeMemory0(p unsafe.Pointer) { + syscall.Syscall(procFwpmFreeMemory0.Addr(), 1, uintptr(p), 0, 0) + return +} + +func fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmGetAppIdFromFileName0.Addr(), 2, uintptr(unsafe.Pointer(fileName)), uintptr(appID), 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmProviderAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(provider)), uintptr(sd)) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmSubLayerAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(subLayer)), uintptr(sd)) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmTransactionAbort0(engineHandle uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmTransactionAbort0.Addr(), 1, uintptr(engineHandle), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmTransactionBegin0.Addr(), 2, uintptr(engineHandle), uintptr(flags), 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmTransactionCommit0(engineHandle uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmTransactionCommit0.Addr(), 1, uintptr(engineHandle), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 4a8cf8cec..d644b428d 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -16,6 +16,7 @@ import ( "golang.org/x/sys/windows/registry" nberrors "github.com/netbirdio/netbird/client/errors" + "github.com/netbirdio/netbird/client/internal/dns/dnsfw" "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/winregistry" ) @@ -71,6 +72,7 @@ type registryConfigurator struct { routingAll bool gpo bool nrptEntryCount int + dnsFirewall dnsfw.Manager } func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { @@ -90,8 +92,9 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { } configurator := ®istryConfigurator{ - guid: guid, - gpo: useGPO, + guid: guid, + gpo: useGPO, + dnsFirewall: dnsfw.New(), } if err := configurator.configureInterface(); err != nil { @@ -170,15 +173,23 @@ func (r *registryConfigurator) disableWINSForInterface() error { func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { if config.RouteAll { + if err := r.dnsFirewall.Enable(r.guid, config.ServerIP); err != nil { + return fmt.Errorf("dns firewall: %w", err) + } if err := r.addDNSSetupForAll(config.ServerIP); err != nil { return fmt.Errorf("add dns setup: %w", err) } - } else if r.routingAll { - if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil { - return fmt.Errorf("delete interface registry key property: %w", err) + } else { + if err := r.dnsFirewall.Disable(); err != nil { + log.Errorf("disable dns firewall: %v", err) + } + if r.routingAll { + if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil { + return fmt.Errorf("delete interface registry key property: %w", err) + } + r.routingAll = false + log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP) } - r.routingAll = false - log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP) } r.updateState(stateManager) @@ -406,6 +417,10 @@ func (r *registryConfigurator) restoreHostDNS() error { return fmt.Errorf("remove interface registry key: %w", err) } + if err := r.dnsFirewall.Disable(); err != nil { + log.Errorf("disable dns firewall: %v", err) + } + go r.flushDNSCache() return nil From 6a201d12b503cd92dc9687d6b3da075eacc64118 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 5 May 2026 18:16:28 +0200 Subject: [PATCH 2/7] Extract applyRouteAll helper and reorder package declarations --- client/internal/dns/dnsfw/config.go | 8 ++-- client/internal/dns/dnsfw/dnsfw_other.go | 10 ++-- client/internal/dns/dnsfw/dnsfw_windows.go | 54 +++++++++++----------- client/internal/dns/host_windows.go | 45 ++++++++++-------- 4 files changed, 63 insertions(+), 54 deletions(-) diff --git a/client/internal/dns/dnsfw/config.go b/client/internal/dns/dnsfw/config.go index 6efa6b371..372a34686 100644 --- a/client/internal/dns/dnsfw/config.go +++ b/client/internal/dns/dnsfw/config.go @@ -21,16 +21,16 @@ const ( EnvStrict = "NB_DNS_FIREWALL_STRICT" ) +// defaultBlockedPorts are the well-known DNS ports we block for non-netbird +// processes: 53 (plain DNS) and 853 (DNS-over-TLS). +var defaultBlockedPorts = []uint16{53, 853} + // strictMode reports whether strict mode is enabled via env. func strictMode() bool { v, _ := strconv.ParseBool(os.Getenv(EnvStrict)) return v } -// defaultBlockedPorts are the well-known DNS ports we block for non-netbird -// processes: 53 (plain DNS) and 853 (DNS-over-TLS). -var defaultBlockedPorts = []uint16{53, 853} - // blockedPorts returns the effective port list, honoring env overrides. // A nil return means the firewall should not be installed. func blockedPorts() []uint16 { diff --git a/client/internal/dns/dnsfw/dnsfw_other.go b/client/internal/dns/dnsfw/dnsfw_other.go index 458c443a1..142a8417e 100644 --- a/client/internal/dns/dnsfw/dnsfw_other.go +++ b/client/internal/dns/dnsfw/dnsfw_other.go @@ -4,12 +4,12 @@ package dnsfw import "net/netip" -// New returns a no-op manager on non-Windows platforms. -func New() Manager { - return noopManager{} -} - type noopManager struct{} func (noopManager) Enable(string, netip.Addr) error { return nil } func (noopManager) Disable() error { return nil } + +// New returns a no-op manager on non-Windows platforms. +func New() Manager { + return noopManager{} +} diff --git a/client/internal/dns/dnsfw/dnsfw_windows.go b/client/internal/dns/dnsfw/dnsfw_windows.go index f9dc53734..e1ae412a7 100644 --- a/client/internal/dns/dnsfw/dnsfw_windows.go +++ b/client/internal/dns/dnsfw/dnsfw_windows.go @@ -13,10 +13,10 @@ import ( "golang.org/x/sys/windows" ) -// New returns a Windows DNS firewall manager backed by WFP. -func New() Manager { - return &windowsManager{} -} +var ( + modIphlpapi = windows.NewLazyDLL("iphlpapi.dll") + procConvertInterfaceGuidToLuid = modIphlpapi.NewProc("ConvertInterfaceGuidToLuid") +) type windowsManager struct { mu sync.Mutex @@ -80,6 +80,24 @@ func (m *windowsManager) Enable(ifaceGUID string, virtualDNSIP netip.Addr) error return nil } +func (m *windowsManager) Disable() error { + m.mu.Lock() + defer m.mu.Unlock() + return m.disableLocked() +} + +func (m *windowsManager) disableLocked() error { + if m.session == 0 { + return nil + } + if err := closeSession(m.session); err != nil { + return fmt.Errorf("close wfp session: %w", err) + } + m.session = 0 + log.Info("dns firewall removed") + return nil +} + // failOrLog returns err unchanged in strict mode. In non-strict mode the // error is logged and nil is returned. func (m *windowsManager) failOrLog(strict bool, err error) error { @@ -90,6 +108,11 @@ func (m *windowsManager) failOrLog(strict bool, err error) error { return nil } +// New returns a Windows DNS firewall manager backed by WFP. +func New() Manager { + return &windowsManager{} +} + // luidFromGUID converts a Windows interface GUID string to its LUID. func luidFromGUID(ifaceGUID string) (luid uint64, err error) { defer func() { @@ -111,26 +134,3 @@ func luidFromGUID(ifaceGUID string) (luid uint64, err error) { } return luid, nil } - -var ( - modIphlpapi = windows.NewLazyDLL("iphlpapi.dll") - procConvertInterfaceGuidToLuid = modIphlpapi.NewProc("ConvertInterfaceGuidToLuid") -) - -func (m *windowsManager) Disable() error { - m.mu.Lock() - defer m.mu.Unlock() - return m.disableLocked() -} - -func (m *windowsManager) disableLocked() error { - if m.session == 0 { - return nil - } - if err := closeSession(m.session); err != nil { - return fmt.Errorf("close wfp session: %w", err) - } - m.session = 0 - log.Info("dns firewall removed") - return nil -} diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index d644b428d..2a1cfed1f 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -172,24 +172,8 @@ func (r *registryConfigurator) disableWINSForInterface() error { } func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { - if config.RouteAll { - if err := r.dnsFirewall.Enable(r.guid, config.ServerIP); err != nil { - return fmt.Errorf("dns firewall: %w", err) - } - if err := r.addDNSSetupForAll(config.ServerIP); err != nil { - return fmt.Errorf("add dns setup: %w", err) - } - } else { - if err := r.dnsFirewall.Disable(); err != nil { - log.Errorf("disable dns firewall: %v", err) - } - if r.routingAll { - if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil { - return fmt.Errorf("delete interface registry key property: %w", err) - } - r.routingAll = false - log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP) - } + if err := r.applyRouteAll(config); err != nil { + return err } r.updateState(stateManager) @@ -231,6 +215,31 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager return nil } +func (r *registryConfigurator) applyRouteAll(config HostDNSConfig) error { + if config.RouteAll { + if err := r.dnsFirewall.Enable(r.guid, config.ServerIP); err != nil { + return fmt.Errorf("dns firewall: %w", err) + } + if err := r.addDNSSetupForAll(config.ServerIP); err != nil { + return fmt.Errorf("add dns setup: %w", err) + } + return nil + } + + if err := r.dnsFirewall.Disable(); err != nil { + log.Errorf("disable dns firewall: %v", err) + } + if !r.routingAll { + return nil + } + if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil { + return fmt.Errorf("delete interface registry key property: %w", err) + } + r.routingAll = false + log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP) + return nil +} + func (r *registryConfigurator) updateState(stateManager *statemanager.Manager) { if err := stateManager.UpdateState(&ShutdownState{ Guid: r.guid, From 0571eeaba03b6b3ada81058fd932f9b112870198 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 5 May 2026 18:24:09 +0200 Subject: [PATCH 3/7] Move strictMode to Windows-only and add manager unit tests --- client/internal/dns/dnsfw/config.go | 6 -- client/internal/dns/dnsfw/dnsfw_windows.go | 7 ++ .../internal/dns/dnsfw/dnsfw_windows_test.go | 72 +++++++++++++++++++ 3 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 client/internal/dns/dnsfw/dnsfw_windows_test.go diff --git a/client/internal/dns/dnsfw/config.go b/client/internal/dns/dnsfw/config.go index 372a34686..f11913cdd 100644 --- a/client/internal/dns/dnsfw/config.go +++ b/client/internal/dns/dnsfw/config.go @@ -25,12 +25,6 @@ const ( // processes: 53 (plain DNS) and 853 (DNS-over-TLS). var defaultBlockedPorts = []uint16{53, 853} -// strictMode reports whether strict mode is enabled via env. -func strictMode() bool { - v, _ := strconv.ParseBool(os.Getenv(EnvStrict)) - return v -} - // blockedPorts returns the effective port list, honoring env overrides. // A nil return means the firewall should not be installed. func blockedPorts() []uint16 { diff --git a/client/internal/dns/dnsfw/dnsfw_windows.go b/client/internal/dns/dnsfw/dnsfw_windows.go index e1ae412a7..5ee95fbc1 100644 --- a/client/internal/dns/dnsfw/dnsfw_windows.go +++ b/client/internal/dns/dnsfw/dnsfw_windows.go @@ -6,6 +6,7 @@ import ( "fmt" "net/netip" "os" + "strconv" "sync" "unsafe" @@ -113,6 +114,12 @@ func New() Manager { return &windowsManager{} } +// strictMode reports whether strict mode is enabled via env. +func strictMode() bool { + v, _ := strconv.ParseBool(os.Getenv(EnvStrict)) + return v +} + // luidFromGUID converts a Windows interface GUID string to its LUID. func luidFromGUID(ifaceGUID string) (luid uint64, err error) { defer func() { diff --git a/client/internal/dns/dnsfw/dnsfw_windows_test.go b/client/internal/dns/dnsfw/dnsfw_windows_test.go new file mode 100644 index 000000000..0f7c623bf --- /dev/null +++ b/client/internal/dns/dnsfw/dnsfw_windows_test.go @@ -0,0 +1,72 @@ +//go:build windows + +package dnsfw + +import ( + "net/netip" + "os" + "testing" +) + +func TestStrictMode(t *testing.T) { + tests := []struct { + name string + val string + set bool + want bool + }{ + {name: "unset", want: false}, + {name: "true", val: "true", set: true, want: true}, + {name: "1", val: "1", set: true, want: true}, + {name: "false", val: "false", set: true, want: false}, + {name: "invalid is false", val: "garbage", set: true, want: false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(EnvStrict, tc.val) + if !tc.set { + os.Unsetenv(EnvStrict) + } + if got := strictMode(); got != tc.want { + t.Fatalf("strictMode() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestWindowsManagerDisableIdempotent(t *testing.T) { + m := &windowsManager{} + if err := m.Disable(); err != nil { + t.Fatalf("first Disable on fresh manager: %v", err) + } + if err := m.Disable(); err != nil { + t.Fatalf("second Disable on fresh manager: %v", err) + } + if m.session != 0 { + t.Fatalf("session should remain zero, got %d", m.session) + } +} + +func TestWindowsManagerEnableNoOpWhenDisabledByEnv(t *testing.T) { + t.Setenv(EnvDisable, "true") + + m := &windowsManager{} + if err := m.Enable("00000000-0000-0000-0000-000000000000", netip.Addr{}); err != nil { + t.Fatalf("Enable should be a no-op when firewall disabled by env: %v", err) + } + if m.session != 0 { + t.Fatalf("session must remain zero when env disables firewall, got %d", m.session) + } +} + +func TestWindowsManagerEnableNoOpWhenPortsEmpty(t *testing.T) { + t.Setenv(EnvPorts, "") + + m := &windowsManager{} + if err := m.Enable("00000000-0000-0000-0000-000000000000", netip.Addr{}); err != nil { + t.Fatalf("Enable should be a no-op when ports list is empty: %v", err) + } + if m.session != 0 { + t.Fatalf("session must remain zero when ports list is empty, got %d", m.session) + } +} From 7fd16666e3a568f280119fb5244955ade851352a Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 5 May 2026 18:25:53 +0200 Subject: [PATCH 4/7] Fix Windows lint: handle close error and exclude vendored WFP types from unused --- .golangci.yaml | 3 +++ client/internal/dns/dnsfw/session_windows.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.golangci.yaml b/.golangci.yaml index 900af4ac0..dd8e29b6a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -92,6 +92,9 @@ linters: - linters: - unused path: client/firewall/iptables/rule\.go + - linters: + - unused + path: client/internal/dns/dnsfw/(types|syscall|zsyscall)_windows.*\.go - linters: - gosec - mirror diff --git a/client/internal/dns/dnsfw/session_windows.go b/client/internal/dns/dnsfw/session_windows.go index 7efaac6e3..7a668d62e 100644 --- a/client/internal/dns/dnsfw/session_windows.go +++ b/client/internal/dns/dnsfw/session_windows.go @@ -66,7 +66,7 @@ func installFilters(cfg installConfig) (session uintptr, err error) { base, err := registerBaseObjects(session) if err != nil { - fwpmEngineClose0(session) + _ = fwpmEngineClose0(session) return 0, fmt.Errorf("register base objects: %w", err) } From 0415137acdb58d966e6d4033e9d4fe81f76ff0ec Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 5 May 2026 18:29:23 +0200 Subject: [PATCH 5/7] Address CodeRabbit nits: errors.As, defensive disable, port-aware filter name, log wording, provenance --- client/internal/dns/dnsfw/config.go | 2 +- client/internal/dns/dnsfw/dnsfw_windows.go | 5 +++-- client/internal/dns/dnsfw/helpers_windows.go | 4 +++- client/internal/dns/dnsfw/rules_windows.go | 2 +- client/internal/dns/dnsfw/syscall_windows.go | 2 ++ client/internal/dns/dnsfw/types_windows.go | 2 ++ client/internal/dns/dnsfw/types_windows_32.go | 2 ++ client/internal/dns/dnsfw/types_windows_64.go | 2 ++ 8 files changed, 16 insertions(+), 5 deletions(-) diff --git a/client/internal/dns/dnsfw/config.go b/client/internal/dns/dnsfw/config.go index f11913cdd..0f4fb673a 100644 --- a/client/internal/dns/dnsfw/config.go +++ b/client/internal/dns/dnsfw/config.go @@ -52,7 +52,7 @@ func blockedPorts() []uint16 { ports = append(ports, uint16(port)) } if len(ports) == 0 { - log.Infof("dns firewall disabled via empty %s", EnvPorts) + log.Infof("dns firewall disabled: %s yielded no valid ports", EnvPorts) return nil } return ports diff --git a/client/internal/dns/dnsfw/dnsfw_windows.go b/client/internal/dns/dnsfw/dnsfw_windows.go index 5ee95fbc1..2fefc34bb 100644 --- a/client/internal/dns/dnsfw/dnsfw_windows.go +++ b/client/internal/dns/dnsfw/dnsfw_windows.go @@ -91,10 +91,11 @@ func (m *windowsManager) disableLocked() error { if m.session == 0 { return nil } - if err := closeSession(m.session); err != nil { + session := m.session + m.session = 0 + if err := closeSession(session); err != nil { return fmt.Errorf("close wfp session: %w", err) } - m.session = 0 log.Info("dns firewall removed") return nil } diff --git a/client/internal/dns/dnsfw/helpers_windows.go b/client/internal/dns/dnsfw/helpers_windows.go index d13dde9f9..a17906f08 100644 --- a/client/internal/dns/dnsfw/helpers_windows.go +++ b/client/internal/dns/dnsfw/helpers_windows.go @@ -8,6 +8,7 @@ package dnsfw import ( + "errors" "fmt" "runtime" "syscall" @@ -40,7 +41,8 @@ func filterWeight(weight uint8) wtFwpValue0 { } func wrapErr(err error) error { - if _, ok := err.(syscall.Errno); !ok { + var errno syscall.Errno + if !errors.As(err, &errno) { return err } _, file, line, ok := runtime.Caller(1) diff --git a/client/internal/dns/dnsfw/rules_windows.go b/client/internal/dns/dnsfw/rules_windows.go index 3287e3940..f0a145ae9 100644 --- a/client/internal/dns/dnsfw/rules_windows.go +++ b/client/internal/dns/dnsfw/rules_windows.go @@ -214,7 +214,7 @@ func blockDNSPort(session uintptr, base *baseObjects, port uint16, weight uint8) action: wtFwpmAction0{_type: cFWP_ACTION_BLOCK}, } - return addOutboundFilters(session, &filter, "Block DNS port") + return addOutboundFilters(session, &filter, fmt.Sprintf("Block DNS port %d", port)) } // addOutboundFilters installs the same filter on the v4 and v6 outbound ALE diff --git a/client/internal/dns/dnsfw/syscall_windows.go b/client/internal/dns/dnsfw/syscall_windows.go index 56d33290a..4b01798ba 100644 --- a/client/internal/dns/dnsfw/syscall_windows.go +++ b/client/internal/dns/dnsfw/syscall_windows.go @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + * + * Adapted from wireguard-windows tunnel/firewall/syscall_windows.go. */ package dnsfw diff --git a/client/internal/dns/dnsfw/types_windows.go b/client/internal/dns/dnsfw/types_windows.go index aac39dbea..85dfc8692 100644 --- a/client/internal/dns/dnsfw/types_windows.go +++ b/client/internal/dns/dnsfw/types_windows.go @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + * + * Adapted from wireguard-windows tunnel/firewall/types_windows.go. */ package dnsfw diff --git a/client/internal/dns/dnsfw/types_windows_32.go b/client/internal/dns/dnsfw/types_windows_32.go index af8a1951e..eff82fe4e 100644 --- a/client/internal/dns/dnsfw/types_windows_32.go +++ b/client/internal/dns/dnsfw/types_windows_32.go @@ -3,6 +3,8 @@ /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + * + * Adapted from wireguard-windows tunnel/firewall/types_windows_32.go. */ package dnsfw diff --git a/client/internal/dns/dnsfw/types_windows_64.go b/client/internal/dns/dnsfw/types_windows_64.go index 5ccdc428f..30d2e4b60 100644 --- a/client/internal/dns/dnsfw/types_windows_64.go +++ b/client/internal/dns/dnsfw/types_windows_64.go @@ -3,6 +3,8 @@ /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + * + * Adapted from wireguard-windows tunnel/firewall/types_windows_64.go. */ package dnsfw From f42b8aed90d1603afa725f32771e71f22c4e75bd Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 5 May 2026 18:37:24 +0200 Subject: [PATCH 6/7] Reject port 0 in NB_DNS_FIREWALL_PORTS and roll back firewall on DNS setup failure --- client/internal/dns/dnsfw/config.go | 4 ++++ client/internal/dns/dnsfw/config_test.go | 1 + client/internal/dns/host_windows.go | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/client/internal/dns/dnsfw/config.go b/client/internal/dns/dnsfw/config.go index 0f4fb673a..ee7981603 100644 --- a/client/internal/dns/dnsfw/config.go +++ b/client/internal/dns/dnsfw/config.go @@ -49,6 +49,10 @@ func blockedPorts() []uint16 { log.Warnf("dns firewall: ignoring invalid port %q in %s: %v", raw, EnvPorts, err) continue } + if port == 0 { + log.Warnf("dns firewall: ignoring port 0 in %s", EnvPorts) + continue + } ports = append(ports, uint16(port)) } if len(ports) == 0 { diff --git a/client/internal/dns/dnsfw/config_test.go b/client/internal/dns/dnsfw/config_test.go index 12b266cd4..3a7a9d283 100644 --- a/client/internal/dns/dnsfw/config_test.go +++ b/client/internal/dns/dnsfw/config_test.go @@ -20,6 +20,7 @@ func TestBlockedPorts(t *testing.T) { {name: "override multi", ports: "53, 853 ,5353", setPorts: true, want: []uint16{53, 853, 5353}}, {name: "override empty disables", ports: "", setPorts: true, want: nil}, {name: "override invalid skipped", ports: "53,not-a-port,853", setPorts: true, want: []uint16{53, 853}}, + {name: "override zero skipped", ports: "53,0,853", setPorts: true, want: []uint16{53, 853}}, {name: "override only invalid disables", ports: "abc", setPorts: true, want: nil}, } diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 2a1cfed1f..d792d6882 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -221,7 +221,11 @@ func (r *registryConfigurator) applyRouteAll(config HostDNSConfig) error { return fmt.Errorf("dns firewall: %w", err) } if err := r.addDNSSetupForAll(config.ServerIP); err != nil { - return fmt.Errorf("add dns setup: %w", err) + merr := multierror.Append(nil, fmt.Errorf("add dns setup: %w", err)) + if dErr := r.dnsFirewall.Disable(); dErr != nil { + merr = multierror.Append(merr, fmt.Errorf("rollback dns firewall: %w", dErr)) + } + return nberrors.FormatErrorOrNil(merr) } return nil } From 5f8b88471fd13a2206efdd9fbd83c67a49f5c735 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Wed, 6 May 2026 11:56:19 +0200 Subject: [PATCH 7/7] Initialize dnsFirewall in registryConfigurator tests --- client/internal/dns/host_windows_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/internal/dns/host_windows_test.go b/client/internal/dns/host_windows_test.go index 3cd2b1bd5..c47723324 100644 --- a/client/internal/dns/host_windows_test.go +++ b/client/internal/dns/host_windows_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/windows/registry" + + "github.com/netbirdio/netbird/client/internal/dns/dnsfw" ) // TestNRPTEntriesCleanupOnConfigChange tests that old NRPT entries are properly cleaned up @@ -34,8 +36,9 @@ func TestNRPTEntriesCleanupOnConfigChange(t *testing.T) { }() cfg := ®istryConfigurator{ - guid: testGUID, - gpo: false, + guid: testGUID, + gpo: false, + dnsFirewall: dnsfw.New(), } // Create 125 domains which will result in 3 NRPT rules (50+50+25) @@ -134,8 +137,9 @@ func TestNRPTDomainBatching(t *testing.T) { }() cfg := ®istryConfigurator{ - guid: testGUID, - gpo: false, + guid: testGUID, + gpo: false, + dnsFirewall: dnsfw.New(), } testCases := []struct {