From 1a1bc14804491546c9d3375bf49c6869382b50b6 Mon Sep 17 00:00:00 2001
From: Vladimir Stoilov <vladimir@safing.io>
Date: Wed, 27 Nov 2024 17:10:47 +0200
Subject: [PATCH] Feature/systemd query events (#1728)

* [service] Subscribe to systemd-resolver events

* [service] Add disabled state to the resolver

* [service] Add ETW DNS event listener

* [service] DNS listener refactoring

* [service] Add windows core dll project

* [service] DNSListener refactoring, small bugfixes

* [service] Change dns bypass rule

* [service] Update gitignore

* [service] Remove shim from integration module

* [service] Add DNS packet analyzer

* [service] Add self-check in dns monitor

* [service] Fix go linter errors

* [CI] Add github workflow for the windows core dll

* [service] Minor fixes to the dns monitor
---
 .github/workflows/windows-dll.yml             |  41 ++++
 .gitignore                                    |   1 +
 go.mod                                        |   2 +
 go.sum                                        |   2 +
 service/compat/module.go                      |   1 +
 service/compat/selfcheck.go                   |   6 +
 service/firewall/bypassing.go                 |  30 +++
 service/firewall/dns.go                       |  54 +++--
 .../dnsmonitor/etwlink_windows.go             |  99 +++++++++
 .../interception/dnsmonitor/eventlistener.go  |  19 ++
 .../dnsmonitor/eventlistener_linux.go         | 144 +++++++++++++
 .../dnsmonitor/eventlistener_windows.go       | 103 +++++++++
 .../interception/dnsmonitor/module.go         | 138 ++++++++++++
 .../interception/dnsmonitor/varlinktypes.go   |  83 ++++++++
 service/firewall/interception/nfq/nfq.go      |   2 +-
 .../interception/windowskext/packet.go        |   2 +-
 .../interception/windowskext2/handler.go      |   1 +
 .../interception/windowskext2/packet.go       |  12 +-
 service/firewall/module.go                    |   5 +-
 service/firewall/packet_handler.go            | 104 ++++++++-
 service/instance.go                           |  25 ++-
 service/integration/etw_windows.go            | 114 ++++++++++
 service/integration/integration.go            |  16 ++
 service/integration/integration_windows.go    |  52 +++++
 service/integration/module.go                 |  49 +++++
 service/network/connection.go                 |  33 +++
 service/network/module.go                     |   4 +
 service/network/packet/parse.go               |  61 +++++-
 service/resolver/ipinfo.go                    |  21 ++
 service/resolver/main.go                      |   6 +
 service/resolver/resolver.go                  |  21 +-
 service/resolver/resolvers.go                 |  11 +-
 windows_core_dll/build.ps1                    |   2 +
 windows_core_dll/dllmain.cpp                  | 197 ++++++++++++++++++
 windows_core_dll/framework.h                  |   5 +
 windows_core_dll/pch.cpp                      |   5 +
 windows_core_dll/pch.h                        |  22 ++
 windows_core_dll/windows_core_dll.sln         |  31 +++
 windows_core_dll/windows_core_dll.vcxproj     | 158 ++++++++++++++
 .../windows_core_dll.vcxproj.filters          |  33 +++
 .../windows_core_dll.vcxproj.user             |   4 +
 41 files changed, 1668 insertions(+), 51 deletions(-)
 create mode 100644 .github/workflows/windows-dll.yml
 create mode 100644 service/firewall/interception/dnsmonitor/etwlink_windows.go
 create mode 100644 service/firewall/interception/dnsmonitor/eventlistener.go
 create mode 100644 service/firewall/interception/dnsmonitor/eventlistener_linux.go
 create mode 100644 service/firewall/interception/dnsmonitor/eventlistener_windows.go
 create mode 100644 service/firewall/interception/dnsmonitor/module.go
 create mode 100644 service/firewall/interception/dnsmonitor/varlinktypes.go
 create mode 100644 service/integration/etw_windows.go
 create mode 100644 service/integration/integration.go
 create mode 100644 service/integration/integration_windows.go
 create mode 100644 service/integration/module.go
 create mode 100644 windows_core_dll/build.ps1
 create mode 100644 windows_core_dll/dllmain.cpp
 create mode 100644 windows_core_dll/framework.h
 create mode 100644 windows_core_dll/pch.cpp
 create mode 100644 windows_core_dll/pch.h
 create mode 100644 windows_core_dll/windows_core_dll.sln
 create mode 100644 windows_core_dll/windows_core_dll.vcxproj
 create mode 100644 windows_core_dll/windows_core_dll.vcxproj.filters
 create mode 100644 windows_core_dll/windows_core_dll.vcxproj.user

diff --git a/.github/workflows/windows-dll.yml b/.github/workflows/windows-dll.yml
new file mode 100644
index 00000000..9b2fd723
--- /dev/null
+++ b/.github/workflows/windows-dll.yml
@@ -0,0 +1,41 @@
+name: Windows Portmaster Core DLL
+
+on:
+  push:
+    paths:
+      - 'windows_core_dll/**'
+    branches:
+      - master
+      - develop
+
+  pull_request:
+    paths:
+      - 'windows_core_dll/**'
+    branches:
+      - master
+      - develop
+  workflow_dispatch:
+
+jobs:
+  build:
+    name: Build
+    runs-on: windows-latest
+    steps:
+    - name: Checkout Repository
+      uses: actions/checkout@v4
+    - name: Add msbuild to PATH
+      uses: microsoft/setup-msbuild@v2
+    - name: Build DLL
+      run: msbuild windows_core_dll\windows_core_dll.sln -t:rebuild -property:Configuration=Release
+    - name: Verify DLL
+      shell: powershell
+      run: |
+        if (!(Test-Path "windows_core_dll/x64/Release/portmaster-core.dll")) {
+          Write-Error "DLL build failed: portmaster-core.dll not found"
+          exit 1
+        }
+    - name: Upload artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: portmaster-core-dll
+        path: windows_core_dll/x64/Release/portmaster-core.dll
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e0a6550a..03d8b25f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,4 @@ go.work.sum
 
 # Kext releases
 windows_kext/release/kext_release_*.zip
+windows_core_dll/.vs/windows_core_dll
diff --git a/go.mod b/go.mod
index d40c1410..5f609ef6 100644
--- a/go.mod
+++ b/go.mod
@@ -59,6 +59,7 @@ require (
 	github.com/tidwall/gjson v1.18.0
 	github.com/tidwall/sjson v1.2.5
 	github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
+	github.com/varlink/go v0.4.0
 	github.com/vincent-petithory/dataurl v1.0.0
 	go.etcd.io/bbolt v1.3.11
 	golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
@@ -92,6 +93,7 @@ require (
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/native v1.1.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+	github.com/maruel/panicparse/v2 v2.3.1 // indirect
 	github.com/mdlayher/netlink v1.7.2 // indirect
 	github.com/mdlayher/socket v0.5.1 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
diff --git a/go.sum b/go.sum
index c2bfeae6..13ec98f8 100644
--- a/go.sum
+++ b/go.sum
@@ -313,6 +313,8 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G
 github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
 github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
 github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
+github.com/varlink/go v0.4.0 h1:+/BQoUO9eJK/+MTSHwFcJch7TMsb6N6Dqp6g0qaXXRo=
+github.com/varlink/go v0.4.0/go.mod h1:DKg9Y2ctoNkesREGAEak58l+jOC6JU2aqZvUYs5DynU=
 github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
 github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
diff --git a/service/compat/module.go b/service/compat/module.go
index 5ac97b51..e6781c9b 100644
--- a/service/compat/module.go
+++ b/service/compat/module.go
@@ -181,4 +181,5 @@ func New(instance instance) (*Compat, error) {
 
 type instance interface {
 	NetEnv() *netenv.NetEnv
+	Resolver() *resolver.ResolverModule
 }
diff --git a/service/compat/selfcheck.go b/service/compat/selfcheck.go
index 27efd488..0bbef4e4 100644
--- a/service/compat/selfcheck.go
+++ b/service/compat/selfcheck.go
@@ -158,6 +158,12 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
 
 	// Step 3: Have the nameserver respond with random data in the answer section.
 
+	// Check if the resolver is enabled
+	if module.instance.Resolver().IsDisabled() {
+		// There is no control over the response, there is nothing more that can be checked.
+		return nil, nil
+	}
+
 	// Wait for the reply from the resolver.
 	select {
 	case err := <-dnsCheckLookupError:
diff --git a/service/firewall/bypassing.go b/service/firewall/bypassing.go
index 415fc6c8..4fe9a119 100644
--- a/service/firewall/bypassing.go
+++ b/service/firewall/bypassing.go
@@ -43,8 +43,24 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
 		return endpoints.NoMatch, "", nil
 	}
 
+	// If Portmaster resolver is disabled allow requests going to system dns resolver.
+	// And allow all connections out of the System Resolver.
+	if module.instance.Resolver().IsDisabled() {
+		// TODO(vladimir): Is there a more specific check that can be done?
+		if conn.Process().IsSystemResolver() {
+			return endpoints.NoMatch, "", nil
+		}
+		if conn.Entity.Port == 53 && conn.Entity.IPScope.IsLocalhost() {
+			return endpoints.NoMatch, "", nil
+		}
+	}
+
 	// Block bypass attempts using an (encrypted) DNS server.
 	switch {
+	case looksLikeOutgoingDNSRequest(conn) && module.instance.Resolver().IsDisabled():
+		// Allow. Packet will be analyzed and blocked if its not a dns request, before sent.
+		conn.Inspecting = true
+		return endpoints.NoMatch, "", nil
 	case conn.Entity.Port == 53:
 		return endpoints.Denied,
 			"blocked DNS query, manual dns setup required",
@@ -62,3 +78,17 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
 
 	return endpoints.NoMatch, "", nil
 }
+
+func looksLikeOutgoingDNSRequest(conn *network.Connection) bool {
+	// Outbound on remote port 53, UDP.
+	if conn.Inbound {
+		return false
+	}
+	if conn.Entity.Port != 53 {
+		return false
+	}
+	if conn.IPProtocol != packet.UDP {
+		return false
+	}
+	return true
+}
diff --git a/service/firewall/dns.go b/service/firewall/dns.go
index 9b1a55e5..e3434708 100644
--- a/service/firewall/dns.go
+++ b/service/firewall/dns.go
@@ -287,6 +287,30 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
 		}
 	}
 
+	// Create new record for this IP.
+	record := resolver.ResolvedDomain{
+		Domain:            q.FQDN,
+		Resolver:          rrCache.Resolver,
+		DNSRequestContext: rrCache.ToDNSRequestContext(),
+		Expires:           rrCache.Expires,
+	}
+	// Process CNAMEs
+	record.AddCNAMEs(cnames)
+	// Link connection with cnames.
+	if conn.Type == network.DNSRequest {
+		conn.Entity.CNAME = record.CNAMEs
+	}
+
+	SaveIPsInCache(ips, profileID, record)
+}
+
+// formatRR is a friendlier alternative to miekg/dns.RR.String().
+func formatRR(rr dns.RR) string {
+	return strings.ReplaceAll(rr.String(), "\t", " ")
+}
+
+// SaveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
+func SaveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
 	// Package IPs and CNAMEs into IPInfo structs.
 	for _, ip := range ips {
 		// Never save domain attributions for localhost IPs.
@@ -294,31 +318,6 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
 			continue
 		}
 
-		// Create new record for this IP.
-		record := resolver.ResolvedDomain{
-			Domain:            q.FQDN,
-			Resolver:          rrCache.Resolver,
-			DNSRequestContext: rrCache.ToDNSRequestContext(),
-			Expires:           rrCache.Expires,
-		}
-
-		// Resolve all CNAMEs in the correct order and add the to the record - up to max 50 layers.
-		domain := q.FQDN
-		for range 50 {
-			nextDomain, isCNAME := cnames[domain]
-			if !isCNAME || nextDomain == domain {
-				break
-			}
-
-			record.CNAMEs = append(record.CNAMEs, nextDomain)
-			domain = nextDomain
-		}
-
-		// Update the entity to include the CNAMEs of the query response.
-		conn.Entity.CNAME = record.CNAMEs
-
-		// Check if there is an existing record for this DNS response.
-		// Else create a new one.
 		ipString := ip.String()
 		info, err := resolver.GetIPInfo(profileID, ipString)
 		if err != nil {
@@ -341,8 +340,3 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
 		}
 	}
 }
-
-// formatRR is a friendlier alternative to miekg/dns.RR.String().
-func formatRR(rr dns.RR) string {
-	return strings.ReplaceAll(rr.String(), "\t", " ")
-}
diff --git a/service/firewall/interception/dnsmonitor/etwlink_windows.go b/service/firewall/interception/dnsmonitor/etwlink_windows.go
new file mode 100644
index 00000000..d014bbab
--- /dev/null
+++ b/service/firewall/interception/dnsmonitor/etwlink_windows.go
@@ -0,0 +1,99 @@
+//go:build windows
+// +build windows
+
+package dnsmonitor
+
+import (
+	"fmt"
+	"runtime"
+	"sync"
+	"sync/atomic"
+
+	"github.com/safing/portmaster/service/integration"
+	"golang.org/x/sys/windows"
+)
+
+type ETWSession struct {
+	i integration.ETWFunctions
+
+	shutdownGuard atomic.Bool
+	shutdownMutex sync.Mutex
+
+	state uintptr
+}
+
+// NewSession creates new ETW event listener and initilizes it. This is a low level interface, make sure to call DestorySession when you are done using it.
+func NewSession(etwInterface integration.ETWFunctions, callback func(domain string, result string)) (*ETWSession, error) {
+	etwSession := &ETWSession{
+		i: etwInterface,
+	}
+
+	// Make sure session from previous instances are not running.
+	_ = etwSession.i.StopOldSession()
+
+	// Initialize notification activated callback
+	win32Callback := windows.NewCallback(func(domain *uint16, result *uint16) uintptr {
+		callback(windows.UTF16PtrToString(domain), windows.UTF16PtrToString(result))
+		return 0
+	})
+	// The function only allocates memory it will not fail.
+	etwSession.state = etwSession.i.CreateState(win32Callback)
+
+	// Make sure DestroySession is called even if caller forgets to call it.
+	runtime.SetFinalizer(etwSession, func(s *ETWSession) {
+		_ = s.i.DestroySession(s.state)
+	})
+
+	// Initialize session.
+	err := etwSession.i.InitializeSession(etwSession.state)
+	if err != nil {
+		return nil, fmt.Errorf("failed to initialzie session: %q", err)
+	}
+
+	return etwSession, nil
+}
+
+// StartTrace starts the tracing session of dns events. This is a blocking call. It will not return until the trace is stopped.
+func (l *ETWSession) StartTrace() error {
+	return l.i.StartTrace(l.state)
+}
+
+// IsRunning returns true if DestroySession has NOT been called.
+func (l *ETWSession) IsRunning() bool {
+	return !l.shutdownGuard.Load()
+}
+
+// FlushTrace flushes the trace buffer.
+func (l *ETWSession) FlushTrace() error {
+	l.shutdownMutex.Lock()
+	defer l.shutdownMutex.Unlock()
+
+	// Make sure session is still running.
+	if l.shutdownGuard.Load() {
+		return nil
+	}
+
+	return l.i.FlushTrace(l.state)
+}
+
+// StopTrace stopes the trace. This will cause StartTrace to return.
+func (l *ETWSession) StopTrace() error {
+	return l.i.StopTrace(l.state)
+}
+
+// DestroySession closes the session and frees the allocated memory. Listener cannot be used after this function is called.
+func (l *ETWSession) DestroySession() error {
+	l.shutdownMutex.Lock()
+	defer l.shutdownMutex.Unlock()
+
+	if l.shutdownGuard.Swap(true) {
+		return nil
+	}
+
+	err := l.i.DestroySession(l.state)
+	if err != nil {
+		return err
+	}
+	l.state = 0
+	return nil
+}
diff --git a/service/firewall/interception/dnsmonitor/eventlistener.go b/service/firewall/interception/dnsmonitor/eventlistener.go
new file mode 100644
index 00000000..911130c9
--- /dev/null
+++ b/service/firewall/interception/dnsmonitor/eventlistener.go
@@ -0,0 +1,19 @@
+//go:build !linux && !windows
+// +build !linux,!windows
+
+package dnsmonitor
+
+type Listener struct{}
+
+func newListener(_ *DNSMonitor) (*Listener, error) {
+	return &Listener{}, nil
+}
+
+func (l *Listener) flush() error {
+	// Nothing to flush
+	return nil
+}
+
+func (l *Listener) stop() error {
+	return nil
+}
diff --git a/service/firewall/interception/dnsmonitor/eventlistener_linux.go b/service/firewall/interception/dnsmonitor/eventlistener_linux.go
new file mode 100644
index 00000000..d987a082
--- /dev/null
+++ b/service/firewall/interception/dnsmonitor/eventlistener_linux.go
@@ -0,0 +1,144 @@
+//go:build linux
+// +build linux
+
+package dnsmonitor
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"os"
+
+	"github.com/miekg/dns"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/resolver"
+	"github.com/varlink/go/varlink"
+)
+
+type Listener struct {
+	varlinkConn *varlink.Connection
+}
+
+func newListener(module *DNSMonitor) (*Listener, error) {
+	// Set source of the resolver.
+	ResolverInfo.Source = resolver.ServerSourceSystemd
+
+	// Check if the system has systemd-resolver.
+	_, err := os.Stat("/run/systemd/resolve/io.systemd.Resolve.Monitor")
+	if err != nil {
+		return nil, fmt.Errorf("system does not support systemd resolver monitor")
+	}
+
+	listener := &Listener{}
+
+	restartAttempts := 0
+
+	module.mgr.Go("systemd-resolver-event-listener", func(w *mgr.WorkerCtx) error {
+		// Abort initialization if the connection failed after too many tries.
+		if restartAttempts > 10 {
+			return nil
+		}
+		restartAttempts += 1
+
+		// Initialize varlink connection
+		varlinkConn, err := varlink.NewConnection(module.mgr.Ctx(), "unix:/run/systemd/resolve/io.systemd.Resolve.Monitor")
+		if err != nil {
+			return fmt.Errorf("failed to connect to systemd-resolver varlink service: %w", err)
+		}
+		defer func() {
+			if varlinkConn != nil {
+				err = varlinkConn.Close()
+				if err != nil {
+					log.Errorf("dnsmonitor: failed to close varlink connection: %s", err)
+				}
+			}
+		}()
+
+		listener.varlinkConn = varlinkConn
+		// Subscribe to the dns query events
+		receive, err := listener.varlinkConn.Send(w.Ctx(), "io.systemd.Resolve.Monitor.SubscribeQueryResults", nil, varlink.More)
+		if err != nil {
+			var varlinkErr *varlink.Error
+			if errors.As(err, &varlinkErr) {
+				return fmt.Errorf("failed to issue Varlink call: %+v", varlinkErr.Parameters)
+			} else {
+				return fmt.Errorf("failed to issue Varlink call: %w", err)
+			}
+		}
+
+		for {
+			queryResult := QueryResult{}
+			// Receive the next event from the resolver.
+			flags, err := receive(w.Ctx(), &queryResult)
+			if err != nil {
+				var varlinkErr *varlink.Error
+				if errors.As(err, &varlinkErr) {
+					return fmt.Errorf("failed to receive Varlink reply: %+v", varlinkErr.Parameters)
+				} else {
+					return fmt.Errorf("failed to receive Varlink reply: %w", err)
+				}
+			}
+
+			// Check if the reply indicates the end of the stream
+			if flags&varlink.Continues == 0 {
+				break
+			}
+
+			// Ignore if there is no question.
+			if queryResult.Question == nil || len(*queryResult.Question) == 0 {
+				continue
+			}
+
+			// Protmaster self check
+			domain := (*queryResult.Question)[0].Name
+			if processIfSelfCheckDomain(dns.Fqdn(domain)) {
+				// Not need to process result.
+				continue
+			}
+
+			if queryResult.Rcode != nil {
+				continue // Ignore DNS errors
+			}
+
+			listener.processAnswer(domain, &queryResult)
+		}
+		return nil
+	})
+	return listener, nil
+}
+
+func (l *Listener) flush() error {
+	// Nothing to flush
+	return nil
+}
+
+func (l *Listener) stop() error {
+	return nil
+}
+
+func (l *Listener) processAnswer(domain string, queryResult *QueryResult) {
+	// Allocated data struct for the parsed result.
+	cnames := make(map[string]string)
+	ips := make([]net.IP, 0, 5)
+
+	// Check if the query is valid
+	if queryResult.Answer == nil {
+		return
+	}
+
+	// Go trough each answer entry.
+	for _, a := range *queryResult.Answer {
+		if a.RR.Address != nil {
+			ip := net.IP(*a.RR.Address)
+			// Answer contains ip address.
+			ips = append(ips, ip)
+
+		} else if a.RR.Name != nil {
+			// Answer is a CNAME.
+			cnames[domain] = *a.RR.Name
+		}
+	}
+
+	saveDomain(domain, ips, cnames)
+}
diff --git a/service/firewall/interception/dnsmonitor/eventlistener_windows.go b/service/firewall/interception/dnsmonitor/eventlistener_windows.go
new file mode 100644
index 00000000..b6a39fd8
--- /dev/null
+++ b/service/firewall/interception/dnsmonitor/eventlistener_windows.go
@@ -0,0 +1,103 @@
+//go:build windows
+// +build windows
+
+package dnsmonitor
+
+import (
+	"fmt"
+	"net"
+	"strconv"
+	"strings"
+
+	"github.com/miekg/dns"
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/resolver"
+)
+
+type Listener struct {
+	etw *ETWSession
+}
+
+func newListener(module *DNSMonitor) (*Listener, error) {
+	// Set source of the resolver.
+	ResolverInfo.Source = resolver.ServerSourceETW
+
+	listener := &Listener{}
+	var err error
+	// Initialize new dns event session.
+	listener.etw, err = NewSession(module.instance.OSIntegration().GetETWInterface(), listener.processEvent)
+	if err != nil {
+		return nil, err
+	}
+
+	// Start listening for events.
+	module.mgr.Go("etw-dns-event-listener", func(w *mgr.WorkerCtx) error {
+		return listener.etw.StartTrace()
+	})
+
+	return listener, nil
+}
+
+func (l *Listener) flush() error {
+	return l.etw.FlushTrace()
+}
+
+func (l *Listener) stop() error {
+	if l == nil {
+		return fmt.Errorf("listener is nil")
+	}
+	if l.etw == nil {
+		return fmt.Errorf("invalid etw session")
+	}
+	// Stop and destroy trace. Destroy should be called even if stop fails for some reason.
+	err := l.etw.StopTrace()
+	err2 := l.etw.DestroySession()
+
+	if err != nil {
+		return fmt.Errorf("StopTrace failed: %w", err)
+	}
+
+	if err2 != nil {
+		return fmt.Errorf("DestroySession failed: %w", err2)
+	}
+	return nil
+}
+
+func (l *Listener) processEvent(domain string, result string) {
+	if processIfSelfCheckDomain(dns.Fqdn(domain)) {
+		// Not need to process result.
+		return
+	}
+
+	// Ignore empty results
+	if len(result) == 0 {
+		return
+	}
+
+	cnames := make(map[string]string)
+	ips := []net.IP{}
+
+	resultArray := strings.Split(result, ";")
+	for _, r := range resultArray {
+		// For results other than IP addresses, the string starts with "type:"
+		if strings.HasPrefix(r, "type:") {
+			dnsValueArray := strings.Split(r, " ")
+			if len(dnsValueArray) < 3 {
+				continue
+			}
+
+			// Ignore everything except CNAME records
+			if value, err := strconv.ParseInt(dnsValueArray[1], 10, 16); err == nil && value == int64(dns.TypeCNAME) {
+				cnames[domain] = dnsValueArray[2]
+			}
+
+		} else {
+			// If the event doesn't start with "type:", it's an IP address
+			ip := net.ParseIP(r)
+			if ip != nil {
+				ips = append(ips, ip)
+			}
+		}
+	}
+	saveDomain(domain, ips, cnames)
+}
diff --git a/service/firewall/interception/dnsmonitor/module.go b/service/firewall/interception/dnsmonitor/module.go
new file mode 100644
index 00000000..eed8be11
--- /dev/null
+++ b/service/firewall/interception/dnsmonitor/module.go
@@ -0,0 +1,138 @@
+package dnsmonitor
+
+import (
+	"errors"
+	"net"
+	"strings"
+
+	"github.com/miekg/dns"
+	"github.com/safing/portmaster/base/database"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/service/compat"
+	"github.com/safing/portmaster/service/integration"
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/network/netutils"
+	"github.com/safing/portmaster/service/resolver"
+)
+
+var ResolverInfo = resolver.ResolverInfo{
+	Name: "SystemResolver",
+	Type: resolver.ServerTypeMonitor,
+}
+
+type DNSMonitor struct {
+	instance instance
+	mgr      *mgr.Manager
+
+	listener *Listener
+}
+
+// Manager returns the module manager.
+func (dl *DNSMonitor) Manager() *mgr.Manager {
+	return dl.mgr
+}
+
+// Start starts the module.
+func (dl *DNSMonitor) Start() error {
+	// Initialize dns event listener
+	var err error
+	dl.listener, err = newListener(dl)
+	if err != nil {
+		log.Errorf("dnsmonitor: failed to start dns listener: %s", err)
+	}
+
+	return nil
+}
+
+// Stop stops the module.
+func (dl *DNSMonitor) Stop() error {
+	if dl.listener != nil {
+		err := dl.listener.stop()
+		if err != nil {
+			log.Errorf("dnsmonitor: failed to close listener: %s", err)
+		}
+	}
+	return nil
+}
+
+// Flush flushes the buffer forcing all events to be processed.
+func (dl *DNSMonitor) Flush() error {
+	return dl.listener.flush()
+}
+
+func saveDomain(domain string, ips []net.IP, cnames map[string]string) {
+	fqdn := dns.Fqdn(domain)
+	// Create new record for this IP.
+	record := resolver.ResolvedDomain{
+		Domain:            fqdn,
+		Resolver:          &ResolverInfo,
+		DNSRequestContext: &resolver.DNSRequestContext{},
+		Expires:           0,
+	}
+
+	// Process cnames
+	record.AddCNAMEs(cnames)
+
+	// Add to cache
+	saveIPsInCache(ips, resolver.IPInfoProfileScopeGlobal, record)
+}
+
+func New(instance instance) (*DNSMonitor, error) {
+	// Initialize module
+	m := mgr.New("DNSMonitor")
+	module := &DNSMonitor{
+		mgr:      m,
+		instance: instance,
+	}
+
+	return module, nil
+}
+
+type instance interface {
+	OSIntegration() *integration.OSIntegration
+}
+
+func processIfSelfCheckDomain(fqdn string) bool {
+	// Check for compat check dns request.
+	if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
+		subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
+		_ = compat.SubmitDNSCheckDomain(subdomain)
+		log.Infof("dnsmonitor: self-check domain received")
+		// No need to parse the answer.
+		return true
+	}
+
+	return false
+}
+
+// saveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
+func saveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
+	// Package IPs and CNAMEs into IPInfo structs.
+	for _, ip := range ips {
+		// Never save domain attributions for localhost IPs.
+		if netutils.GetIPScope(ip) == netutils.HostLocal {
+			continue
+		}
+
+		ipString := ip.String()
+		info, err := resolver.GetIPInfo(profileID, ipString)
+		if err != nil {
+			if !errors.Is(err, database.ErrNotFound) {
+				log.Errorf("dnsmonitor: failed to search for IP info record: %s", err)
+			}
+
+			info = &resolver.IPInfo{
+				IP:        ipString,
+				ProfileID: profileID,
+			}
+		}
+
+		// Add the new record to the resolved domains for this IP and scope.
+		info.AddDomain(record)
+
+		// Save if the record is new or has been updated.
+		if err := info.Save(); err != nil {
+			log.Errorf("dnsmonitor: failed to save IP info record: %s", err)
+		}
+	}
+}
diff --git a/service/firewall/interception/dnsmonitor/varlinktypes.go b/service/firewall/interception/dnsmonitor/varlinktypes.go
new file mode 100644
index 00000000..3021ac18
--- /dev/null
+++ b/service/firewall/interception/dnsmonitor/varlinktypes.go
@@ -0,0 +1,83 @@
+//go:build linux
+// +build linux
+
+package dnsmonitor
+
+// List of struct that define the systemd-resolver varlink dns event protocol.
+// Source: `sudo varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve.Monitor io.systemd.Resolve.Monitor`
+
+type ResourceKey struct {
+	Class int    `json:"class"`
+	Type  int    `json:"type"`
+	Name  string `json:"name"`
+}
+
+type ResourceRecord struct {
+	Key     ResourceKey `json:"key"`
+	Name    *string     `json:"name,omitempty"`
+	Address *[]byte     `json:"address,omitempty"`
+	// Rest of the fields are not used.
+	// Priority     *int        `json:"priority,omitempty"`
+	// Weight       *int        `json:"weight,omitempty"`
+	// Port         *int        `json:"port,omitempty"`
+	// CPU          *string     `json:"cpu,omitempty"`
+	// OS           *string     `json:"os,omitempty"`
+	// Items        *[]string   `json:"items,omitempty"`
+	// MName        *string     `json:"mname,omitempty"`
+	// RName        *string     `json:"rname,omitempty"`
+	// Serial       *int        `json:"serial,omitempty"`
+	// Refresh      *int        `json:"refresh,omitempty"`
+	// Expire       *int        `json:"expire,omitempty"`
+	// Minimum      *int        `json:"minimum,omitempty"`
+	// Exchange     *string     `json:"exchange,omitempty"`
+	// Version      *int        `json:"version,omitempty"`
+	// Size         *int        `json:"size,omitempty"`
+	// HorizPre     *int        `json:"horiz_pre,omitempty"`
+	// VertPre      *int        `json:"vert_pre,omitempty"`
+	// Latitude     *int        `json:"latitude,omitempty"`
+	// Longitude    *int        `json:"longitude,omitempty"`
+	// Altitude     *int        `json:"altitude,omitempty"`
+	// KeyTag       *int        `json:"key_tag,omitempty"`
+	// Algorithm    *int        `json:"algorithm,omitempty"`
+	// DigestType   *int        `json:"digest_type,omitempty"`
+	// Digest       *string     `json:"digest,omitempty"`
+	// FPType       *int        `json:"fptype,omitempty"`
+	// Fingerprint  *string     `json:"fingerprint,omitempty"`
+	// Flags        *int        `json:"flags,omitempty"`
+	// Protocol     *int        `json:"protocol,omitempty"`
+	// DNSKey       *string     `json:"dnskey,omitempty"`
+	// Signer       *string     `json:"signer,omitempty"`
+	// TypeCovered  *int        `json:"type_covered,omitempty"`
+	// Labels       *int        `json:"labels,omitempty"`
+	// OriginalTTL  *int        `json:"original_ttl,omitempty"`
+	// Expiration   *int        `json:"expiration,omitempty"`
+	// Inception    *int        `json:"inception,omitempty"`
+	// Signature    *string     `json:"signature,omitempty"`
+	// NextDomain   *string     `json:"next_domain,omitempty"`
+	// Types        *[]int      `json:"types,omitempty"`
+	// Iterations   *int        `json:"iterations,omitempty"`
+	// Salt         *string     `json:"salt,omitempty"`
+	// Hash         *string     `json:"hash,omitempty"`
+	// CertUsage    *int        `json:"cert_usage,omitempty"`
+	// Selector     *int        `json:"selector,omitempty"`
+	// MatchingType *int        `json:"matching_type,omitempty"`
+	// Data         *string     `json:"data,omitempty"`
+	// Tag          *string     `json:"tag,omitempty"`
+	// Value        *string     `json:"value,omitempty"`
+}
+
+type Answer struct {
+	RR      *ResourceRecord `json:"rr,omitempty"`
+	Raw     string          `json:"raw"`
+	IfIndex *int            `json:"ifindex,omitempty"`
+}
+
+type QueryResult struct {
+	Ready              *bool          `json:"ready,omitempty"`
+	State              *string        `json:"state,omitempty"`
+	Rcode              *int           `json:"rcode,omitempty"`
+	Errno              *int           `json:"errno,omitempty"`
+	Question           *[]ResourceKey `json:"question,omitempty"`
+	CollectedQuestions *[]ResourceKey `json:"collectedQuestions,omitempty"`
+	Answer             *[]Answer      `json:"answer,omitempty"`
+}
diff --git a/service/firewall/interception/nfq/nfq.go b/service/firewall/interception/nfq/nfq.go
index 22a5b390..397d1857 100644
--- a/service/firewall/interception/nfq/nfq.go
+++ b/service/firewall/interception/nfq/nfq.go
@@ -188,7 +188,7 @@ func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
 			return 0
 		}
 
-		if err := pmpacket.Parse(*attrs.Payload, &pkt.Base); err != nil {
+		if err := pmpacket.ParseLayer3(*attrs.Payload, &pkt.Base); err != nil {
 			log.Warningf("nfqueue: failed to parse payload: %s", err)
 			_ = pkt.Drop()
 			return 0
diff --git a/service/firewall/interception/windowskext/packet.go b/service/firewall/interception/windowskext/packet.go
index 5942d7d9..9145926c 100644
--- a/service/firewall/interception/windowskext/packet.go
+++ b/service/firewall/interception/windowskext/packet.go
@@ -59,7 +59,7 @@ func (pkt *Packet) LoadPacketData() error {
 			return packet.ErrFailedToLoadPayload
 		}
 
-		err = packet.Parse(payload, &pkt.Base)
+		err = packet.ParseLayer3(payload, &pkt.Base)
 		if err != nil {
 			log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
 			return packet.ErrFailedToLoadPayload
diff --git a/service/firewall/interception/windowskext2/handler.go b/service/firewall/interception/windowskext2/handler.go
index 57f74c71..d144fa63 100644
--- a/service/firewall/interception/windowskext2/handler.go
+++ b/service/firewall/interception/windowskext2/handler.go
@@ -55,6 +55,7 @@ func Handler(ctx context.Context, packets chan packet.Packet, bandwidthUpdate ch
 				newPacket := &Packet{
 					verdictRequest: conn.ID,
 					payload:        conn.Payload,
+					payloadLayer:   conn.PayloadLayer,
 					verdictSet:     abool.NewBool(false),
 				}
 				info := newPacket.Info()
diff --git a/service/firewall/interception/windowskext2/packet.go b/service/firewall/interception/windowskext2/packet.go
index 52a7a2a7..00d95036 100644
--- a/service/firewall/interception/windowskext2/packet.go
+++ b/service/firewall/interception/windowskext2/packet.go
@@ -4,6 +4,7 @@
 package windowskext
 
 import (
+	"fmt"
 	"sync"
 
 	"github.com/tevino/abool"
@@ -19,6 +20,7 @@ type Packet struct {
 
 	verdictRequest uint64
 	payload        []byte
+	payloadLayer   uint8
 	verdictSet     *abool.AtomicBool
 
 	payloadLoaded bool
@@ -51,7 +53,15 @@ func (pkt *Packet) LoadPacketData() error {
 		pkt.payloadLoaded = true
 
 		if len(pkt.payload) > 0 {
-			err := packet.Parse(pkt.payload, &pkt.Base)
+			var err error
+			switch pkt.payloadLayer {
+			case 3:
+				err = packet.ParseLayer3(pkt.payload, &pkt.Base)
+			case 4:
+				err = packet.ParseLayer4(pkt.payload, &pkt.Base)
+			default:
+				err = fmt.Errorf("unsupported payload layer: %d", pkt.payloadLayer)
+			}
 			if err != nil {
 				log.Tracef("payload: %#v", pkt.payload)
 				log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
diff --git a/service/firewall/module.go b/service/firewall/module.go
index 131d4cac..2ac87de2 100644
--- a/service/firewall/module.go
+++ b/service/firewall/module.go
@@ -16,6 +16,7 @@ import (
 	"github.com/safing/portmaster/service/netquery"
 	"github.com/safing/portmaster/service/network"
 	"github.com/safing/portmaster/service/profile"
+	"github.com/safing/portmaster/service/resolver"
 	"github.com/safing/portmaster/spn/access"
 	"github.com/safing/portmaster/spn/captain"
 )
@@ -34,8 +35,7 @@ func (ss *stringSliceFlag) Set(value string) error {
 var allowedClients stringSliceFlag
 
 type Firewall struct {
-	mgr *mgr.Manager
-
+	mgr      *mgr.Manager
 	instance instance
 }
 
@@ -165,4 +165,5 @@ type instance interface {
 	Access() *access.Access
 	Network() *network.Network
 	NetQuery() *netquery.NetQuery
+	Resolver() *resolver.ResolverModule
 }
diff --git a/service/firewall/packet_handler.go b/service/firewall/packet_handler.go
index a290182f..c0e59cb7 100644
--- a/service/firewall/packet_handler.go
+++ b/service/firewall/packet_handler.go
@@ -6,10 +6,12 @@ import (
 	"fmt"
 	"net"
 	"os"
+	"strings"
 	"sync/atomic"
 	"time"
 
 	"github.com/google/gopacket/layers"
+	"github.com/miekg/dns"
 	"github.com/tevino/abool"
 
 	"github.com/safing/portmaster/base/log"
@@ -23,6 +25,7 @@ import (
 	"github.com/safing/portmaster/service/network/netutils"
 	"github.com/safing/portmaster/service/network/packet"
 	"github.com/safing/portmaster/service/process"
+	"github.com/safing/portmaster/service/resolver"
 	"github.com/safing/portmaster/spn/access"
 )
 
@@ -444,8 +447,9 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
 		filterConnection = false
 		log.Tracer(pkt.Ctx()).Infof("filter: granting own pre-authenticated connection %s", conn)
 
-		// Redirect outbound DNS packets if enabled,
+	// Redirect outbound DNS packets if enabled,
 	case dnsQueryInterception() &&
+		!module.instance.Resolver().IsDisabled() &&
 		pkt.IsOutbound() &&
 		pkt.Info().DstPort == 53 &&
 		// that don't match the address of our nameserver,
@@ -478,11 +482,13 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
 
 	// Decide how to continue handling connection.
 	switch {
+	case conn.Inspecting && looksLikeOutgoingDNSRequest(conn):
+		inspectDNSPacket(conn, pkt)
+		conn.UpdateFirewallHandler(inspectDNSPacket)
 	case conn.Inspecting:
 		log.Tracer(pkt.Ctx()).Trace("filter: start inspecting")
 		conn.UpdateFirewallHandler(inspectAndVerdictHandler)
 		inspectAndVerdictHandler(conn, pkt)
-
 	default:
 		conn.StopFirewallHandler()
 		verdictHandler(conn, pkt)
@@ -506,7 +512,7 @@ func FilterConnection(ctx context.Context, conn *network.Connection, pkt packet.
 	}
 
 	// TODO: Enable inspection framework again.
-	conn.Inspecting = false
+	// conn.Inspecting = false
 
 	// TODO: Quick fix for the SPN.
 	// Use inspection framework for proper encryption detection.
@@ -580,6 +586,98 @@ func inspectAndVerdictHandler(conn *network.Connection, pkt packet.Packet) {
 	issueVerdict(conn, pkt, 0, true)
 }
 
+func inspectDNSPacket(conn *network.Connection, pkt packet.Packet) {
+	// Ignore info-only packets in this handler.
+	if pkt.InfoOnly() {
+		return
+	}
+
+	dnsPacket := new(dns.Msg)
+	err := pkt.LoadPacketData()
+	if err != nil {
+		_ = pkt.Block()
+		log.Errorf("filter: failed to load packet payload: %s", err)
+		return
+	}
+
+	// Parse and block invalid packets.
+	err = dnsPacket.Unpack(pkt.Payload())
+	if err != nil {
+		err = pkt.PermanentBlock()
+		if err != nil {
+			log.Errorf("filter: failed to block packet: %s", err)
+		}
+		_ = conn.SetVerdict(network.VerdictBlock, "none DNS data on DNS port", "", nil)
+		conn.VerdictPermanent = true
+		conn.Save()
+		return
+	}
+
+	// Packet was parsed.
+	// Allow it but only after the answer was added to the cache.
+	defer func() {
+		err = pkt.Accept()
+		if err != nil {
+			log.Errorf("filter: failed to accept dns packet: %s", err)
+		}
+	}()
+
+	// Check if packet has a question.
+	if len(dnsPacket.Question) == 0 {
+		return
+	}
+
+	// Read create structs with the needed data.
+	question := dnsPacket.Question[0]
+	fqdn := dns.Fqdn(question.Name)
+
+	// Check for compat check dns request.
+	if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
+		subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
+		_ = compat.SubmitDNSCheckDomain(subdomain)
+		log.Infof("packet_handler: self-check domain received")
+		// No need to parse the answer.
+		return
+	}
+
+	// Check if there is an answer.
+	if len(dnsPacket.Answer) == 0 {
+		return
+	}
+
+	resolverInfo := &resolver.ResolverInfo{
+		Name:    "DNSRequestObserver",
+		Type:    resolver.ServerTypeFirewall,
+		Source:  resolver.ServerSourceFirewall,
+		IP:      conn.Entity.IP,
+		Domain:  conn.Entity.Domain,
+		IPScope: conn.Entity.IPScope,
+	}
+
+	rrCache := &resolver.RRCache{
+		Domain:   fqdn,
+		Question: dns.Type(question.Qtype),
+		RCode:    dnsPacket.Rcode,
+		Answer:   dnsPacket.Answer,
+		Ns:       dnsPacket.Ns,
+		Extra:    dnsPacket.Extra,
+		Resolver: resolverInfo,
+	}
+
+	query := &resolver.Query{
+		FQDN:               fqdn,
+		QType:              dns.Type(question.Qtype),
+		NoCaching:          false,
+		IgnoreFailing:      false,
+		LocalResolversOnly: false,
+		ICANNSpace:         false,
+		DomainRoot:         "",
+	}
+
+	// Save to cache
+	UpdateIPsAndCNAMEs(query, rrCache, conn)
+}
+
 func icmpFilterHandler(conn *network.Connection, pkt packet.Packet) {
 	// Load packet data.
 	err := pkt.LoadPacketData()
diff --git a/service/instance.go b/service/instance.go
index ad6e9dab..ee482605 100644
--- a/service/instance.go
+++ b/service/instance.go
@@ -19,6 +19,8 @@ import (
 	"github.com/safing/portmaster/service/core/base"
 	"github.com/safing/portmaster/service/firewall"
 	"github.com/safing/portmaster/service/firewall/interception"
+	"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
+	"github.com/safing/portmaster/service/integration"
 	"github.com/safing/portmaster/service/intel/customlists"
 	"github.com/safing/portmaster/service/intel/filterlists"
 	"github.com/safing/portmaster/service/intel/geoip"
@@ -65,6 +67,7 @@ type Instance struct {
 
 	core         *core.Core
 	updates      *updates.Updates
+	integration  *integration.OSIntegration
 	geoip        *geoip.GeoIP
 	netenv       *netenv.NetEnv
 	ui           *ui.UI
@@ -74,6 +77,7 @@ type Instance struct {
 	firewall     *firewall.Firewall
 	filterLists  *filterlists.FilterLists
 	interception *interception.Interception
+	dnsmonitor   *dnsmonitor.DNSMonitor
 	customlist   *customlists.CustomList
 	status       *status.Status
 	broadcasts   *broadcasts.Broadcasts
@@ -107,7 +111,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 	instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
 
 	var err error
-
 	// Base modules
 	instance.base, err = base.New(instance)
 	if err != nil {
@@ -151,6 +154,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 	if err != nil {
 		return instance, fmt.Errorf("create updates module: %w", err)
 	}
+	instance.integration, err = integration.New(instance)
+	if err != nil {
+		return instance, fmt.Errorf("create integration module: %w", err)
+	}
 	instance.geoip, err = geoip.New(instance)
 	if err != nil {
 		return instance, fmt.Errorf("create customlist module: %w", err)
@@ -187,6 +194,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 	if err != nil {
 		return instance, fmt.Errorf("create interception module: %w", err)
 	}
+	instance.dnsmonitor, err = dnsmonitor.New(instance)
+	if err != nil {
+		return instance, fmt.Errorf("create dns-listener module: %w", err)
+	}
 	instance.customlist, err = customlists.New(instance)
 	if err != nil {
 		return instance, fmt.Errorf("create customlist module: %w", err)
@@ -275,6 +286,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 
 		instance.core,
 		instance.updates,
+		instance.integration,
 		instance.geoip,
 		instance.netenv,
 
@@ -288,6 +300,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 		instance.filterLists,
 		instance.customlist,
 		instance.interception,
+		instance.dnsmonitor,
 
 		instance.compat,
 		instance.status,
@@ -378,6 +391,11 @@ func (i *Instance) Updates() *updates.Updates {
 	return i.updates
 }
 
+// OSIntegration returns the integration module.
+func (i *Instance) OSIntegration() *integration.OSIntegration {
+	return i.integration
+}
+
 // GeoIP returns the geoip module.
 func (i *Instance) GeoIP() *geoip.GeoIP {
 	return i.geoip
@@ -463,6 +481,11 @@ func (i *Instance) Interception() *interception.Interception {
 	return i.interception
 }
 
+// DNSMonitor returns the dns-listener module.
+func (i *Instance) DNSMonitor() *dnsmonitor.DNSMonitor {
+	return i.dnsmonitor
+}
+
 // CustomList returns the customlist module.
 func (i *Instance) CustomList() *customlists.CustomList {
 	return i.customlist
diff --git a/service/integration/etw_windows.go b/service/integration/etw_windows.go
new file mode 100644
index 00000000..eac3ad8f
--- /dev/null
+++ b/service/integration/etw_windows.go
@@ -0,0 +1,114 @@
+//go:build windows
+// +build windows
+
+package integration
+
+import (
+	"fmt"
+
+	"golang.org/x/sys/windows"
+)
+
+type ETWFunctions struct {
+	createState       *windows.Proc
+	initializeSession *windows.Proc
+	startTrace        *windows.Proc
+	flushTrace        *windows.Proc
+	stopTrace         *windows.Proc
+	destroySession    *windows.Proc
+	stopOldSession    *windows.Proc
+}
+
+func initializeETW(dll *windows.DLL) (ETWFunctions, error) {
+	var functions ETWFunctions
+	var err error
+	functions.createState, err = dll.FindProc("PM_ETWCreateState")
+	if err != nil {
+		return functions, fmt.Errorf("failed to load function PM_ETWCreateState: %q", err)
+	}
+	functions.initializeSession, err = dll.FindProc("PM_ETWInitializeSession")
+	if err != nil {
+		return functions, fmt.Errorf("failed to load function PM_ETWInitializeSession: %q", err)
+	}
+	functions.startTrace, err = dll.FindProc("PM_ETWStartTrace")
+	if err != nil {
+		return functions, fmt.Errorf("failed to load function PM_ETWStartTrace: %q", err)
+	}
+	functions.flushTrace, err = dll.FindProc("PM_ETWFlushTrace")
+	if err != nil {
+		return functions, fmt.Errorf("failed to load function PM_ETWFlushTrace: %q", err)
+	}
+	functions.stopTrace, err = dll.FindProc("PM_ETWStopTrace")
+	if err != nil {
+		return functions, fmt.Errorf("failed to load function PM_ETWStopTrace: %q", err)
+	}
+	functions.destroySession, err = dll.FindProc("PM_ETWDestroySession")
+	if err != nil {
+		return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
+	}
+	functions.stopOldSession, err = dll.FindProc("PM_ETWStopOldSession")
+	if err != nil {
+		return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
+	}
+	return functions, nil
+}
+
+// CreateState calls the dll createState C function.
+func (etw ETWFunctions) CreateState(callback uintptr) uintptr {
+	state, _, _ := etw.createState.Call(callback)
+	return state
+}
+
+// InitializeSession calls the dll initializeSession C function.
+func (etw ETWFunctions) InitializeSession(state uintptr) error {
+	rc, _, _ := etw.initializeSession.Call(state)
+	if rc != 0 {
+		return fmt.Errorf("failed with status code: %d", rc)
+	}
+	return nil
+}
+
+// StartTrace calls the dll startTrace C function.
+func (etw ETWFunctions) StartTrace(state uintptr) error {
+	rc, _, _ := etw.startTrace.Call(state)
+	if rc != 0 {
+		return fmt.Errorf("failed with status code: %d", rc)
+	}
+	return nil
+}
+
+// FlushTrace calls the dll flushTrace C function.
+func (etw ETWFunctions) FlushTrace(state uintptr) error {
+	rc, _, _ := etw.flushTrace.Call(state)
+	if rc != 0 {
+		return fmt.Errorf("failed with status code: %d", rc)
+	}
+	return nil
+}
+
+// StopTrace calls the dll stopTrace C function.
+func (etw ETWFunctions) StopTrace(state uintptr) error {
+	rc, _, _ := etw.stopTrace.Call(state)
+	if rc != 0 {
+		return fmt.Errorf("failed with status code: %d", rc)
+	}
+	return nil
+}
+
+// DestroySession calls the dll destroySession C function.
+func (etw ETWFunctions) DestroySession(state uintptr) error {
+	rc, _, _ := etw.destroySession.Call(state)
+	if rc != 0 {
+		return fmt.Errorf("failed with status code: %d", rc)
+	}
+	return nil
+}
+
+// StopOldSession calls the dll stopOldSession C function.
+func (etw ETWFunctions) StopOldSession() error {
+	rc, _, _ := etw.stopOldSession.Call()
+	if rc != 0 {
+		return fmt.Errorf("failed with status code: %d", rc)
+	}
+	return nil
+}
diff --git a/service/integration/integration.go b/service/integration/integration.go
new file mode 100644
index 00000000..2189b152
--- /dev/null
+++ b/service/integration/integration.go
@@ -0,0 +1,16 @@
+//go:build !windows
+// +build !windows
+
+package integration
+
+type OSSpecific struct{}
+
+// Initialize is empty on any OS different then Windows.
+func (i *OSIntegration) Initialize() error {
+	return nil
+}
+
+// CleanUp releases any resourses allocated during initializaion.
+func (i *OSIntegration) CleanUp() error {
+	return nil
+}
diff --git a/service/integration/integration_windows.go b/service/integration/integration_windows.go
new file mode 100644
index 00000000..786d6da6
--- /dev/null
+++ b/service/integration/integration_windows.go
@@ -0,0 +1,52 @@
+//go:build windows
+// +build windows
+
+package integration
+
+import (
+	"fmt"
+
+	"github.com/safing/portmaster/service/updates"
+	"golang.org/x/sys/windows"
+)
+
+type OSSpecific struct {
+	dll          *windows.DLL
+	etwFunctions ETWFunctions
+}
+
+// Initialize loads the dll and finds all the needed functions from it.
+func (i *OSIntegration) Initialize() error {
+	// Find path to the dll.
+	file, err := updates.GetFile("portmaster-core.dll")
+	if err != nil {
+		return err
+	}
+
+	// Load the DLL.
+	i.os.dll, err = windows.LoadDLL(file.Path())
+	if err != nil {
+		return fmt.Errorf("failed to load dll: %q", err)
+	}
+
+	// Enumerate all needed dll functions.
+	i.os.etwFunctions, err = initializeETW(i.os.dll)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// CleanUp releases any resourses allocated during initializaion.
+func (i *OSIntegration) CleanUp() error {
+	if i.os.dll != nil {
+		return i.os.dll.Release()
+	}
+	return nil
+}
+
+// GetETWInterface return struct containing all the ETW related functions.
+func (i *OSIntegration) GetETWInterface() ETWFunctions {
+	return i.os.etwFunctions
+}
diff --git a/service/integration/module.go b/service/integration/module.go
new file mode 100644
index 00000000..0e43798a
--- /dev/null
+++ b/service/integration/module.go
@@ -0,0 +1,49 @@
+package integration
+
+import (
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/updates"
+)
+
+// OSIntegration module provides special integration with the OS.
+type OSIntegration struct {
+	m      *mgr.Manager
+	states *mgr.StateMgr
+
+	//nolint:unused
+	os OSSpecific
+
+	instance instance
+}
+
+// New returns a new OSIntegration module.
+func New(instance instance) (*OSIntegration, error) {
+	m := mgr.New("OSIntegration")
+	module := &OSIntegration{
+		m:      m,
+		states: m.NewStateMgr(),
+
+		instance: instance,
+	}
+
+	return module, nil
+}
+
+// Manager returns the module manager.
+func (i *OSIntegration) Manager() *mgr.Manager {
+	return i.m
+}
+
+// Start starts the module.
+func (i *OSIntegration) Start() error {
+	return i.Initialize()
+}
+
+// Stop stops the module.
+func (i *OSIntegration) Stop() error {
+	return i.CleanUp()
+}
+
+type instance interface {
+	Updates() *updates.Updates
+}
diff --git a/service/network/connection.go b/service/network/connection.go
index 7ea96400..b3dd70fc 100644
--- a/service/network/connection.go
+++ b/service/network/connection.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"runtime"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -18,6 +19,7 @@ import (
 	"github.com/safing/portmaster/service/netenv"
 	"github.com/safing/portmaster/service/network/netutils"
 	"github.com/safing/portmaster/service/network/packet"
+	"github.com/safing/portmaster/service/network/reference"
 	"github.com/safing/portmaster/service/process"
 	_ "github.com/safing/portmaster/service/process/tags"
 	"github.com/safing/portmaster/service/resolver"
@@ -542,6 +544,23 @@ func (conn *Connection) GatherConnectionInfo(pkt packet.Packet) (err error) {
 			// Try again with the global scope, in case DNS went through the system resolver.
 			ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
 		}
+
+		if runtime.GOOS == "windows" && err != nil {
+			// On windows domains may come with delay.
+			if module.instance.Resolver().IsDisabled() && conn.shouldWaitForDomain() {
+				// Flush the dns listener buffer and try again.
+				for i := range 4 {
+					_ = module.instance.DNSMonitor().Flush()
+					ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
+					if err == nil {
+						log.Tracer(pkt.Ctx()).Debugf("network: found domain from dnsmonitor after %d tries", i+1)
+						break
+					}
+					time.Sleep(5 * time.Millisecond)
+				}
+			}
+		}
+
 		if err == nil {
 			lastResolvedDomain := ipinfo.MostRecentDomain()
 			if lastResolvedDomain != nil {
@@ -869,3 +888,17 @@ func (conn *Connection) String() string {
 		return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
 	}
 }
+
+func (conn *Connection) shouldWaitForDomain() bool {
+	// Should wait for Global Unicast, outgoing and not ICMP connections
+	switch {
+	case conn.Entity.IPScope != netutils.Global:
+		return false
+	case conn.Inbound:
+		return false
+	case reference.IsICMP(conn.Entity.Protocol):
+		return false
+	}
+
+	return true
+}
diff --git a/service/network/module.go b/service/network/module.go
index 4cab1cb1..eb9b452d 100644
--- a/service/network/module.go
+++ b/service/network/module.go
@@ -9,10 +9,12 @@ import (
 	"sync/atomic"
 
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/netenv"
 	"github.com/safing/portmaster/service/network/state"
 	"github.com/safing/portmaster/service/profile"
+	"github.com/safing/portmaster/service/resolver"
 )
 
 // Events.
@@ -188,4 +190,6 @@ func New(instance instance) (*Network, error) {
 
 type instance interface {
 	Profile() *profile.ProfileModule
+	Resolver() *resolver.ResolverModule
+	DNSMonitor() *dnsmonitor.DNSMonitor
 }
diff --git a/service/network/packet/parse.go b/service/network/packet/parse.go
index 562546af..adfc69d9 100644
--- a/service/network/packet/parse.go
+++ b/service/network/packet/parse.go
@@ -106,11 +106,12 @@ func checkError(packet gopacket.Packet, info *Info) error {
 	return nil
 }
 
-// Parse parses an IP packet and saves the information in the given packet object.
-func Parse(packetData []byte, pktBase *Base) (err error) {
+// ParseLayer3 parses an IP packet and saves the information in the given packet object.
+func ParseLayer3(packetData []byte, pktBase *Base) (err error) {
 	if len(packetData) == 0 {
 		return errors.New("empty packet")
 	}
+
 	pktBase.layer3Data = packetData
 
 	ipVersion := packetData[0] >> 4
@@ -155,6 +156,62 @@ func Parse(packetData []byte, pktBase *Base) (err error) {
 	return nil
 }
 
+// ParseLayer4 parses an layer 4 packet and saves the information in the given packet object.
+func ParseLayer4(packetData []byte, pktBase *Base) (err error) {
+	if len(packetData) == 0 {
+		return errors.New("empty packet")
+	}
+
+	var layer gopacket.LayerType
+	switch pktBase.info.Protocol {
+	case ICMP:
+		layer = layers.LayerTypeICMPv4
+	case IGMP:
+		layer = layers.LayerTypeIGMP
+	case TCP:
+		layer = layers.LayerTypeTCP
+	case UDP:
+		layer = layers.LayerTypeUDP
+	case ICMPv6:
+		layer = layers.LayerTypeICMPv6
+	case UDPLite:
+		return fmt.Errorf("UDPLite not supported")
+	case RAW:
+		return fmt.Errorf("RAW protocol not supported")
+	case AnyHostInternalProtocol61:
+		return fmt.Errorf("AnyHostInternalProtocol61 protocol not supported")
+	default:
+		return fmt.Errorf("protocol not supported")
+	}
+
+	packet := gopacket.NewPacket(packetData, layer, gopacket.DecodeOptions{
+		Lazy:   true,
+		NoCopy: true,
+	})
+
+	availableDecoders := []func(gopacket.Packet, *Info) error{
+		parseTCP,
+		parseUDP,
+		// parseUDPLite, // We don't yet support udplite.
+		parseICMPv4,
+		parseICMPv6,
+		parseIGMP,
+		checkError,
+	}
+
+	for _, dec := range availableDecoders {
+		if err := dec(packet, pktBase.Info()); err != nil {
+			return err
+		}
+	}
+
+	pktBase.layers = packet
+	if transport := packet.TransportLayer(); transport != nil {
+		pktBase.layer5Data = transport.LayerPayload()
+	}
+	return nil
+}
+
 func init() {
 	genIPProtocolFromLayerType()
 }
diff --git a/service/resolver/ipinfo.go b/service/resolver/ipinfo.go
index 89cf9297..32cc0cc3 100644
--- a/service/resolver/ipinfo.go
+++ b/service/resolver/ipinfo.go
@@ -52,6 +52,27 @@ type ResolvedDomain struct {
 	Expires int64
 }
 
+// AddCNAMEs adds all cnames from the map related to its set Domain.
+func (resolved *ResolvedDomain) AddCNAMEs(cnames map[string]string) {
+	// Resolve all CNAMEs in the correct order and add the to the record - up to max 50 layers.
+	domain := resolved.Domain
+domainLoop:
+	for range 50 {
+		nextDomain, isCNAME := cnames[domain]
+		switch {
+		case !isCNAME:
+			break domainLoop
+		case nextDomain == resolved.Domain:
+			break domainLoop
+		case nextDomain == domain:
+			break domainLoop
+		}
+
+		resolved.CNAMEs = append(resolved.CNAMEs, nextDomain)
+		domain = nextDomain
+	}
+}
+
 // String returns a string representation of ResolvedDomain including
 // the CNAME chain. It implements fmt.Stringer.
 func (resolved *ResolvedDomain) String() string {
diff --git a/service/resolver/main.go b/service/resolver/main.go
index 8a43d12b..107d5fc8 100644
--- a/service/resolver/main.go
+++ b/service/resolver/main.go
@@ -29,6 +29,8 @@ type ResolverModule struct { //nolint
 	failingResolverWorkerMgr   *mgr.WorkerMgr
 	suggestUsingStaleCacheTask *mgr.WorkerMgr
 
+	isDisabled atomic.Bool
+
 	states *mgr.StateMgr
 }
 
@@ -52,6 +54,10 @@ func (rm *ResolverModule) Stop() error {
 	return nil
 }
 
+func (rm *ResolverModule) IsDisabled() bool {
+	return rm.isDisabled.Load()
+}
+
 func prep() error {
 	// Set DNS test connectivity function for the online status check
 	netenv.DNSTestQueryFunc = func(ctx context.Context, fdqn string) (ips []net.IP, ok bool, err error) {
diff --git a/service/resolver/resolver.go b/service/resolver/resolver.go
index 1a1a12f4..35d71329 100644
--- a/service/resolver/resolver.go
+++ b/service/resolver/resolver.go
@@ -17,17 +17,22 @@ import (
 
 // DNS Resolver Attributes.
 const (
-	ServerTypeDNS  = "dns"
-	ServerTypeTCP  = "tcp"
-	ServerTypeDoT  = "dot"
-	ServerTypeDoH  = "doh"
-	ServerTypeMDNS = "mdns"
-	ServerTypeEnv  = "env"
+	ServerTypeDNS      = "dns"
+	ServerTypeTCP      = "tcp"
+	ServerTypeDoT      = "dot"
+	ServerTypeDoH      = "doh"
+	ServerTypeMDNS     = "mdns"
+	ServerTypeEnv      = "env"
+	ServerTypeMonitor  = "monitor"
+	ServerTypeFirewall = "firewall"
 
 	ServerSourceConfigured      = "config"
 	ServerSourceOperatingSystem = "system"
 	ServerSourceMDNS            = "mdns"
 	ServerSourceEnv             = "env"
+	ServerSourceETW             = "etw"
+	ServerSourceSystemd         = "systemd"
+	ServerSourceFirewall        = "firewall"
 )
 
 // DNS resolver scheme aliases.
@@ -82,11 +87,11 @@ type ResolverInfo struct { //nolint:golint,maligned // TODO
 	Name string
 
 	// Type describes the type of the resolver.
-	// Possible values include dns, tcp, dot, doh, mdns, env.
+	// Possible values include dns, tcp, dot, doh, mdns, env, monitor, firewall.
 	Type string
 
 	// Source describes where the resolver configuration came from.
-	// Possible values include config, system, mdns, env.
+	// Possible values include config, system, mdns, env, etw, systemd, firewall.
 	Source string
 
 	// IP is the IP address of the resolver
diff --git a/service/resolver/resolvers.go b/service/resolver/resolvers.go
index c5609a01..45876b4e 100644
--- a/service/resolver/resolvers.go
+++ b/service/resolver/resolvers.go
@@ -388,7 +388,6 @@ func loadResolvers() {
 
 	// Resolve module error about missing resolvers.
 	module.states.Remove(missingResolversErrorID)
-
 	// Check if settings were changed and clear name cache when they did.
 	newResolverConfig := configuredNameServers()
 	if len(currentResolverConfig) > 0 &&
@@ -399,6 +398,14 @@ func loadResolvers() {
 			return err
 		})
 	}
+
+	// If no resolvers are configure set the disabled state. So other modules knows that the users does not want to use Portmaster resolver.
+	if len(newResolverConfig) == 0 {
+		module.isDisabled.Store(true)
+	} else {
+		module.isDisabled.Store(false)
+	}
+
 	currentResolverConfig = newResolverConfig
 
 	newResolvers := append(
@@ -431,7 +438,7 @@ func loadResolvers() {
 	// save resolvers
 	globalResolvers = newResolvers
 
-	// assing resolvers to scopes
+	// assign resolvers to scopes
 	setScopedResolvers(globalResolvers)
 
 	// set active resolvers (for cache validation)
diff --git a/windows_core_dll/build.ps1 b/windows_core_dll/build.ps1
new file mode 100644
index 00000000..d58f45ed
--- /dev/null
+++ b/windows_core_dll/build.ps1
@@ -0,0 +1,2 @@
+msbuild .\windows_core_dll.sln /p:Configuration=Release
+ls .\x64\Release\portmaster-core.dll
\ No newline at end of file
diff --git a/windows_core_dll/dllmain.cpp b/windows_core_dll/dllmain.cpp
new file mode 100644
index 00000000..cc0efaac
--- /dev/null
+++ b/windows_core_dll/dllmain.cpp
@@ -0,0 +1,197 @@
+// dllmain.cpp : Defines the entry point for the DLL application.
+#include "pch.h"
+
+#pragma comment(lib, "tdh.lib")
+
+// GUID of the DNS log provider
+static const GUID DNS_CLIENT_PROVIDER_GUID = {
+    0x1C95126E,
+    0x7EEA,
+    0x49A9,
+    {0xA3, 0xFE, 0xA3, 0x78, 0xB0, 0x3D, 0xDB, 0x4D} };
+
+// GUID of the event session. This should be unique for the application.
+static const GUID PORTMASTER_ETW_SESSION_GUID = {
+    0x0211d070,
+    0xc3b2,
+    0x4609,
+    {0x92, 0xf5, 0x28, 0xe7, 0x18, 0xb2, 0x3b, 0x18} };
+
+// Name of the session. This is visble when user queries all ETW sessions.
+// (example `logman query -ets`)
+#define LOGSESSION_NAME L"PortmasterDNSEventListener"
+
+// Fuction type of the callback that will be called on each event.
+typedef uint64_t(*GoEventRecordCallback)(wchar_t* domain, wchar_t* result);
+
+// Holds the state of the ETW Session.
+struct ETWSessionState {
+    TRACEHANDLE SessionTraceHandle;
+    EVENT_TRACE_PROPERTIES* SessionProperties;
+    TRACEHANDLE sessionHandle;
+    GoEventRecordCallback callback;
+};
+
+// getPropertyValue reads a property from the event.
+static bool getPropertyValue(PEVENT_RECORD evt, LPWSTR prop, PBYTE* pData) {
+    // Describe the data that needs to be retrieved from the event.
+    PROPERTY_DATA_DESCRIPTOR DataDescriptor;
+    ZeroMemory(&DataDescriptor, sizeof(DataDescriptor));
+    DataDescriptor.PropertyName = (ULONGLONG)(prop);
+    DataDescriptor.ArrayIndex = 0;
+
+    DWORD PropertySize = 0;
+    // Check if the data is avaliable and what is the size of it.
+    DWORD status =
+        TdhGetPropertySize(evt, 0, NULL, 1, &DataDescriptor, &PropertySize);
+    if (ERROR_SUCCESS != status) {
+        return false;
+    }
+
+    // Allocate memory for the data.
+    *pData = (PBYTE)malloc(PropertySize);
+    if (NULL == *pData) {
+        return false;
+    }
+
+    // Get the data.
+    status =
+        TdhGetProperty(evt, 0, NULL, 1, &DataDescriptor, PropertySize, *pData);
+    if (ERROR_SUCCESS != status) {
+        if (*pData) {
+            free(*pData);
+            *pData = NULL;
+        }
+        return false;
+    }
+
+    return true;
+}
+
+// EventRecordCallback is a callback called on each event.
+static void WINAPI EventRecordCallback(PEVENT_RECORD eventRecord) {
+    PBYTE resultValue = NULL;
+    PBYTE domainValue = NULL;
+
+    getPropertyValue(eventRecord, (LPWSTR)L"QueryResults", &resultValue);
+    getPropertyValue(eventRecord, (LPWSTR)L"QueryName", &domainValue);
+
+    ETWSessionState* state = (ETWSessionState*)eventRecord->UserContext;
+
+    if (resultValue != NULL && domainValue != NULL) {
+        state->callback((wchar_t*)domainValue, (wchar_t*)resultValue);
+    }
+
+    free(resultValue);
+    free(domainValue);
+}
+
+extern "C" {
+    // PM_ETWCreateState allocates memory for the state and initializes the config for the session. PM_ETWDestroySession must be called to avoid leaks.
+    // callback must be set to a valid function pointer.
+    __declspec(dllexport) ETWSessionState* PM_ETWCreateState(GoEventRecordCallback callback) {
+        // Create trace session properties.
+        ULONG BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
+        EVENT_TRACE_PROPERTIES* SessionProperties =
+            (EVENT_TRACE_PROPERTIES*)calloc(1, BufferSize);
+        SessionProperties->Wnode.BufferSize = BufferSize;
+        SessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
+        SessionProperties->Wnode.ClientContext = 1; // QPC clock resolution
+        SessionProperties->Wnode.Guid = PORTMASTER_ETW_SESSION_GUID;
+        SessionProperties->LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
+        SessionProperties->MaximumFileSize = 1; // MB
+        SessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
+
+        // Create state
+        ETWSessionState* state = (ETWSessionState*)calloc(1, sizeof(ETWSessionState));
+        state->SessionProperties = SessionProperties;
+        state->callback = callback;
+        return state;
+    }
+
+    // PM_ETWInitializeSession initializes the session.
+    __declspec(dllexport) uint32_t PM_ETWInitializeSession(ETWSessionState* state) {
+        return StartTrace(&state->SessionTraceHandle, LOGSESSION_NAME,
+            state->SessionProperties);
+    }
+
+    // PM_ETWStartTrace subscribes to the dns events and start listening. The function blocks while the listener is running.
+    // Call PM_ETWStopTrace to stop the listener.
+    __declspec(dllexport) uint32_t PM_ETWStartTrace(ETWSessionState* state) {
+        ULONG status =
+            EnableTraceEx2(state->SessionTraceHandle, (LPCGUID)&DNS_CLIENT_PROVIDER_GUID,
+                EVENT_CONTROL_CODE_ENABLE_PROVIDER,
+                TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL);
+
+        if (status != ERROR_SUCCESS) {
+            return status;
+        }
+
+        EVENT_TRACE_LOGFILE trace = { 0 };
+
+        trace.LoggerName = (LPWSTR)(LOGSESSION_NAME);
+        trace.ProcessTraceMode =
+            PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;
+        trace.EventRecordCallback = EventRecordCallback;
+        trace.Context = state;
+
+        state->sessionHandle = OpenTrace(&trace);
+        if (state->sessionHandle == INVALID_PROCESSTRACE_HANDLE) {
+            return 1;
+        }
+
+        status = ProcessTrace(&state->sessionHandle, 1, NULL, NULL);
+        if (status != ERROR_SUCCESS) {
+            return 1;
+        }
+
+        return ERROR_SUCCESS;
+    }
+
+    // PM_ETWFlushTrace flushes the event buffer.
+    __declspec(dllexport) uint32_t PM_ETWFlushTrace(ETWSessionState* state) {
+        return ControlTrace(state->SessionTraceHandle, LOGSESSION_NAME,
+            state->SessionProperties, EVENT_TRACE_CONTROL_FLUSH);
+    }
+
+    // PM_ETWFlushTrace stops the listener.
+    __declspec(dllexport) uint32_t PM_ETWStopTrace(ETWSessionState* state) {
+        return ControlTrace(state->SessionTraceHandle, LOGSESSION_NAME, state->SessionProperties,
+            EVENT_TRACE_CONTROL_STOP);
+    }
+
+    // PM_ETWFlushTrace Closes the session and frees resourses.
+    __declspec(dllexport) uint32_t PM_ETWDestroySession(ETWSessionState* state) {
+        if (state == NULL) {
+            return 1;
+        }
+        uint32_t status = CloseTrace(state->sessionHandle);
+
+        // Free memory.
+        free(state->SessionProperties);
+        free(state);
+        return status;
+    }
+
+    // PM_ETWStopOldSession removes old session with the same name if they exist. 
+    // It returns success(0) only if its able to delete the old session.
+    __declspec(dllexport) ULONG PM_ETWStopOldSession() {
+        ULONG status = ERROR_SUCCESS;
+        TRACEHANDLE sessionHandle = 0;
+
+        // Create trace session properties
+        size_t bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
+        EVENT_TRACE_PROPERTIES* sessionProperties = (EVENT_TRACE_PROPERTIES*)calloc(1, bufferSize);
+        sessionProperties->Wnode.BufferSize = (ULONG)bufferSize;
+        sessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
+        sessionProperties->Wnode.ClientContext = 1; // QPC clock resolution
+        sessionProperties->Wnode.Guid = PORTMASTER_ETW_SESSION_GUID;
+        sessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
+
+        // Use Control trace will stop the session which will trigger a delete.
+        status = ControlTrace(NULL, LOGSESSION_NAME, sessionProperties, EVENT_TRACE_CONTROL_STOP);
+
+        free(sessionProperties);
+        return status;
+    }
+}
\ No newline at end of file
diff --git a/windows_core_dll/framework.h b/windows_core_dll/framework.h
new file mode 100644
index 00000000..a9744f82
--- /dev/null
+++ b/windows_core_dll/framework.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
+// Windows Header Files
+#include <windows.h>
diff --git a/windows_core_dll/pch.cpp b/windows_core_dll/pch.cpp
new file mode 100644
index 00000000..91c22df2
--- /dev/null
+++ b/windows_core_dll/pch.cpp
@@ -0,0 +1,5 @@
+// pch.cpp: source file corresponding to the pre-compiled header
+
+#include "pch.h"
+
+// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
diff --git a/windows_core_dll/pch.h b/windows_core_dll/pch.h
new file mode 100644
index 00000000..e5658cac
--- /dev/null
+++ b/windows_core_dll/pch.h
@@ -0,0 +1,22 @@
+// pch.h: This is a precompiled header file.
+// Files listed below are compiled only once, improving build performance for future builds.
+// This also affects IntelliSense performance, including code completion and many code browsing features.
+// However, files listed here are ALL re-compiled if any one of them is updated between builds.
+// Do not add files here that you will be updating frequently as this negates the performance advantage.
+
+#ifndef PCH_H
+#define PCH_H
+
+// add headers that you want to pre-compile here
+#include "framework.h"
+
+#include <evntrace.h>
+#include <tdh.h>
+#include <iostream>
+#include <string>
+#include <evntcons.h>
+#include <codecvt>
+#include <thread>
+
+
+#endif //PCH_H
diff --git a/windows_core_dll/windows_core_dll.sln b/windows_core_dll/windows_core_dll.sln
new file mode 100644
index 00000000..73d46ca7
--- /dev/null
+++ b/windows_core_dll/windows_core_dll.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35222.181
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "windows_core_dll", "windows_core_dll.vcxproj", "{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x64.ActiveCfg = Debug|x64
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x64.Build.0 = Debug|x64
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x86.ActiveCfg = Debug|Win32
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x86.Build.0 = Debug|Win32
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x64.ActiveCfg = Release|x64
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x64.Build.0 = Release|x64
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x86.ActiveCfg = Release|Win32
+		{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x86.Build.0 = Release|Win32
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {8E60106D-49DF-49C7-AC08-02775342FEAE}
+	EndGlobalSection
+EndGlobal
diff --git a/windows_core_dll/windows_core_dll.vcxproj b/windows_core_dll/windows_core_dll.vcxproj
new file mode 100644
index 00000000..cf6d65f1
--- /dev/null
+++ b/windows_core_dll/windows_core_dll.vcxproj
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>17.0</VCProjectVersion>
+    <Keyword>Win32Proj</Keyword>
+    <ProjectGuid>{6f3c7eaf-8511-4822-aaf0-1086d27e4da9}</ProjectGuid>
+    <RootNamespace>windowscoredll</RootNamespace>
+    <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
+    <ProjectName>portmaster-core</ProjectName>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <ConfigurationType>DynamicLibrary</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v143</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <ConfigurationType>DynamicLibrary</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v143</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+    <ConfigurationType>DynamicLibrary</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v143</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+    <ConfigurationType>DynamicLibrary</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v143</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="Shared">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>WIN32;_DEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <PrecompiledHeader>Use</PrecompiledHeader>
+      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <EnableUAC>false</EnableUAC>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>WIN32;NDEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <PrecompiledHeader>Use</PrecompiledHeader>
+      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <EnableUAC>false</EnableUAC>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>_DEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <PrecompiledHeader>Use</PrecompiledHeader>
+      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <EnableUAC>false</EnableUAC>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>NDEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <PrecompiledHeader>Use</PrecompiledHeader>
+      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <EnableUAC>false</EnableUAC>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClInclude Include="framework.h" />
+    <ClInclude Include="pch.h" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="dllmain.cpp" />
+    <ClCompile Include="pch.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
+    </ClCompile>
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>
\ No newline at end of file
diff --git a/windows_core_dll/windows_core_dll.vcxproj.filters b/windows_core_dll/windows_core_dll.vcxproj.filters
new file mode 100644
index 00000000..f99bb483
--- /dev/null
+++ b/windows_core_dll/windows_core_dll.vcxproj.filters
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="Source Files">
+      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+      <Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+    </Filter>
+    <Filter Include="Header Files">
+      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+      <Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
+    </Filter>
+    <Filter Include="Resource Files">
+      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+    </Filter>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="framework.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="pch.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="dllmain.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="pch.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/windows_core_dll/windows_core_dll.vcxproj.user b/windows_core_dll/windows_core_dll.vcxproj.user
new file mode 100644
index 00000000..0f14913f
--- /dev/null
+++ b/windows_core_dll/windows_core_dll.vcxproj.user
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup />
+</Project>
\ No newline at end of file