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