From b0f664047b728c9ade433a89ae4e7194028034c5 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 29 Apr 2024 17:04:08 +0300 Subject: [PATCH] Add rust kext to the mono repo --- go.mod | 5 +- go.sum | 92 +- .../interception/windowskext2/kext.go | 6 +- .../interception/windowskext2/packet.go | 2 +- .../interception/windowskext2/service.go | 6 +- windows_kext/.gitignore | 400 +++++ windows_kext/Cargo.lock | 417 +++++ windows_kext/PacketFlow.md | 89 + windows_kext/PortmasterKext64.inf | 67 + windows_kext/README.md | 71 + windows_kext/c_helper/ARM64/c_helper.lib | Bin 0 -> 568450 bytes windows_kext/c_helper/c_helper.filters | 22 + windows_kext/c_helper/c_helper.sln | 51 + windows_kext/c_helper/c_helper.vcxproj | 188 ++ windows_kext/c_helper/helper.c | 89 + windows_kext/c_helper/x64/c_helper.lib | Bin 0 -> 107038 bytes windows_kext/driver/.cargo/config.toml | 3 + windows_kext/driver/Cargo.lock | 415 +++++ windows_kext/driver/Cargo.toml | 26 + windows_kext/driver/Makefile.toml | 18 + windows_kext/driver/README.md | 70 + windows_kext/driver/rust-toolchain | 1 + windows_kext/driver/src/ale_callouts.rs | 537 ++++++ windows_kext/driver/src/array_holder.rs | 25 + windows_kext/driver/src/bandwidth.rs | 293 ++++ windows_kext/driver/src/callouts.rs | 185 ++ windows_kext/driver/src/common.rs | 59 + windows_kext/driver/src/connection.rs | 499 ++++++ windows_kext/driver/src/connection_cache.rs | 200 +++ windows_kext/driver/src/connection_map.rs | 179 ++ windows_kext/driver/src/device.rs | 329 ++++ windows_kext/driver/src/driver_hashmap.rs | 25 + windows_kext/driver/src/entry.rs | 135 ++ windows_kext/driver/src/id_cache.rs | 131 ++ windows_kext/driver/src/lib.rs | 43 + windows_kext/driver/src/logger.rs | 114 ++ windows_kext/driver/src/packet_callouts.rs | 298 ++++ windows_kext/driver/src/packet_util.rs | 399 +++++ windows_kext/driver/src/stream_callouts.rs | 203 +++ windows_kext/kext_interface/command.go | 121 ++ windows_kext/kext_interface/info.go | 263 +++ windows_kext/kext_interface/ioctl.go | 36 + windows_kext/kext_interface/kext.go | 247 +++ windows_kext/kext_interface/kext_file.go | 98 ++ windows_kext/kext_interface/kext_file_test.go | 12 + windows_kext/kext_interface/protocol_test.go | 246 +++ windows_kext/kext_interface/version.txt | 1 + windows_kext/protocol/Cargo.lock | 193 +++ windows_kext/protocol/Cargo.toml | 14 + windows_kext/protocol/README.md | 4 + windows_kext/protocol/src/command.rs | 158 ++ windows_kext/protocol/src/info.rs | 552 ++++++ windows_kext/protocol/src/lib.rs | 5 + windows_kext/release/Cargo.lock | 525 ++++++ windows_kext/release/Cargo.toml | 14 + windows_kext/release/README.md | 27 + windows_kext/release/kext_release_v2-0-0.zip | Bin 0 -> 6382594 bytes windows_kext/release/src/main.rs | 121 ++ .../release/templates/PortmasterKext.ddf | 24 + .../release/templates/PortmasterKext64.inf | 68 + windows_kext/release/templates/build_cab.ps1 | 48 + windows_kext/release/templates/finalize.bat | 25 + windows_kext/release/templates/link.ps1 | 41 + windows_kext/test_protocol.sh | 24 + windows_kext/wdk/.cargo/config.toml | 2 + windows_kext/wdk/Cargo.lock | 139 ++ windows_kext/wdk/Cargo.toml | 20 + windows_kext/wdk/README.md | 14 + windows_kext/wdk/build.rs | 13 + windows_kext/wdk/rust-analyzer.cargo.target | 1 + windows_kext/wdk/rust-toolchain | 1 + windows_kext/wdk/src/allocator.rs | 70 + windows_kext/wdk/src/attributes.rs | 12 + windows_kext/wdk/src/consts.rs | 50 + windows_kext/wdk/src/debug.rs | 33 + windows_kext/wdk/src/driver.rs | 103 ++ windows_kext/wdk/src/error.rs | 9 + windows_kext/wdk/src/fast_mutex.rs | 143 ++ windows_kext/wdk/src/ffi.rs | 535 ++++++ windows_kext/wdk/src/filter_engine/callout.rs | 101 ++ .../wdk/src/filter_engine/callout_data.rs | 209 +++ .../wdk/src/filter_engine/classify.rs | 87 + .../wdk/src/filter_engine/connect_request.rs | 79 + windows_kext/wdk/src/filter_engine/ffi.rs | 255 +++ windows_kext/wdk/src/filter_engine/layer.rs | 1519 +++++++++++++++++ .../wdk/src/filter_engine/metadata.rs | 175 ++ windows_kext/wdk/src/filter_engine/mod.rs | 232 +++ .../wdk/src/filter_engine/net_buffer.rs | 355 ++++ windows_kext/wdk/src/filter_engine/packet.rs | 344 ++++ .../wdk/src/filter_engine/stream_data.rs | 67 + .../wdk/src/filter_engine/transaction.rs | 74 + windows_kext/wdk/src/interface.rs | 100 ++ windows_kext/wdk/src/ioqueue.rs | 216 +++ windows_kext/wdk/src/irp_helpers.rs | 198 +++ windows_kext/wdk/src/lib.rs | 32 + windows_kext/wdk/src/rw_spin_lock.rs | 73 + windows_kext/wdk/src/spin_lock.rs | 53 + windows_kext/wdk/src/utils.rs | 22 + 98 files changed, 13811 insertions(+), 84 deletions(-) create mode 100644 windows_kext/.gitignore create mode 100644 windows_kext/Cargo.lock create mode 100644 windows_kext/PacketFlow.md create mode 100644 windows_kext/PortmasterKext64.inf create mode 100644 windows_kext/README.md create mode 100644 windows_kext/c_helper/ARM64/c_helper.lib create mode 100644 windows_kext/c_helper/c_helper.filters create mode 100644 windows_kext/c_helper/c_helper.sln create mode 100644 windows_kext/c_helper/c_helper.vcxproj create mode 100644 windows_kext/c_helper/helper.c create mode 100644 windows_kext/c_helper/x64/c_helper.lib create mode 100644 windows_kext/driver/.cargo/config.toml create mode 100644 windows_kext/driver/Cargo.lock create mode 100644 windows_kext/driver/Cargo.toml create mode 100644 windows_kext/driver/Makefile.toml create mode 100644 windows_kext/driver/README.md create mode 100644 windows_kext/driver/rust-toolchain create mode 100644 windows_kext/driver/src/ale_callouts.rs create mode 100644 windows_kext/driver/src/array_holder.rs create mode 100644 windows_kext/driver/src/bandwidth.rs create mode 100644 windows_kext/driver/src/callouts.rs create mode 100644 windows_kext/driver/src/common.rs create mode 100644 windows_kext/driver/src/connection.rs create mode 100644 windows_kext/driver/src/connection_cache.rs create mode 100644 windows_kext/driver/src/connection_map.rs create mode 100644 windows_kext/driver/src/device.rs create mode 100644 windows_kext/driver/src/driver_hashmap.rs create mode 100644 windows_kext/driver/src/entry.rs create mode 100644 windows_kext/driver/src/id_cache.rs create mode 100644 windows_kext/driver/src/lib.rs create mode 100644 windows_kext/driver/src/logger.rs create mode 100644 windows_kext/driver/src/packet_callouts.rs create mode 100644 windows_kext/driver/src/packet_util.rs create mode 100644 windows_kext/driver/src/stream_callouts.rs create mode 100644 windows_kext/kext_interface/command.go create mode 100644 windows_kext/kext_interface/info.go create mode 100644 windows_kext/kext_interface/ioctl.go create mode 100644 windows_kext/kext_interface/kext.go create mode 100644 windows_kext/kext_interface/kext_file.go create mode 100644 windows_kext/kext_interface/kext_file_test.go create mode 100644 windows_kext/kext_interface/protocol_test.go create mode 100644 windows_kext/kext_interface/version.txt create mode 100644 windows_kext/protocol/Cargo.lock create mode 100644 windows_kext/protocol/Cargo.toml create mode 100644 windows_kext/protocol/README.md create mode 100644 windows_kext/protocol/src/command.rs create mode 100644 windows_kext/protocol/src/info.rs create mode 100644 windows_kext/protocol/src/lib.rs create mode 100644 windows_kext/release/Cargo.lock create mode 100644 windows_kext/release/Cargo.toml create mode 100644 windows_kext/release/README.md create mode 100644 windows_kext/release/kext_release_v2-0-0.zip create mode 100644 windows_kext/release/src/main.rs create mode 100644 windows_kext/release/templates/PortmasterKext.ddf create mode 100644 windows_kext/release/templates/PortmasterKext64.inf create mode 100644 windows_kext/release/templates/build_cab.ps1 create mode 100644 windows_kext/release/templates/finalize.bat create mode 100644 windows_kext/release/templates/link.ps1 create mode 100755 windows_kext/test_protocol.sh create mode 100644 windows_kext/wdk/.cargo/config.toml create mode 100644 windows_kext/wdk/Cargo.lock create mode 100644 windows_kext/wdk/Cargo.toml create mode 100644 windows_kext/wdk/README.md create mode 100644 windows_kext/wdk/build.rs create mode 100644 windows_kext/wdk/rust-analyzer.cargo.target create mode 100644 windows_kext/wdk/rust-toolchain create mode 100644 windows_kext/wdk/src/allocator.rs create mode 100644 windows_kext/wdk/src/attributes.rs create mode 100644 windows_kext/wdk/src/consts.rs create mode 100644 windows_kext/wdk/src/debug.rs create mode 100644 windows_kext/wdk/src/driver.rs create mode 100644 windows_kext/wdk/src/error.rs create mode 100644 windows_kext/wdk/src/fast_mutex.rs create mode 100644 windows_kext/wdk/src/ffi.rs create mode 100644 windows_kext/wdk/src/filter_engine/callout.rs create mode 100644 windows_kext/wdk/src/filter_engine/callout_data.rs create mode 100644 windows_kext/wdk/src/filter_engine/classify.rs create mode 100644 windows_kext/wdk/src/filter_engine/connect_request.rs create mode 100644 windows_kext/wdk/src/filter_engine/ffi.rs create mode 100644 windows_kext/wdk/src/filter_engine/layer.rs create mode 100644 windows_kext/wdk/src/filter_engine/metadata.rs create mode 100644 windows_kext/wdk/src/filter_engine/mod.rs create mode 100644 windows_kext/wdk/src/filter_engine/net_buffer.rs create mode 100644 windows_kext/wdk/src/filter_engine/packet.rs create mode 100644 windows_kext/wdk/src/filter_engine/stream_data.rs create mode 100644 windows_kext/wdk/src/filter_engine/transaction.rs create mode 100644 windows_kext/wdk/src/interface.rs create mode 100644 windows_kext/wdk/src/ioqueue.rs create mode 100644 windows_kext/wdk/src/irp_helpers.rs create mode 100644 windows_kext/wdk/src/lib.rs create mode 100644 windows_kext/wdk/src/rw_spin_lock.rs create mode 100644 windows_kext/wdk/src/spin_lock.rs create mode 100644 windows_kext/wdk/src/utils.rs diff --git a/go.mod b/go.mod index 5d016b1e..ecf77779 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/Xuanwo/go-locale v1.1.0 github.com/agext/levenshtein v1.2.3 github.com/awalterschulze/gographviz v2.0.3+incompatible + github.com/brianvoe/gofakeit v3.18.0+incompatible github.com/cilium/ebpf v0.14.0 github.com/coreos/go-iptables v0.7.0 github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 @@ -34,7 +35,6 @@ require ( github.com/rot256/pblind v0.0.0-20231024115251-cd3f239f28c1 github.com/safing/jess v0.3.3 github.com/safing/portbase v0.19.4 - github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.8.0 github.com/spkg/zipfs v0.7.1 @@ -60,7 +60,6 @@ require ( github.com/alessio/shellescape v1.4.2 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/bluele/gcache v0.0.2 // indirect - github.com/brianvoe/gofakeit v3.18.0+incompatible // indirect github.com/danieljoos/wincred v1.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -91,6 +90,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec // indirect github.com/satori/go.uuid v1.2.0 // indirect github.com/seehuhn/fortuna v1.0.1 // indirect github.com/seehuhn/sha256d v1.0.0 // indirect @@ -103,7 +103,6 @@ require ( github.com/tklauser/numcpus v0.7.0 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect - github.com/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240417112037-e925bb329127 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index d493aef3..ad1e9591 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIo github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/VictoriaMetrics/metrics v1.29.1 h1:yTORfGeO1T0C6P/tEeT4Mf7rBU5TUu3kjmHvmlaoeO8= -github.com/VictoriaMetrics/metrics v1.29.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= github.com/VictoriaMetrics/metrics v1.33.1 h1:CNV3tfm2Kpv7Y9W3ohmvqgFWPR55tV2c7M2U6OIo+UM= github.com/VictoriaMetrics/metrics v1.33.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= @@ -32,8 +30,6 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= -github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= github.com/cilium/ebpf v0.14.0 h1:0PsxAjO6EjI1rcT+rkp6WcCnE0ZvfkXBYiMedJtrSUs= github.com/cilium/ebpf v0.14.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= @@ -63,15 +59,10 @@ github.com/florianl/go-nfqueue v1.3.1 h1:khQ9fYCrjbu5CF8dZF55G2RTIEIQRI0Aj5k3msJ github.com/florianl/go-nfqueue v1.3.1/go.mod h1:aHWbgkhryJxF5XxYvJ3oRZpdD4JP74Zu/hP1zuhja+M= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= -github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= -github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= -github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= @@ -80,6 +71,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -118,8 +111,6 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= @@ -159,8 +150,6 @@ github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4h github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -199,12 +188,8 @@ github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/ github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g= github.com/mdlayher/socket v0.1.0/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= -github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= -github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -231,30 +216,17 @@ github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rot256/pblind v0.0.0-20231024115251-cd3f239f28c1 h1:vfAp3Jbca7Vt8axzmkS5M/RtFJmj0CKmrtWAlHtesaA= github.com/rot256/pblind v0.0.0-20231024115251-cd3f239f28c1/go.mod h1:2x8fbm9T+uTl919COhEVHKGkve1DnkrEnDbtGptZuW8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safing/jess v0.3.3 h1:0U0bWdO0sFCgox+nMOqISFrnJpVmi+VFOW1xdX6q3qw= github.com/safing/jess v0.3.3/go.mod h1:t63qHB+4xd1HIv9MKN/qI2rc7ytvx7d6l4hbX7zxer0= -github.com/safing/portbase v0.18.9 h1:j+ToHKQz0U2+Tx4jMP7QPky/H0R4uY6qUM+lIJlO6ks= -github.com/safing/portbase v0.18.9/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= -github.com/safing/portbase v0.19.0 h1:2T6f/w90IdIsSgUfyXoveqZM7tVwW+IFrtLbPVXtY3k= -github.com/safing/portbase v0.19.0/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= -github.com/safing/portbase v0.19.1 h1:Uk/WyP9HsIJrCn0pE4a7AWIrfUSHyCOObQyRmXsGQ9A= -github.com/safing/portbase v0.19.1/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= -github.com/safing/portbase v0.19.2 h1:qGF5Jv9eEE33d2aIxeBQdnitnBoF44BGVFtboqfE+1A= -github.com/safing/portbase v0.19.2/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= -github.com/safing/portbase v0.19.3 h1:fzb4d2nzhmRq4Lt6sgn9R20iykireAkBNyf9pfGqQjk= -github.com/safing/portbase v0.19.3/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= github.com/safing/portbase v0.19.4 h1:Oh7oUBp6xn5whhKtvnNKS5rhHqyXJDDxfxwf+gRswhQ= github.com/safing/portbase v0.19.4/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec h1:oSJY1seobofPwpMoJRkCgXnTwfiQWNfGMCPDfqgAEfg= github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec/go.mod h1:abwyAQrZGemWbSh/aCD9nnkp0SvFFf/mGWkAbOwPnFE= -github.com/safing/spn v0.7.5 h1:WfkMs2omLrwxBWccGGG9Akx0AvsvJLG+W7rjWQpQhl4= -github.com/safing/spn v0.7.5/go.mod h1:Hg585WJuib4JI3R7Kndq/10MJPCUl1UmeJJwL3JIwdQ= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0= @@ -292,8 +264,6 @@ github.com/tannerryan/ring v1.1.2/go.mod h1:DkELJEjbZhJBtFKR9Xziwj3HKZnb/knRgljN github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -315,10 +285,6 @@ github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OL github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 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/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240120091731-1a3450b13959 h1:5j8cHx9n4drternoY4HXomea+4aYJuKMgnA3VhlG5WM= -github.com/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240120091731-1a3450b13959/go.mod h1:PCv02zl4R2SbmEUDetMKO+kTfvMvsVVZuOzOXRMcHwE= -github.com/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240417112037-e925bb329127 h1:bjBak6OaP3i8FsGViCExzMOvsWwjN0rFE7DPAGzYAq0= -github.com/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240417112037-e925bb329127/go.mod h1:+1fUhn4PQbvGmO5qqNj1T8F298VXZP6ni1YeKS0JhZA= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= @@ -328,8 +294,6 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= @@ -340,22 +304,14 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= -go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= -golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= @@ -365,8 +321,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -390,10 +344,6 @@ golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -401,8 +351,6 @@ golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -439,10 +387,6 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -463,8 +407,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -486,29 +428,33 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20240110202538-8053cd8f0bf6 h1:Ass5FAjCCQ5WECPE9NN7ItZnKJ38i6sM8MCMNBGee5I= -gvisor.dev/gvisor v0.0.0-20240110202538-8053cd8f0bf6/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= -gvisor.dev/gvisor v0.0.0-20240327015314-08ed01b28587 h1:wH3g/qTCPlVBwkFktYuKNFJGeo7ctLNEjzrMlfPrVgE= -gvisor.dev/gvisor v0.0.0-20240327015314-08ed01b28587/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= gvisor.dev/gvisor v0.0.0-20240409213450-87d8df37c71e h1:jpvBdtqDLzu2MZuruscr008NwJxiDidjFF4ZQq7YZbk= gvisor.dev/gvisor v0.0.0-20240409213450-87d8df37c71e/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= -modernc.org/libc v1.40.1 h1:ZhRylEBcj3GyQbPVC8JxIg7SdrT4JOxIDJoUon0NfF8= -modernc.org/libc v1.40.1/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= +modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= +modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= -zombiezen.com/go/sqlite v1.0.0 h1:D2EvOZqumJBy+6t+0uNTTXnepUpB/pKG45op/UziI1o= -zombiezen.com/go/sqlite v1.0.0/go.mod h1:Yx7FJ77tr7Ucwi5solhXAxpflyxk/BHNXArZ/JvDm60= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= zombiezen.com/go/sqlite v1.2.0 h1:jja0Ubpzpl6bjr/bSaPyvafHO+extoDJJXIaqXT7VOU= zombiezen.com/go/sqlite v1.2.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY= diff --git a/service/firewall/interception/windowskext2/kext.go b/service/firewall/interception/windowskext2/kext.go index ca42d7c0..4cbf9041 100644 --- a/service/firewall/interception/windowskext2/kext.go +++ b/service/firewall/interception/windowskext2/kext.go @@ -8,7 +8,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portmaster/service/network" - "github.com/vlabo/portmaster_windows_rust_kext/kext_interface" + "github.com/safing/portmaster/windows_kext/kext_interface" "golang.org/x/sys/windows" ) @@ -131,7 +131,7 @@ func UpdateVerdict(conn *network.Connection) error { LocalPort: conn.LocalPort, RemoteAddress: [4]byte(conn.Entity.IP), RemotePort: conn.Entity.Port, - Verdict: uint8(conn.Verdict.Active), + Verdict: uint8(conn.Verdict), } return kext_interface.SendUpdateV4Command(kextFile, update) @@ -142,7 +142,7 @@ func UpdateVerdict(conn *network.Connection) error { LocalPort: conn.LocalPort, RemoteAddress: [16]byte(conn.Entity.IP), RemotePort: conn.Entity.Port, - Verdict: uint8(conn.Verdict.Active), + Verdict: uint8(conn.Verdict), } return kext_interface.SendUpdateV6Command(kextFile, update) diff --git a/service/firewall/interception/windowskext2/packet.go b/service/firewall/interception/windowskext2/packet.go index aa9acd54..f785efe3 100644 --- a/service/firewall/interception/windowskext2/packet.go +++ b/service/firewall/interception/windowskext2/packet.go @@ -7,10 +7,10 @@ import ( "sync" "github.com/tevino/abool" - "github.com/vlabo/portmaster_windows_rust_kext/kext_interface" "github.com/safing/portbase/log" "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/windows_kext/kext_interface" ) // Packet represents an IP packet. diff --git a/service/firewall/interception/windowskext2/service.go b/service/firewall/interception/windowskext2/service.go index 8b342a67..5f6dc20e 100644 --- a/service/firewall/interception/windowskext2/service.go +++ b/service/firewall/interception/windowskext2/service.go @@ -2,10 +2,8 @@ // +build windows package windowskext - -import ( - "github.com/vlabo/portmaster_windows_rust_kext/kext_interface" -) + +import "github.com/safing/portmaster/windows_kext/kext_interface" func createKextService(driverName string, driverPath string) (*kext_interface.KextService, error) { return kext_interface.CreateKextService(driverName, driverPath) diff --git a/windows_kext/.gitignore b/windows_kext/.gitignore new file mode 100644 index 00000000..c4230841 --- /dev/null +++ b/windows_kext/.gitignore @@ -0,0 +1,400 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +*/**/[Rr]elease/ +*/**/[Rr]eleases/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +*.exe + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +pm_kext/RCa04400 +pm_kext/RCa05788 +pm_kext/RCa06452 +target +kext.sys +release/build diff --git a/windows_kext/Cargo.lock b/windows_kext/Cargo.lock new file mode 100644 index 00000000..29a6169a --- /dev/null +++ b/windows_kext/Cargo.lock @@ -0,0 +1,417 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "include_dir" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b56e147e6187d61e9d0f039f10e070d0c0a887e24fe0bb9ca3f29bfde62cab" +dependencies = [ + "glob", + "include_dir_impl", + "proc-macro-hack", +] + +[[package]] +name = "include_dir_impl" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0c890c85da4bab7bce4204c707396bbd3c6c8a681716a51c8814cfc2b682df" +dependencies = [ + "anyhow", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "portmaster-windows-kext" +version = "0.1.0" +dependencies = [ + "serde", + "serde-generate", + "serde-reflection", + "widestring", + "winapi", + "windows-sys", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-generate" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c9331265d81c61212dc75df7b0836544ed8e32dba77a522f113805ff9a948e" +dependencies = [ + "heck", + "include_dir", + "phf", + "serde", + "serde-reflection", + "textwrap", +] + +[[package]] +name = "serde-reflection" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05a5f801ac62a51a49d378fdb3884480041b99aced450b28990673e8ff99895" +dependencies = [ + "once_cell", + "serde", + "thiserror", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "textwrap" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd05616119e612a8041ef58f2b578906cc2531a6069047ae092cfb86a325d835" +dependencies = [ + "smawk", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "winapi" +version = "0.3.7" +source = "git+https://github.com/Trantect/winapi-rs.git?branch=feature/km#981da7663da0dd9f82bcfceacdc949293a612ef0" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "git+https://github.com/Trantect/winapi-rs.git?branch=feature/km#981da7663da0dd9f82bcfceacdc949293a612ef0" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "git+https://github.com/Trantect/winapi-rs.git?branch=feature/km#981da7663da0dd9f82bcfceacdc949293a612ef0" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" diff --git a/windows_kext/PacketFlow.md b/windows_kext/PacketFlow.md new file mode 100644 index 00000000..67eab5f3 --- /dev/null +++ b/windows_kext/PacketFlow.md @@ -0,0 +1,89 @@ +# There and back again, a packets tale. + +An explanation on the complete path of the packet from entering to the exit of the kernel extension. + +## Entry + +The packet entry point depends on the packet and the internal windows filter state: + +- First packet of outbound connection -> AleAuthConnect Layer +- First packet of inbound connection -> InboundIppacket Layer + +## ALE layer + +Each defined ALE layer has a filter linked to it. This filter has a state. +When a decision is made to block or permit a connection it will be saved to the filter state. +The only way to update the decision in a filter is to clear the whole state and apply the decision for the next packet of each connection. + +### First packet + +For outgoing connections this logic fallows: + - Packet enters in one of the ALE layer + - Packet is TCP or UDP + 1. Save and absorb packet. + 2. Send an event to Portmaster. + 2. Create a cache entry. + - If Packet is not TCP/UDP forward to packet layer + +For incoming connection this logic fallow: + - Packet enter in one of the Packet layer, if packet is TCP or UDP it will be forwarded to ALE layer. From there: + 1. Save packet and absorb. + 2. Send an event to Portmaster. + 2. Create a cache entry. + 3. Wait for Portmasters decision. + - If Packet is not TCP/UDP. It will be handled only by the packet layer. + + +If more packets arrive before Portmaster returns a decision, packet will be absorbed and another event will be sent. +For Outgoing connection this will happen in ALE layer. +For Incoming connection this will happen in Packet layer. + +### Pormtaster returns a verdict for the connection + +Connection cache will be updated and the packet will be injected. +The next steps depend of the direction of the packet and the verdict + +* Permanent Verdict / Outgoing connection + - Allow / Block / Drop directly in the ALE layer. For Block and Drop packet layer will not see the rest of the packet in the connection. +* Temporary Verdict / Outgoing connection + - Always Allow - this connections are solely handled by the packet layer. (This is true only for outgoing connections) + +* Permanent or Temporary Verdict / Incoming connection + - Allow / Block / Drop directly in the ALE layer. They always go through the packet layer first no need to do anything special + +Fallowing specifics apply to the ALE layer: +1. Connections with flag `reauthorize == false` are special. When the flag is `false` that means that a applications is calling a function `connect()` or `accept()` for a connection. This is a special case because we control the result of the function, telling the application that it's allowed or not allowed to continue with the connection. Since we are making request to Portmaster we need to take longer time. This is done with pending the packet. This allows the kernel extension to pause the event and continue when it has the verdict. See `ale_callouts.rs -> save_packet()` function. +2. If packet payload is present it is from the transport layer. + + +## Packet layer + +The logic for the packet is split in two: + +### TCP or UDP protocols + +The packet layer will not process packets that miss a cache entry: +- Incoming packet: it will forward it to the ALE layer. +- Outgoing packet: this is treated as invalid state since ALE should be the entry for the packets. If it happens the packet layer will create a request to Portmaster for it. + +For packets with a cache entry: +- Permanent Verdict: apply the verdict. +- Redirect Verdict: copy the packet, modify and inject. Drop the original packet. +- Temporary verdict: send request to Portmaster. + +After portmaster returns the verdict for the packet. If its allowed it will be modified (if needed) and injected everything else will be dropped. +The packet layer will permit all injected packets. + +### Not TCP or UDP protocols -> ICMP, IGMP ... + +Does packets are treated as with temporary verdict. There will be no cache entry for them. +Every packet will be send to Portmaster for a decision and re-injected if allowed. + +## Connection Cache + +It holds information for all TCP and UDP connections. Local and destination ip addresses and ports, verdict, protocol, process id +It also holds last active time and end time. + +Cache entry is removed automatically 1 minute after an end state has been set or after 10 minutes of inactivity. + +End stat is set by Endpoint layers or Resource release layers. \ No newline at end of file diff --git a/windows_kext/PortmasterKext64.inf b/windows_kext/PortmasterKext64.inf new file mode 100644 index 00000000..1e3c5aae --- /dev/null +++ b/windows_kext/PortmasterKext64.inf @@ -0,0 +1,67 @@ +;/*++ +; +;Copyright (c) Safing ICS Technologies GmbH. +; +; This program is free software: you can redistribute it and/or modify +; it under the terms of the GNU General Public License as published by +; the Free Software Foundation, either version 3 of the License, or +; (at your option) any later version. +; +; This program is distributed in the hope that it will be useful, +; but WITHOUT ANY WARRANTY; without even the implied warranty of +; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +; GNU General Public License for more details. +; +; You should have received a copy of the GNU General Public License +; along with this program. If not, see . +; +;--*/ + +[Version] +Signature = "$Windows NT$" +Class = WFPCALLOUTS +ClassGuid = {57465043-616C-6C6F-7574-5F636C617373} +Provider = %Provider% +CatalogFile = PortmasterKext64.Cat +DriverVer = 01/01/2019,1.0.11.0 + +[SourceDisksNames] +1 = %DiskName% + +[SourceDisksFiles] +PortmasterKext64.sys = 1 + +[DestinationDirs] +DefaultDestDir = 12 ; %windir%\system32\drivers +PortmasterKext.DriverFiles = 12 ; %windir%\system32\drivers + +[DefaultInstall] +OptionDesc = %Description% +CopyFiles = PortmasterKext.DriverFiles + +[DefaultInstall.Services] +AddService = %ServiceName%,,PortmasterKext.Service + +[DefaultUninstall] +DelFiles = PortmasterKext.DriverFiles + +[DefaultUninstall.Services] +DelService = PortmasterKext,0x200 ; SPSVCINST_STOPSERVICE + +[PortmasterKext.DriverFiles] +PortmasterKext64.sys,,,0x00000040 ; COPYFLG_OVERWRITE_OLDER_ONLY + +[PortmasterKext.Service] +DisplayName = %ServiceName% +Description = %ServiceDesc% +ServiceType = 1 ; SERVICE_KERNEL_DRIVER +StartType = 0 ; SERVICE_BOOT_START +ErrorControl = 1 ; SERVICE_ERROR_NORMAL +ServiceBinary = %12%\PortmasterKext64.sys + +[Strings] +Provider = "Safing ICS Technologies GmbH" +DiskName = "PortmasterKext Installation Disk" +Description = "PortmasterKext Driver" +ServiceName = "PortmasterKext" +ServiceDesc = "PortmasterKext Driver" diff --git a/windows_kext/README.md b/windows_kext/README.md new file mode 100644 index 00000000..f77de347 --- /dev/null +++ b/windows_kext/README.md @@ -0,0 +1,71 @@ +# Portmaster Windows kext +Implementation of Safing's Portmaster Windows kernel extension in Rust. + +### Documentation + +- [Driver](driver/README.md) -> entry point. +- [WDK](wdk/README.md) -> Windows Driver Kit interface. +- [Packet Path](PacketDoc.md) -> Detiled documentation of what happens to a packet when it enters the kernel extension. +- [Release](release/README.md) -> Guide how to do a release build + +### Building + +The Windows Portmaster Kernel Extension is currently only developed and tested for the amd64 (64-bit) architecture. + +__Prerequesites:__ + +- Visual Studio 2022 + - Install C++ and Windows 11 SDK (22H2) components + - Add `link.exe` and `signtool` in the PATH +- Rust + - https://www.rust-lang.org/tools/install +- Cargo make(optional) + - https://github.com/sagiegurari/cargo-make + +__Setup Test Signing:__ + +In order to test the driver on your machine, you will have to test sign it (starting with Windows 10). + + +Create a new certificate for test signing: + + :: Open a *x64 Free Build Environment* console as Administrator. + + :: Run the MakeCert.exe tool to create a test certificate: + MakeCert -r -pe -ss PrivateCertStore -n "CN=DriverCertificate" DriverCertificate.cer + + :: Install the test certificate with CertMgr.exe: + CertMgr /add DriverCertificate.cer /s /r localMachine root + + +Enable Test Signing on the dev machine: + + :: Before you can load test-signed drivers, you must enable Windows test mode. To do this, run this command: + Bcdedit.exe -set TESTSIGNING ON + :: Then, restart Windows. For more information, see The TESTSIGNING Boot Configuration Option. + + +__Build driver:__ + +``` +cd driver +cargo build +``` +> Build also works on linux + +__Link and sign:__ +On a windows machine copy `driver.lib` form the project target directory (`driver/target/x86_64-pc-windows-msvc/debug/driver.lib`) in the same folder as `link.bat`. +Run `link.bat`. + +`driver.sys` should appear in the folder. Load and use the driver. + +### Test +- Install go + - https://go.dev/dl/ + +``` +cd kext_tester +go run . +``` + +> make sure the hardcoded path in main.go is pointing to the correct `.sys` file diff --git a/windows_kext/c_helper/ARM64/c_helper.lib b/windows_kext/c_helper/ARM64/c_helper.lib new file mode 100644 index 0000000000000000000000000000000000000000..c9b45ef16db0e83cec8093e08640a50f7927ff61 GIT binary patch literal 568450 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 + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file diff --git a/windows_kext/c_helper/c_helper.sln b/windows_kext/c_helper/c_helper.sln new file mode 100644 index 00000000..134d688a --- /dev/null +++ b/windows_kext/c_helper/c_helper.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33502.453 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "c_helper", "c_helper.vcxproj", "{39A5E911-A716-4708-8B88-3895183C6372}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|ARM.ActiveCfg = Debug|ARM + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|ARM.Build.0 = Debug|ARM + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|ARM.Deploy.0 = Debug|ARM + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|ARM64.Build.0 = Debug|ARM64 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|x64.ActiveCfg = Debug|x64 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|x64.Build.0 = Debug|x64 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|x64.Deploy.0 = Debug|x64 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|x86.ActiveCfg = Debug|Win32 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|x86.Build.0 = Debug|Win32 + {39A5E911-A716-4708-8B88-3895183C6372}.Debug|x86.Deploy.0 = Debug|Win32 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|ARM.ActiveCfg = Release|ARM + {39A5E911-A716-4708-8B88-3895183C6372}.Release|ARM.Build.0 = Release|ARM + {39A5E911-A716-4708-8B88-3895183C6372}.Release|ARM.Deploy.0 = Release|ARM + {39A5E911-A716-4708-8B88-3895183C6372}.Release|ARM64.ActiveCfg = Release|ARM64 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|ARM64.Build.0 = Release|ARM64 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|ARM64.Deploy.0 = Release|ARM64 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|x64.ActiveCfg = Release|x64 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|x64.Build.0 = Release|x64 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|x64.Deploy.0 = Release|x64 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|x86.ActiveCfg = Release|Win32 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|x86.Build.0 = Release|Win32 + {39A5E911-A716-4708-8B88-3895183C6372}.Release|x86.Deploy.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {91E52350-EBB9-4B0F-9C28-61C0BBAEDC6A} + EndGlobalSection +EndGlobal diff --git a/windows_kext/c_helper/c_helper.vcxproj b/windows_kext/c_helper/c_helper.vcxproj new file mode 100644 index 00000000..250c3109 --- /dev/null +++ b/windows_kext/c_helper/c_helper.vcxproj @@ -0,0 +1,188 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM + + + Release + ARM + + + Debug + ARM64 + + + Release + ARM64 + + + + + + + {39A5E911-A716-4708-8B88-3895183C6372} + {0a049372-4c4d-4ea0-a64e-dc6ad88ceca1} + v4.5 + 12.0 + Debug + Win32 + c_helper + KMDF + $(LatestTargetPlatformVersion) + c_helper + + + + Windows10 + true + WindowsKernelModeDriver10.0 + StaticLibrary + Universal + Unicode + + + Windows10 + false + WindowsKernelModeDriver10.0 + StaticLibrary + Universal + Unicode + + + Windows10 + true + WindowsKernelModeDriver10.0 + StaticLibrary + Universal + Unicode + false + + + Windows10 + false + WindowsKernelModeDriver10.0 + StaticLibrary + Universal + Unicode + false + + + Windows10 + true + WindowsKernelModeDriver10.0 + StaticLibrary + Unicode + + + Windows10 + false + WindowsKernelModeDriver10.0 + StaticLibrary + Universal + Unicode + + + Windows10 + true + WindowsKernelModeDriver10.0 + StaticLibrary + Universal + Unicode + + + Windows10 + false + WindowsKernelModeDriver10.0 + StaticLibrary + Universal + Unicode + false + + + + + + + + + + $(SolutionDir)$(Platform) + + + $(SolutionDir)$(Platform) + + + $(SolutionDir)$(Platform) + $(Platform)\$(ConfigurationName)\ + $(TargetName.Replace(' ','')) + + + + _DEBUG;WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + + + + + WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + + + + + _DEBUG;WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + false + + + + + WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + false + + + + + _DEBUG;WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + + + + + WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + + + + + _DEBUG;WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + + + + + WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WINAPI_PARTITION_DESKTOP=1;WINAPI_PARTITION_SYSTEM=1;WINAPI_PARTITION_APP=1;WINAPI_PARTITION_PC_APP=1;%(PreprocessorDefinitions) + Default + Default + + + + + + \ No newline at end of file diff --git a/windows_kext/c_helper/helper.c b/windows_kext/c_helper/helper.c new file mode 100644 index 00000000..93e893e2 --- /dev/null +++ b/windows_kext/c_helper/helper.c @@ -0,0 +1,89 @@ + +/* + * Name: helper.c + */ + +#include +#include + +#define NDIS640 1 // Windows 8 and Windows Server 2012 + +#include "Ntifs.h" +#include // Windows Driver Development Kit +#include // Windows Driver Foundation + +#pragma warning(push) +#pragma warning(disable: 4201) // Disable "Nameless struct/union" compiler warning for fwpsk.h only! +#include // Functions and enumerated types used to implement callouts in kernel mode +#pragma warning(pop) // Re-enable "Nameless struct/union" compiler warning + +#include // Functions used for managing IKE and AuthIP main mode (MM) policy and security associations +#include // Mappings of OS specific function versions (i.e. fn's that end in 0 or 1) +#include // Used to define GUID's +#include // Used to define GUID's +#include "devguid.h" +#include +#include +#include + +EVT_WDF_DRIVER_UNLOAD emptyEventUnload; + +NTSTATUS pm_InitDriverObject(DRIVER_OBJECT * driverObject, UNICODE_STRING * registryPath, WDFDRIVER * driver, WDFDEVICE * device, wchar_t *win_device_name, wchar_t *dos_device_name, WDF_OBJECT_ATTRIBUTES * objectAttributes, void (*wdfEventUnload)(WDFDRIVER)) { + UNICODE_STRING deviceName = { 0 }; + RtlInitUnicodeString(&deviceName, win_device_name); + + UNICODE_STRING deviceSymlink = { 0 }; + RtlInitUnicodeString(&deviceSymlink, dos_device_name); + + // Create a WDFDRIVER for this driver + WDF_DRIVER_CONFIG config = { 0 }; + WDF_DRIVER_CONFIG_INIT(&config, WDF_NO_EVENT_CALLBACK); + config.DriverInitFlags = WdfDriverInitNonPnpDriver; + config.EvtDriverUnload = wdfEventUnload; // <-- Necessary for this driver to unload correctly + NTSTATUS status = WdfDriverCreate(driverObject, registryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, driver); + if (!NT_SUCCESS(status)) { + return status; + } + + // Create a WDFDEVICE for this driver + PWDFDEVICE_INIT deviceInit = WdfControlDeviceInitAllocate(*driver, &SDDL_DEVOBJ_SYS_ALL_ADM_ALL); // only admins and kernel can access device + if (!deviceInit) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + // Configure the WDFDEVICE_INIT with a name to allow for access from user mode + WdfDeviceInitSetDeviceType(deviceInit, FILE_DEVICE_NETWORK); + WdfDeviceInitSetCharacteristics(deviceInit, FILE_DEVICE_SECURE_OPEN, false); + (void) WdfDeviceInitAssignName(deviceInit, &deviceName); + (void) WdfPdoInitAssignRawDevice(deviceInit, &GUID_DEVCLASS_NET); + WdfDeviceInitSetDeviceClass(deviceInit, &GUID_DEVCLASS_NET); + + status = WdfDeviceCreate(&deviceInit, objectAttributes, device); + if (!NT_SUCCESS(status)) { + WdfDeviceInitFree(deviceInit); + return status; + } + status = WdfDeviceCreateSymbolicLink(*device, &deviceSymlink); + if (!NT_SUCCESS(status)) { + return status; + } + + // The system will not send I/O requests or Windows Management Instrumentation (WMI) requests to a control device object unless the driver has called WdfControlFinishInitializing. + WdfControlFinishInitializing(*device); + + return STATUS_SUCCESS; +} + +void* pm_WdfObjectGetTypedContextWorker(WDFOBJECT wdfObject, PCWDF_OBJECT_CONTEXT_TYPE_INFO typeInfo) { + return WdfObjectGetTypedContextWorker(wdfObject, typeInfo->UniqueType); +} + +DEVICE_OBJECT* pm_GetDeviceObject(WDFDEVICE device) { + return WdfDeviceWdmGetDeviceObject(device); +} + +UINT64 pm_QuerySystemTime() { + UINT64 timestamp = 0; + KeQuerySystemTime(×tamp); + return timestamp; +} \ No newline at end of file diff --git a/windows_kext/c_helper/x64/c_helper.lib b/windows_kext/c_helper/x64/c_helper.lib new file mode 100644 index 0000000000000000000000000000000000000000..249648f77717e31a28cfefeb98f4637065339641 GIT binary patch literal 107038 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 F{{wY6d entry pointer of the driver. + +`device.rs`: +Holds the global state of the driver. +Initialization: Setting up global state, Filter engine and callouts. + +Portmaster communication: +The communication happens concurrently with the File read/write API. +That means when Pormtaster sends a command the kernel extension will start to process it and queue the result in the `IOQueue`. + +`fn read()` -> called on read request from Portmaster +- `IOQueue` holds all the events queued for Portmaster. + +Blocks until there is a element that can be poped or shutdown request is sent from Portmaster. +If there is more then one event in the queue it will write as much as it can in the supplied buffer. + +`fn write()` -> called on write request from Portmaster. +Used when Portmaster wants to send a command to kernel extension. +Verdict Response, GetLogs ... (see `protocol` for list of all the commands) + + +## Callouts + +`callouts.rs` -> defines the list of all used callouts in the kernel extension. + +ALE (Application Layer Enforcement) +https://learn.microsoft.com/en-us/windows/win32/fwp/application-layer-enforcement--ale- + +### ALE Auth + +Connection level filtering. It will make a decision based on the first packet of a connection. Works together with the packet layer to provide firewall functionality. +- **AleLayerOutboundV4** +- **AleLayerInboundV4** +- **AleLayerOutboundV6** +- **AleLayerInboundV6** + + +### ALE endpoint / resource assignment and release + +Used to listen for event when connection has ended. Does no filtering. +- **AleEndpointClosureV4, AleEndpointClosureV6** - Triggered when connection to an endpoint has ended. Usually only TCP is triggered. The triggered connection will be marked for deletion. + +- **AleResourceAssignmentV4, AleResourceAssignmentV6** -> only for logging (not used) +- AleResourceReleaseV4, AleResourceReleaseV6 -> Triggered when port is release from an application. The triggered connection/s will be marked for deletion. + +### Stream layer + +This layer works on the application OSI layer. Meaning that only the payload of the TCP/UDP connection will be available. +It is used for bandwidth monitoring. This functionality is completely separate from the rest of the system so it can be disabled or enabled without affect anything else. + +- **StreamLayerV4, StreamLayerV6** -> For TCP connections +- **DatagramDataLayerV4, DatagramDataLayerV6** -> For UDP connections + + +### Packet layer + +This layer handled each packet on the network OSI layer. Works together with ALE Auth layer to provide firewall functionality. +- **IPPacketOutboundV4, IPPacketOutboundV6** -> Triggered on every outbound packet. +- **IPPacketInboundV4, IPPacketInboundV6** -> Triggered on every inbound packet. diff --git a/windows_kext/driver/rust-toolchain b/windows_kext/driver/rust-toolchain new file mode 100644 index 00000000..2bf5ad04 --- /dev/null +++ b/windows_kext/driver/rust-toolchain @@ -0,0 +1 @@ +stable diff --git a/windows_kext/driver/src/ale_callouts.rs b/windows_kext/driver/src/ale_callouts.rs new file mode 100644 index 00000000..c45b7f4c --- /dev/null +++ b/windows_kext/driver/src/ale_callouts.rs @@ -0,0 +1,537 @@ +use crate::connection::{Connection, ConnectionV4, ConnectionV6, Direction, Verdict}; +use crate::connection_map::Key; +use crate::device::{Device, Packet}; + +use crate::info; +use smoltcp::wire::{ + IpAddress, IpProtocol, Ipv4Address, Ipv6Address, IPV4_HEADER_LEN, IPV6_HEADER_LEN, +}; +use wdk::filter_engine::callout_data::CalloutData; +use wdk::filter_engine::layer::{ + self, FieldsAleAuthConnectV4, FieldsAleAuthConnectV6, FieldsAleAuthRecvAcceptV4, + FieldsAleAuthRecvAcceptV6, ValueType, +}; +use wdk::filter_engine::net_buffer::NetBufferList; +use wdk::filter_engine::packet::{Injector, TransportPacketList}; + +// ALE Layers + +#[derive(Debug)] +#[allow(dead_code)] +struct AleLayerData { + is_ipv6: bool, + reauthorize: bool, + process_id: u64, + protocol: IpProtocol, + direction: Direction, + local_ip: IpAddress, + local_port: u16, + remote_ip: IpAddress, + remote_port: u16, + interface_index: u32, + sub_interface_index: u32, +} + +impl AleLayerData { + fn as_key(&self) -> Key { + let mut local_port = 0; + let mut remote_port = 0; + match self.protocol { + IpProtocol::Tcp | IpProtocol::Udp => { + local_port = self.local_port; + remote_port = self.remote_port; + } + _ => {} + } + + Key { + protocol: self.protocol, + local_address: self.local_ip, + local_port, + remote_address: self.remote_ip, + remote_port, + } + } +} + +fn get_protocol(data: &CalloutData, index: usize) -> IpProtocol { + IpProtocol::from(data.get_value_u8(index)) +} + +fn get_ipv4_address(data: &CalloutData, index: usize) -> IpAddress { + IpAddress::Ipv4(Ipv4Address::from_bytes( + &data.get_value_u32(index).to_be_bytes(), + )) +} + +fn get_ipv6_address(data: &CalloutData, index: usize) -> IpAddress { + IpAddress::Ipv6(Ipv6Address::from_bytes(data.get_value_byte_array16(index))) +} + +pub fn ale_layer_connect_v4(data: CalloutData) { + type Fields = FieldsAleAuthConnectV4; + let ale_data = AleLayerData { + is_ipv6: false, + reauthorize: data.is_reauthorize(Fields::Flags as usize), + process_id: data.get_process_id().unwrap_or(0), + protocol: get_protocol(&data, Fields::IpProtocol as usize), + direction: Direction::Outbound, + local_ip: get_ipv4_address(&data, Fields::IpLocalAddress as usize), + local_port: data.get_value_u16(Fields::IpLocalPort as usize), + remote_ip: get_ipv4_address(&data, Fields::IpRemoteAddress as usize), + remote_port: data.get_value_u16(Fields::IpRemotePort as usize), + interface_index: 0, + sub_interface_index: 0, + }; + + ale_layer_auth(data, ale_data); +} + +pub fn ale_layer_accept_v4(data: CalloutData) { + type Fields = FieldsAleAuthRecvAcceptV4; + let ale_data = AleLayerData { + is_ipv6: false, + reauthorize: data.is_reauthorize(Fields::Flags as usize), + process_id: data.get_process_id().unwrap_or(0), + protocol: get_protocol(&data, Fields::IpProtocol as usize), + direction: Direction::Inbound, + local_ip: get_ipv4_address(&data, Fields::IpLocalAddress as usize), + local_port: data.get_value_u16(Fields::IpLocalPort as usize), + remote_ip: get_ipv4_address(&data, Fields::IpRemoteAddress as usize), + remote_port: data.get_value_u16(Fields::IpRemotePort as usize), + interface_index: data.get_value_u32(Fields::InterfaceIndex as usize), + sub_interface_index: data.get_value_u32(Fields::SubInterfaceIndex as usize), + }; + ale_layer_auth(data, ale_data); +} + +pub fn ale_layer_connect_v6(data: CalloutData) { + type Fields = FieldsAleAuthConnectV6; + + let ale_data = AleLayerData { + is_ipv6: true, + reauthorize: data.is_reauthorize(Fields::Flags as usize), + process_id: data.get_process_id().unwrap_or(0), + protocol: get_protocol(&data, Fields::IpProtocol as usize), + direction: Direction::Outbound, + local_ip: get_ipv6_address(&data, Fields::IpLocalAddress as usize), + local_port: data.get_value_u16(Fields::IpLocalPort as usize), + remote_ip: get_ipv6_address(&data, Fields::IpRemoteAddress as usize), + remote_port: data.get_value_u16(Fields::IpRemotePort as usize), + interface_index: data.get_value_u32(Fields::InterfaceIndex as usize), + sub_interface_index: data.get_value_u32(Fields::SubInterfaceIndex as usize), + }; + + ale_layer_auth(data, ale_data); +} + +pub fn ale_layer_accept_v6(data: CalloutData) { + type Fields = FieldsAleAuthRecvAcceptV6; + let ale_data = AleLayerData { + is_ipv6: true, + reauthorize: data.is_reauthorize(Fields::Flags as usize), + process_id: data.get_process_id().unwrap_or(0), + protocol: get_protocol(&data, Fields::IpProtocol as usize), + direction: Direction::Inbound, + local_ip: get_ipv6_address(&data, Fields::IpLocalAddress as usize), + local_port: data.get_value_u16(Fields::IpLocalPort as usize), + remote_ip: get_ipv6_address(&data, Fields::IpRemoteAddress as usize), + remote_port: data.get_value_u16(Fields::IpRemotePort as usize), + interface_index: data.get_value_u32(Fields::InterfaceIndex as usize), + sub_interface_index: data.get_value_u32(Fields::SubInterfaceIndex as usize), + }; + ale_layer_auth(data, ale_data); +} + +fn ale_layer_auth(mut data: CalloutData, ale_data: AleLayerData) { + let Some(device) = crate::entry::get_device() else { + return; + }; + + match ale_data.protocol { + IpProtocol::Tcp | IpProtocol::Udp => { + // Only TCP and UDP make sense to be supported in the ALE layer. + // Everything else is not associated with a connection and will be handled in the packet layer. + } + _ => { + // Outbound: Will be handled by packet layer next. + // Inbound: Was already handled by the packet layer. + data.action_permit(); + return; + } + } + + let key = ale_data.as_key(); + + // Check if connection is already in cache. + let verdict = if ale_data.is_ipv6 { + device + .connection_cache + .read_connection_v6(&key, |conn| -> Option { + // Function is behind spin lock, just copy and return. + Some(conn.verdict) + }) + } else { + device + .connection_cache + .read_connection_v4(&ale_data.as_key(), |conn| -> Option { + // Function is behind spin lock, just copy and return. + Some(conn.verdict) + }) + }; + + // Connection already in cache. + if let Some(verdict) = verdict { + crate::dbg!("processing existing connection: {} {}", key, verdict); + match verdict { + // No verdict yet + Verdict::Undecided => { + crate::dbg!("saving packet: {}", key); + // Connection is already pended. Save packet and wait for verdict. + match save_packet(device, &mut data, &ale_data, false) { + Ok(packet) => { + let info = device.packet_cache.push( + (key, packet), + ale_data.process_id, + ale_data.direction, + true, + ); + if let Some(info) = info { + let _ = device.event_queue.push(info); + } + } + Err(err) => { + crate::err!("failed to pend packet: {}", err); + } + }; + data.block_and_absorb(); + } + // There is a verdict + Verdict::PermanentAccept + | Verdict::Accept + | Verdict::RedirectNameServer + | Verdict::RedirectTunnel => { + // Continue to packet layer. + data.action_permit(); + } + Verdict::PermanentBlock | Verdict::Undeterminable | Verdict::Failed => { + // Packet layer will not see this connection. + crate::dbg!("permanent block {}", key); + data.action_block(); + } + Verdict::PermanentDrop => { + // Packet layer will not see this connection. + crate::dbg!("permanent drop {}", key); + data.block_and_absorb(); + } + Verdict::Block => { + if let Direction::Outbound = ale_data.direction { + // Handled by packet layer. + data.action_permit(); + } else { + // packet layer will still see the packets. + data.action_block(); + } + } + Verdict::Drop => { + if let Direction::Outbound = ale_data.direction { + // Handled by packet layer. + data.action_permit(); + } else { + // packet layer will still see the packets. + data.block_and_absorb(); + } + } + } + } else { + crate::dbg!("pending connection: {} {}", key, ale_data.direction); + // Only first packet of a connection can be pended: reauthorize == false + let can_pend_connection = !ale_data.reauthorize; + match save_packet(device, &mut data, &ale_data, can_pend_connection) { + Ok(packet) => { + let info = device.packet_cache.push( + (key, packet), + ale_data.process_id, + ale_data.direction, + true, + ); + if let Some(info) = info { + let _ = device.event_queue.push(info); + } + } + Err(err) => { + crate::err!("failed to pend packet: {}", err); + } + }; + + // Connection is not in cache, add it. + crate::dbg!("adding connection: {} PID: {}", key, ale_data.process_id); + if ale_data.is_ipv6 { + let conn = + ConnectionV6::from_key(&key, ale_data.process_id, ale_data.direction).unwrap(); + device.connection_cache.add_connection_v6(conn); + } else { + let conn = + ConnectionV4::from_key(&key, ale_data.process_id, ale_data.direction).unwrap(); + device.connection_cache.add_connection_v4(conn); + } + + // Drop packet. It will be re-injected after Portmaster returns a verdict. + data.block_and_absorb(); + } +} + +fn save_packet( + device: &Device, + callout_data: &mut CalloutData, + ale_data: &AleLayerData, + pend: bool, +) -> Result { + let mut packet_list = None; + let mut save_packet_list = true; + match ale_data.protocol { + IpProtocol::Tcp => { + if let Direction::Outbound = ale_data.direction { + // Only time a packet data is missing is during connect state of outbound TCP connection. + // Don't save packet list only if connection is outbound, reauthorize is false and the protocol is TCP. + save_packet_list = ale_data.reauthorize; + } + } + _ => {} + }; + if save_packet_list { + packet_list = create_packet_list(device, callout_data, ale_data); + } + if pend && matches!(ale_data.protocol, IpProtocol::Tcp | IpProtocol::Udp) { + match callout_data.pend_operation(packet_list) { + Ok(classify_defer) => Ok(Packet::AleLayer(classify_defer)), + Err(err) => Err(alloc::format!("failed to defer connection: {}", err)), + } + } else { + Ok(Packet::AleLayer(callout_data.pend_filter_rest(packet_list))) + } +} + +fn create_packet_list( + device: &Device, + callout_data: &mut CalloutData, + ale_data: &AleLayerData, +) -> Option { + let mut nbl = NetBufferList::new(callout_data.get_layer_data() as _); + let mut inbound = false; + if let Direction::Inbound = ale_data.direction { + if ale_data.is_ipv6 { + nbl.retreat(IPV6_HEADER_LEN as u32, true); + } else { + nbl.retreat(IPV4_HEADER_LEN as u32, true); + } + inbound = true; + } + + let address: &[u8] = match &ale_data.remote_ip { + IpAddress::Ipv4(address) => &address.0, + IpAddress::Ipv6(address) => &address.0, + }; + if let Ok(clone) = nbl.clone(&device.network_allocator) { + return Some(Injector::from_ale_callout( + ale_data.is_ipv6, + callout_data, + clone, + address, + inbound, + ale_data.interface_index, + ale_data.sub_interface_index, + )); + } + return None; +} + +pub fn endpoint_closure_v4(data: CalloutData) { + type Fields = layer::FieldsAleEndpointClosureV4; + let Some(device) = crate::entry::get_device() else { + return; + }; + let ip_address_type = data.get_value_type(Fields::IpLocalAddress as usize); + if let ValueType::FwpUint32 = ip_address_type { + let key = Key { + protocol: get_protocol(&data, Fields::IpProtocol as usize), + local_address: get_ipv4_address(&data, Fields::IpLocalAddress as usize), + local_port: data.get_value_u16(Fields::IpLocalPort as usize), + remote_address: get_ipv4_address(&data, Fields::IpRemoteAddress as usize), + remote_port: data.get_value_u16(Fields::IpRemotePort as usize), + }; + + let conn = device.connection_cache.end_connection_v4(key); + if let Some(conn) = conn { + let info = protocol::info::connection_end_event_v4_info( + data.get_process_id().unwrap_or(0), + conn.get_direction() as u8, + u8::from(get_protocol(&data, Fields::IpProtocol as usize)), + conn.local_address.0, + conn.remote_address.0, + conn.local_port, + conn.remote_port, + ); + let _ = device.event_queue.push(info); + } + } else { + // Invalid ip address type. Just ignore the error. + // err!( + // device.logger, + // "unknown ipv4 address type: {:?}", + // ip_address_type + // ); + } +} + +pub fn endpoint_closure_v6(data: CalloutData) { + type Fields = layer::FieldsAleEndpointClosureV6; + let Some(device) = crate::entry::get_device() else { + return; + }; + let local_ip_address_type = data.get_value_type(Fields::IpLocalAddress as usize); + let remote_ip_address_type = data.get_value_type(Fields::IpRemoteAddress as usize); + + if let ValueType::FwpByteArray16Type = local_ip_address_type { + if let ValueType::FwpByteArray16Type = remote_ip_address_type { + let key = Key { + protocol: get_protocol(&data, Fields::IpProtocol as usize), + local_address: get_ipv6_address(&data, Fields::IpLocalAddress as usize), + local_port: data.get_value_u16(Fields::IpLocalPort as usize), + remote_address: get_ipv6_address(&data, Fields::IpRemoteAddress as usize), + remote_port: data.get_value_u16(Fields::IpRemotePort as usize), + }; + + let conn = device.connection_cache.end_connection_v6(key); + if let Some(conn) = conn { + let info = protocol::info::connection_end_event_v6_info( + data.get_process_id().unwrap_or(0), + conn.get_direction() as u8, + u8::from(get_protocol(&data, Fields::IpProtocol as usize)), + conn.local_address.0, + conn.remote_address.0, + conn.local_port, + conn.remote_port, + ); + let _ = device.event_queue.push(info); + } + } + } +} + +pub fn ale_resource_monitor(data: CalloutData) { + let Some(device) = crate::entry::get_device() else { + return; + }; + match data.layer { + layer::Layer::AleResourceAssignmentV4Discard => { + type Fields = layer::FieldsAleResourceAssignmentV4; + if let Some(conns) = device.connection_cache.end_all_on_port_v4(( + get_protocol(&data, Fields::IpProtocol as usize), + data.get_value_u16(Fields::IpLocalPort as usize), + )) { + let process_id = data.get_process_id().unwrap_or(0); + info!( + "Port {}/{} Ipv4 assign request discarded pid={}", + data.get_value_u16(Fields::IpLocalPort as usize), + get_protocol(&data, Fields::IpProtocol as usize), + process_id, + ); + for conn in conns { + let info = protocol::info::connection_end_event_v4_info( + process_id, + conn.get_direction() as u8, + data.get_value_u8(Fields::IpProtocol as usize), + conn.local_address.0, + conn.remote_address.0, + conn.local_port, + conn.remote_port, + ); + let _ = device.event_queue.push(info); + } + } + } + layer::Layer::AleResourceAssignmentV6Discard => { + type Fields = layer::FieldsAleResourceAssignmentV6; + if let Some(conns) = device.connection_cache.end_all_on_port_v6(( + get_protocol(&data, Fields::IpProtocol as usize), + data.get_value_u16(Fields::IpLocalPort as usize), + )) { + let process_id = data.get_process_id().unwrap_or(0); + info!( + "Port {}/{} Ipv6 assign request discarded pid={}", + data.get_value_u16(Fields::IpLocalPort as usize), + get_protocol(&data, Fields::IpProtocol as usize), + process_id, + ); + for conn in conns { + let info = protocol::info::connection_end_event_v6_info( + process_id, + conn.get_direction() as u8, + data.get_value_u8(Fields::IpProtocol as usize), + conn.local_address.0, + conn.remote_address.0, + conn.local_port, + conn.remote_port, + ); + let _ = device.event_queue.push(info); + } + } + } + layer::Layer::AleResourceReleaseV4 => { + type Fields = layer::FieldsAleResourceReleaseV4; + if let Some(conns) = device.connection_cache.end_all_on_port_v4(( + get_protocol(&data, Fields::IpProtocol as usize), + data.get_value_u16(Fields::IpLocalPort as usize), + )) { + let process_id = data.get_process_id().unwrap_or(0); + info!( + "Port {}/{} released pid={}", + data.get_value_u16(Fields::IpLocalPort as usize), + get_protocol(&data, Fields::IpProtocol as usize), + process_id, + ); + for conn in conns { + let info = protocol::info::connection_end_event_v4_info( + process_id, + conn.get_direction() as u8, + data.get_value_u8(Fields::IpProtocol as usize), + conn.local_address.0, + conn.remote_address.0, + conn.local_port, + conn.remote_port, + ); + let _ = device.event_queue.push(info); + } + } + } + layer::Layer::AleResourceReleaseV6 => { + type Fields = layer::FieldsAleResourceReleaseV6; + if let Some(conns) = device.connection_cache.end_all_on_port_v6(( + get_protocol(&data, Fields::IpProtocol as usize), + data.get_value_u16(Fields::IpLocalPort as usize), + )) { + let process_id = data.get_process_id().unwrap_or(0); + info!( + "Port {}/{} released pid={}", + data.get_value_u16(Fields::IpLocalPort as usize), + get_protocol(&data, Fields::IpProtocol as usize), + process_id, + ); + for conn in conns { + let info = protocol::info::connection_end_event_v6_info( + process_id, + conn.get_direction() as u8, + data.get_value_u8(Fields::IpProtocol as usize), + conn.local_address.0, + conn.remote_address.0, + conn.local_port, + conn.remote_port, + ); + let _ = device.event_queue.push(info); + } + } + } + _ => {} + } +} diff --git a/windows_kext/driver/src/array_holder.rs b/windows_kext/driver/src/array_holder.rs new file mode 100644 index 00000000..077a2495 --- /dev/null +++ b/windows_kext/driver/src/array_holder.rs @@ -0,0 +1,25 @@ +use core::cell::RefCell; + +use alloc::vec::Vec; + +pub struct ArrayHolder(RefCell>>); +unsafe impl Sync for ArrayHolder {} + +impl ArrayHolder { + pub const fn default() -> Self { + Self(RefCell::new(None)) + } + + pub fn save(&self, data: &[u8]) { + if let Ok(mut opt) = self.0.try_borrow_mut() { + opt.replace(data.to_vec()); + } + } + + pub fn load(&self) -> Option> { + if let Ok(mut opt) = self.0.try_borrow_mut() { + return opt.take(); + } + None + } +} diff --git a/windows_kext/driver/src/bandwidth.rs b/windows_kext/driver/src/bandwidth.rs new file mode 100644 index 00000000..4fb48786 --- /dev/null +++ b/windows_kext/driver/src/bandwidth.rs @@ -0,0 +1,293 @@ +use protocol::info::{BandwidthValueV4, BandwidthValueV6, Info}; +use smoltcp::wire::{IpProtocol, Ipv4Address, Ipv6Address}; +use wdk::rw_spin_lock::RwSpinLock; + +use crate::driver_hashmap::DeviceHashMap; + +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)] +pub struct Key
+where + Address: Eq + PartialEq, +{ + pub local_ip: Address, + pub local_port: u16, + pub remote_ip: Address, + pub remote_port: u16, +} + +struct Value { + received_bytes: usize, + transmitted_bytes: usize, +} + +enum Direction { + Tx(usize), + Rx(usize), +} +pub struct Bandwidth { + stats_tcp_v4: DeviceHashMap, Value>, + stats_tcp_v4_lock: RwSpinLock, + + stats_tcp_v6: DeviceHashMap, Value>, + stats_tcp_v6_lock: RwSpinLock, + + stats_udp_v4: DeviceHashMap, Value>, + stats_udp_v4_lock: RwSpinLock, + + stats_udp_v6: DeviceHashMap, Value>, + stats_udp_v6_lock: RwSpinLock, +} + +impl Bandwidth { + pub fn new() -> Self { + Self { + stats_tcp_v4: DeviceHashMap::new(), + stats_tcp_v4_lock: RwSpinLock::default(), + + stats_tcp_v6: DeviceHashMap::new(), + stats_tcp_v6_lock: RwSpinLock::default(), + + stats_udp_v4: DeviceHashMap::new(), + stats_udp_v4_lock: RwSpinLock::default(), + + stats_udp_v6: DeviceHashMap::new(), + stats_udp_v6_lock: RwSpinLock::default(), + } + } + + pub fn get_all_updates_tcp_v4(&mut self) -> Option { + let stats_map; + { + let _guard = self.stats_tcp_v4_lock.write_lock(); + if self.stats_tcp_v4.is_empty() { + return None; + } + stats_map = core::mem::replace(&mut self.stats_tcp_v4, DeviceHashMap::new()); + } + + let mut values = alloc::vec::Vec::with_capacity(stats_map.len()); + for (key, value) in stats_map.iter() { + values.push(BandwidthValueV4 { + local_ip: key.local_ip.0, + local_port: key.local_port, + remote_ip: key.remote_ip.0, + remote_port: key.remote_port, + transmitted_bytes: value.transmitted_bytes as u64, + received_bytes: value.received_bytes as u64, + }); + } + Some(protocol::info::bandiwth_stats_array_v4( + u8::from(IpProtocol::Tcp), + values, + )) + } + + pub fn get_all_updates_tcp_v6(&mut self) -> Option { + let stats_map; + { + let _guard = self.stats_tcp_v6_lock.write_lock(); + if self.stats_tcp_v6.is_empty() { + return None; + } + stats_map = core::mem::replace(&mut self.stats_tcp_v6, DeviceHashMap::new()); + } + + let mut values = alloc::vec::Vec::with_capacity(stats_map.len()); + for (key, value) in stats_map.iter() { + values.push(BandwidthValueV6 { + local_ip: key.local_ip.0, + local_port: key.local_port, + remote_ip: key.remote_ip.0, + remote_port: key.remote_port, + transmitted_bytes: value.transmitted_bytes as u64, + received_bytes: value.received_bytes as u64, + }); + } + Some(protocol::info::bandiwth_stats_array_v6( + u8::from(IpProtocol::Tcp), + values, + )) + } + + pub fn get_all_updates_udp_v4(&mut self) -> Option { + let stats_map; + { + let _guard = self.stats_udp_v4_lock.write_lock(); + if self.stats_udp_v4.is_empty() { + return None; + } + stats_map = core::mem::replace(&mut self.stats_udp_v4, DeviceHashMap::new()); + } + + let mut values = alloc::vec::Vec::with_capacity(stats_map.len()); + for (key, value) in stats_map.iter() { + values.push(BandwidthValueV4 { + local_ip: key.local_ip.0, + local_port: key.local_port, + remote_ip: key.remote_ip.0, + remote_port: key.remote_port, + transmitted_bytes: value.transmitted_bytes as u64, + received_bytes: value.received_bytes as u64, + }); + } + Some(protocol::info::bandiwth_stats_array_v4( + u8::from(IpProtocol::Udp), + values, + )) + } + + pub fn get_all_updates_udp_v6(&mut self) -> Option { + let stats_map; + { + let _guard = self.stats_udp_v6_lock.write_lock(); + if self.stats_tcp_v6.is_empty() { + return None; + } + stats_map = core::mem::replace(&mut self.stats_tcp_v6, DeviceHashMap::new()); + } + + let mut values = alloc::vec::Vec::with_capacity(stats_map.len()); + for (key, value) in stats_map.iter() { + values.push(BandwidthValueV6 { + local_ip: key.local_ip.0, + local_port: key.local_port, + remote_ip: key.remote_ip.0, + remote_port: key.remote_port, + transmitted_bytes: value.transmitted_bytes as u64, + received_bytes: value.received_bytes as u64, + }); + } + Some(protocol::info::bandiwth_stats_array_v6( + u8::from(IpProtocol::Udp), + values, + )) + } + + pub fn update_tcp_v4_tx(&mut self, key: Key, tx_bytes: usize) { + Self::update( + &mut self.stats_tcp_v4, + &mut self.stats_tcp_v4_lock, + key, + Direction::Tx(tx_bytes), + ); + } + + pub fn update_tcp_v4_rx(&mut self, key: Key, rx_bytes: usize) { + Self::update( + &mut self.stats_tcp_v4, + &mut self.stats_tcp_v4_lock, + key, + Direction::Rx(rx_bytes), + ); + } + + pub fn update_tcp_v6_tx(&mut self, key: Key, tx_bytes: usize) { + Self::update( + &mut self.stats_tcp_v6, + &mut self.stats_tcp_v6_lock, + key, + Direction::Tx(tx_bytes), + ); + } + + pub fn update_tcp_v6_rx(&mut self, key: Key, rx_bytes: usize) { + Self::update( + &mut self.stats_tcp_v6, + &mut self.stats_tcp_v6_lock, + key, + Direction::Rx(rx_bytes), + ); + } + + pub fn update_udp_v4_tx(&mut self, key: Key, tx_bytes: usize) { + Self::update( + &mut self.stats_udp_v4, + &mut self.stats_udp_v4_lock, + key, + Direction::Tx(tx_bytes), + ); + } + + pub fn update_udp_v4_rx(&mut self, key: Key, rx_bytes: usize) { + Self::update( + &mut self.stats_udp_v4, + &mut self.stats_udp_v4_lock, + key, + Direction::Rx(rx_bytes), + ); + } + + pub fn update_udp_v6_tx(&mut self, key: Key, tx_bytes: usize) { + Self::update( + &mut self.stats_udp_v6, + &mut self.stats_udp_v6_lock, + key, + Direction::Tx(tx_bytes), + ); + } + + pub fn update_udp_v6_rx(&mut self, key: Key, rx_bytes: usize) { + Self::update( + &mut self.stats_udp_v6, + &mut self.stats_udp_v6_lock, + key, + Direction::Rx(rx_bytes), + ); + } + + fn update( + map: &mut DeviceHashMap, Value>, + lock: &mut RwSpinLock, + key: Key
, + bytes: Direction, + ) { + let _guard = lock.write_lock(); + if let Some(value) = map.get_mut(&key) { + match bytes { + Direction::Tx(bytes_count) => value.transmitted_bytes += bytes_count, + Direction::Rx(bytes_count) => value.received_bytes += bytes_count, + } + } else { + let mut received_bytes = 0; + let mut transmitted_bytes = 0; + match bytes { + Direction::Tx(bytes_count) => transmitted_bytes += bytes_count, + Direction::Rx(bytes_count) => received_bytes += bytes_count, + } + map.insert( + key, + Value { + received_bytes, + transmitted_bytes, + }, + ); + } + } + + #[allow(dead_code)] + pub fn get_entries_count(&self) -> usize { + let mut size = 0; + { + let values = &self.stats_tcp_v4.values(); + let _guard = self.stats_tcp_v4_lock.read_lock(); + size += values.len(); + } + { + let values = &self.stats_tcp_v6.values(); + let _guard = self.stats_tcp_v6_lock.read_lock(); + size += values.len(); + } + { + let values = &self.stats_udp_v4.values(); + let _guard = self.stats_udp_v4_lock.read_lock(); + size += values.len(); + } + { + let values = &self.stats_udp_v6.values(); + let _guard = self.stats_udp_v6_lock.read_lock(); + size += values.len(); + } + + return size; + } +} diff --git a/windows_kext/driver/src/callouts.rs b/windows_kext/driver/src/callouts.rs new file mode 100644 index 00000000..71701692 --- /dev/null +++ b/windows_kext/driver/src/callouts.rs @@ -0,0 +1,185 @@ +use alloc::vec::Vec; +use wdk::filter_engine::callout::FilterType; +use wdk::{ + consts, + filter_engine::{callout::Callout, layer::Layer}, +}; + +use crate::{ale_callouts, packet_callouts, stream_callouts}; + +pub fn get_callout_vec() -> Vec { + alloc::vec![ + // ----------------------------------------- + // ALE Auth layers + Callout::new( + "AleLayerOutboundV4", + "ALE layer for outbound connection for ipv4", + 0x58545073_f893_454c_bbea_a57bc964f46d, + Layer::AleAuthConnectV4, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::Resettable, + ale_callouts::ale_layer_connect_v4, + ), + Callout::new( + "AleLayerInboundV4", + "ALE layer for inbound connections for ipv4", + 0xc6021395_0724_4e2c_ae20_3dde51fc3c68, + Layer::AleAuthRecvAcceptV4, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::Resettable, + ale_callouts::ale_layer_accept_v4, + ), + Callout::new( + "AleLayerOutboundV6", + "ALE layer for outbound connections for ipv6", + 0x4bd2a080_2585_478d_977c_7f340c6bc3d4, + Layer::AleAuthConnectV6, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::Resettable, + ale_callouts::ale_layer_connect_v6, + ), + Callout::new( + "AleLayerInboundV6", + "ALE layer for inbound connections for ipv6", + 0xd24480da_38fa_4099_9383_b5c83b69e4f2, + Layer::AleAuthRecvAcceptV6, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::Resettable, + ale_callouts::ale_layer_accept_v6, + ), + // ----------------------------------------- + // ALE connection end layers + Callout::new( + "AleEndpointClosureV4", + "ALE layer for indicating closing of connection for ipv4", + 0x58f02845_ace9_4455_ac80_8a84b86fe566, + Layer::AleEndpointClosureV4, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + ale_callouts::endpoint_closure_v4, + ), + Callout::new( + "AleEndpointClosureV6", + "ALE layer for indicating closing of connection for ipv6", + 0x2bc82359_9dc5_4315_9c93_c89467e283ce, + Layer::AleEndpointClosureV6, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + ale_callouts::endpoint_closure_v6, + ), + // ----------------------------------------- + // ALE resource assignment and release. + // Callout::new( + // "AleResourceAssignmentV4", + // "Ipv4 Port assignment monitoring", + // 0x6b9d1985_6f75_4d05_b9b5_1607e187906f, + // Layer::AleResourceAssignmentV4Discard, + // consts::FWP_ACTION_CALLOUT_INSPECTION, + // FilterType::NonResettable, + // ale_callouts::ale_resource_monitor, + // ), + Callout::new( + "AleResourceReleaseV4", + "Ipv4 Port release monitor", + 0x7b513bb3_a0be_4f77_a4bc_03c052abe8d7, + Layer::AleResourceReleaseV4, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + ale_callouts::ale_resource_monitor, + ), + // Callout::new( + // "AleResourceAssignmentV6", + // "Ipv4 Port assignment monitor", + // 0xb0d02299_3d3e_437d_916a_f0e96a60cc18, + // Layer::AleResourceAssignmentV6Discard, + // consts::FWP_ACTION_CALLOUT_INSPECTION, + // FilterType::NonResettable, + // ale_callouts::ale_resource_monitor, + // ), + Callout::new( + "AleResourceReleaseV6", + "Ipv6 Port release monitor", + 0x6cf36e04_e656_42c3_8cac_a1ce05328bd1, + Layer::AleResourceReleaseV6, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + ale_callouts::ale_resource_monitor, + ), + // ----------------------------------------- + // Stream layer + Callout::new( + "StreamLayerV4", + "Stream layer for ipv4", + 0xe2ca13bf_9710_4caa_a45c_e8c78b5ac780, + Layer::StreamV4, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + stream_callouts::stream_layer_tcp_v4, + ), + Callout::new( + "StreamLayerV6", + "Stream layer for ipv6", + 0x66c549b3_11e2_4b27_8f73_856e6fd82baa, + Layer::StreamV6, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + stream_callouts::stream_layer_tcp_v6, + ), + Callout::new( + "DatagramDataLayerV4", + "DatagramData layer for ipv4", + 0xe7eeeaba_168a_45bb_8747_e1a702feb2c5, + Layer::DatagramDataV4, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + stream_callouts::stream_layer_udp_v4, + ), + Callout::new( + "DatagramDataLayerV6", + "DatagramData layer for ipv4", + 0xb25862cd_f744_4452_b14a_d0c1e5a25b30, + Layer::DatagramDataV6, + consts::FWP_ACTION_CALLOUT_INSPECTION, + FilterType::NonResettable, + stream_callouts::stream_layer_udp_v6, + ), + // ----------------------------------------- + // Packet layers + Callout::new( + "IPPacketOutboundV4", + "IP packet outbound network layer callout for Ipv4", + 0xf3183afe_dc35_49f1_8ea2_b16b5666dd36, + Layer::OutboundIppacketV4, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::NonResettable, + packet_callouts::ip_packet_layer_outbound_v4, + ), + Callout::new( + "IPPacketInboundV4", + "IP packet inbound network layer callout for Ipv4", + 0xf0369374_203d_4bf0_83d2_b2ad3cc17a50, + Layer::InboundIppacketV4, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::NonResettable, + packet_callouts::ip_packet_layer_inbound_v4, + ), + Callout::new( + "IPPacketOutboundV6", + "IP packet outbound network layer callout for Ipv6", + 0x91daf8bc_0908_4bf8_9f81_2c538ab8f25a, + Layer::OutboundIppacketV6, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::NonResettable, + packet_callouts::ip_packet_layer_outbound_v6, + ), + Callout::new( + "IPPacketInboundV6", + "IP packet inbound network layer callout for Ipv6", + 0xfe9faf5f_ceb2_4cd9_9995_f2f2b4f5fcc0, + Layer::InboundIppacketV6, + consts::FWP_ACTION_CALLOUT_TERMINATING, + FilterType::NonResettable, + packet_callouts::ip_packet_layer_inbound_v6, + ) + ] +} diff --git a/windows_kext/driver/src/common.rs b/windows_kext/driver/src/common.rs new file mode 100644 index 00000000..70c01b46 --- /dev/null +++ b/windows_kext/driver/src/common.rs @@ -0,0 +1,59 @@ +#![allow(dead_code)] + +use core::fmt::Display; + +use num_derive::{FromPrimitive, ToPrimitive}; + +pub const ICMPV4_CODE_DESTINATION_UNREACHABLE: u32 = 3; +pub const ICMPV4_CODE_DU_PORT_UNREACHABLE: u32 = 3; // Destination Unreachable (Port unreachable) ; +pub const ICMPV4_CODE_DU_ADMINISTRATIVELY_PROHIBITED: u32 = 13; // Destination Unreachable (Communication Administratively Prohibited) ; + +pub const ICMPV6_CODE_DESTINATION_UNREACHABLE: u32 = 1; +pub const ICMPV6_CODE_DU_PORT_UNREACHABLE: u32 = 4; // Destination Unreachable (Port unreachable) ; + +enum Direction { + Outbound = 0, + Inbound = 1, +} + +const SIOCTL_TYPE: u32 = 40000; +macro_rules! ctl_code { + ($device_type:expr, $function:expr, $method:expr, $access:expr) => { + ($device_type << 16) | ($access << 14) | ($function << 2) | $method + }; +} + +pub const METHOD_BUFFERED: u32 = 0; +pub const METHOD_IN_DIRECT: u32 = 1; +pub const METHOD_OUT_DIRECT: u32 = 2; +pub const METHOD_NEITHER: u32 = 3; + +pub const FILE_READ_DATA: u32 = 0x0001; // file & pipe +pub const FILE_WRITE_DATA: u32 = 0x0002; // file & pipe + +#[repr(u32)] +#[derive(FromPrimitive, ToPrimitive)] +pub enum ControlCode { + Version = ctl_code!( + SIOCTL_TYPE, + 0x800, + METHOD_BUFFERED, + FILE_READ_DATA | FILE_WRITE_DATA + ), + ShutdownRequest = ctl_code!( + SIOCTL_TYPE, + 0x801, + METHOD_BUFFERED, + FILE_READ_DATA | FILE_WRITE_DATA + ), +} + +impl Display for ControlCode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ControlCode::Version => _ = write!(f, "Version"), + ControlCode::ShutdownRequest => _ = write!(f, "Shutdown"), + }; + return Ok(()); + } +} diff --git a/windows_kext/driver/src/connection.rs b/windows_kext/driver/src/connection.rs new file mode 100644 index 00000000..f969ac84 --- /dev/null +++ b/windows_kext/driver/src/connection.rs @@ -0,0 +1,499 @@ +use alloc::{ + boxed::Box, + string::{String, ToString}, +}; +use core::{ + fmt::{Debug, Display}, + sync::atomic::{AtomicU64, Ordering}, +}; +use num_derive::FromPrimitive; +use smoltcp::wire::{IpAddress, IpProtocol, Ipv4Address, Ipv6Address}; + +use crate::connection_map::Key; + +pub static PM_DNS_PORT: u16 = 53; +pub static PM_SPN_PORT: u16 = 717; + +// Make sure this in sync with the Go version +#[derive(Copy, Clone, FromPrimitive)] +#[repr(u8)] +#[rustfmt::skip] +pub enum Verdict { + Undecided = 0, // Undecided is the default status of new connections. + Undeterminable = 1, + Accept = 2, + PermanentAccept = 3, + Block = 4, + PermanentBlock = 5, + Drop = 6, + PermanentDrop = 7, + RedirectNameServer = 8, + RedirectTunnel = 9, + Failed = 10, +} + +impl Display for Verdict { + #[rustfmt::skip] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Verdict::Undecided => write!(f, "Undecided"), + Verdict::Undeterminable => write!(f, "Undeterminable"), + Verdict::Accept => write!(f, "Accept"), + Verdict::PermanentAccept => write!(f, "PermanentAccept"), + Verdict::Block => write!(f, "Block"), + Verdict::PermanentBlock => write!(f, "PermanentBlock"), + Verdict::Drop => write!(f, "Drop"), + Verdict::PermanentDrop => write!(f, "PermanentDrop"), + Verdict::RedirectNameServer => write!(f, "RedirectNameServer"), + Verdict::RedirectTunnel => write!(f, "RedirectTunnel"), + Verdict::Failed => write!(f, "Failed"), + } + } +} + +#[allow(dead_code)] +impl Verdict { + /// Returns true if the verdict is a redirect. + pub fn is_redirect(&self) -> bool { + matches!(self, Verdict::RedirectNameServer | Verdict::RedirectTunnel) + } + + /// Returns true if the verdict is a permanent verdict. + pub fn is_permanent(&self) -> bool { + matches!( + self, + Verdict::PermanentAccept + | Verdict::PermanentBlock + | Verdict::PermanentDrop + | Verdict::RedirectNameServer + | Verdict::RedirectTunnel + ) + } +} + +/// Direction of the connection. +#[derive(Copy, Clone, FromPrimitive)] +#[repr(u8)] +pub enum Direction { + Outbound = 0, + Inbound = 1, +} + +impl Display for Direction { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Direction::Outbound => write!(f, "Outbound"), + Direction::Inbound => write!(f, "Inbound"), + } + } +} + +impl Debug for Direction { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self) + } +} + +#[derive(Clone)] +pub struct ConnectionExtra { + pub(crate) end_timestamp: u64, + pub(crate) direction: Direction, +} + +pub trait Connection { + fn redirect_info(&self) -> Option { + let redirect_address = if self.is_ipv6() { + IpAddress::Ipv6(Ipv6Address::LOOPBACK) + } else { + IpAddress::Ipv4(Ipv4Address::new(127, 0, 0, 1)) + }; + + match self.get_verdict() { + Verdict::RedirectNameServer => Some(RedirectInfo { + local_address: self.get_local_address(), + remote_address: self.get_remote_address(), + remote_port: self.get_remote_port(), + redirect_port: PM_DNS_PORT, + unify: false, + redirect_address, + }), + Verdict::RedirectTunnel => Some(RedirectInfo { + local_address: self.get_local_address(), + remote_address: self.get_remote_address(), + remote_port: self.get_remote_port(), + redirect_port: PM_SPN_PORT, + unify: true, + redirect_address, + }), + _ => None, + } + } + + /// Returns the key of the connection. + fn get_key(&self) -> Key { + Key { + protocol: self.get_protocol(), + local_address: self.get_local_address(), + local_port: self.get_local_port(), + remote_address: self.get_remote_address(), + remote_port: self.get_remote_port(), + } + } + + /// Returns true if the connection is equal to the given key. The key is considered equal if the remote port and address are equal. + fn remote_equals(&self, key: &Key) -> bool; + /// Returns true if the connection is equal to the given key for redirecting. The key is considered equal if the remote port and address are equal. + fn redirect_equals(&self, key: &Key) -> bool; + /// Returns the protocol of the connection. + fn get_protocol(&self) -> IpProtocol; + /// Returns the verdict of the connection. + fn get_verdict(&self) -> Verdict; + /// Returns the local address of the connection. + fn get_local_address(&self) -> IpAddress; + /// Returns the local port of the connection. + fn get_local_port(&self) -> u16; + /// Returns the remote address of the connection. + fn get_remote_address(&self) -> IpAddress; + /// Returns the remote port of the connection. + fn get_remote_port(&self) -> u16; + /// Returns true if the connection is an IPv6 connection. + fn is_ipv6(&self) -> bool; + /// Returns the direction of the connection. + fn get_direction(&self) -> Direction; + // Returns the process id of the connection. + fn get_process_id(&self) -> u64; + /// Ends the connection. + fn end(&mut self, timestamp: u64); + /// Returns true if the connection has ended. + fn has_ended(&self) -> bool { + self.get_end_time() > 0 + } + /// Returns the timestamp when the connection ended. + fn get_end_time(&self) -> u64; + /// Returns the timestamp when the connection was last accessed. + fn get_last_accessed_time(&self) -> u64; + /// Sets the timestamp when the connection was last accessed. + fn set_last_accessed_time(&self, timestamp: u64); +} + +pub struct ConnectionV4 { + pub(crate) protocol: IpProtocol, + pub(crate) local_address: Ipv4Address, + pub(crate) local_port: u16, + pub(crate) remote_address: Ipv4Address, + pub(crate) remote_port: u16, + pub(crate) verdict: Verdict, + pub(crate) process_id: u64, + pub(crate) last_accessed_timestamp: AtomicU64, + pub(crate) extra: Box, +} + +pub struct ConnectionV6 { + pub(crate) protocol: IpProtocol, + pub(crate) local_address: Ipv6Address, + pub(crate) local_port: u16, + pub(crate) remote_address: Ipv6Address, + pub(crate) remote_port: u16, + pub(crate) verdict: Verdict, + pub(crate) process_id: u64, + pub(crate) last_accessed_timestamp: AtomicU64, + pub(crate) extra: Box, +} + +#[derive(Debug)] +pub struct RedirectInfo { + pub(crate) local_address: IpAddress, + pub(crate) remote_address: IpAddress, + pub(crate) remote_port: u16, + pub(crate) redirect_port: u16, + pub(crate) unify: bool, + pub(crate) redirect_address: IpAddress, +} + +impl ConnectionV4 { + /// Creates a new ipv4 connection from the given key. + pub fn from_key(key: &Key, process_id: u64, direction: Direction) -> Result { + let IpAddress::Ipv4(local_address) = key.local_address else { + return Err("wrong ip address version".to_string()); + }; + + let IpAddress::Ipv4(remote_address) = key.remote_address else { + return Err("wrong ip address version".to_string()); + }; + + let timestamp = wdk::utils::get_system_timestamp_ms(); + + Ok(Self { + protocol: key.protocol, + local_address, + local_port: key.local_port, + remote_address, + remote_port: key.remote_port, + verdict: Verdict::Undecided, + process_id, + last_accessed_timestamp: AtomicU64::new(timestamp), + extra: Box::new(ConnectionExtra { + direction, + end_timestamp: 0, + }), + }) + } +} + +impl Connection for ConnectionV4 { + fn remote_equals(&self, key: &Key) -> bool { + if self.remote_port != key.remote_port { + return false; + } + if let IpAddress::Ipv4(remote_address) = &key.remote_address { + return self.remote_address.eq(remote_address); + } + false + } + + fn get_key(&self) -> Key { + Key { + protocol: self.protocol, + local_address: IpAddress::Ipv4(self.local_address), + local_port: self.local_port, + remote_address: IpAddress::Ipv4(self.remote_address), + remote_port: self.remote_port, + } + } + + fn redirect_equals(&self, key: &Key) -> bool { + match self.verdict { + Verdict::RedirectNameServer => { + if key.remote_port != PM_DNS_PORT { + return false; + } + + match key.remote_address { + IpAddress::Ipv4(a) => a.is_loopback(), + IpAddress::Ipv6(_) => false, + } + } + Verdict::RedirectTunnel => { + if key.remote_port != PM_SPN_PORT { + return false; + } + key.local_address.eq(&key.remote_address) + } + _ => false, + } + } + + fn get_protocol(&self) -> IpProtocol { + self.protocol + } + + fn get_verdict(&self) -> Verdict { + self.verdict + } + + fn get_local_address(&self) -> IpAddress { + IpAddress::Ipv4(self.local_address) + } + + fn get_local_port(&self) -> u16 { + self.local_port + } + + fn get_remote_address(&self) -> IpAddress { + IpAddress::Ipv4(self.remote_address) + } + + fn get_remote_port(&self) -> u16 { + self.remote_port + } + + fn is_ipv6(&self) -> bool { + false + } + + fn get_process_id(&self) -> u64 { + self.process_id + } + + fn get_direction(&self) -> Direction { + self.extra.direction + } + + fn end(&mut self, timestamp: u64) { + self.extra.end_timestamp = timestamp; + } + + fn get_end_time(&self) -> u64 { + self.extra.end_timestamp + } + + fn get_last_accessed_time(&self) -> u64 { + self.last_accessed_timestamp.load(Ordering::Relaxed) + } + + fn set_last_accessed_time(&self, timestamp: u64) { + self.last_accessed_timestamp + .store(timestamp, Ordering::Relaxed); + } +} + +impl Clone for ConnectionV4 { + fn clone(&self) -> Self { + Self { + protocol: self.protocol, + local_address: self.local_address, + local_port: self.local_port, + remote_address: self.remote_address, + remote_port: self.remote_port, + verdict: self.verdict, + process_id: self.process_id, + last_accessed_timestamp: AtomicU64::new( + self.last_accessed_timestamp.load(Ordering::Relaxed), + ), + extra: self.extra.clone(), + } + } +} + +impl ConnectionV6 { + /// Creates a new ipv6 connection from the given key. + pub fn from_key(key: &Key, process_id: u64, direction: Direction) -> Result { + let IpAddress::Ipv6(local_address) = key.local_address else { + return Err("wrong ip address version".to_string()); + }; + + let IpAddress::Ipv6(remote_address) = key.remote_address else { + return Err("wrong ip address version".to_string()); + }; + let timestamp = wdk::utils::get_system_timestamp_ms(); + + Ok(Self { + protocol: key.protocol, + local_address, + local_port: key.local_port, + remote_address, + remote_port: key.remote_port, + verdict: Verdict::Undecided, + process_id, + last_accessed_timestamp: AtomicU64::new(timestamp), + extra: Box::new(ConnectionExtra { + direction, + end_timestamp: 0, + }), + }) + } +} + +impl Connection for ConnectionV6 { + fn remote_equals(&self, key: &Key) -> bool { + if self.remote_port != key.remote_port { + return false; + } + if let IpAddress::Ipv6(remote_address) = &key.remote_address { + return self.remote_address.eq(remote_address); + } + false + } + fn get_key(&self) -> Key { + Key { + protocol: self.protocol, + local_address: IpAddress::Ipv6(self.local_address), + local_port: self.local_port, + remote_address: IpAddress::Ipv6(self.remote_address), + remote_port: self.remote_port, + } + } + + fn redirect_equals(&self, key: &Key) -> bool { + match self.verdict { + Verdict::RedirectNameServer => { + if key.remote_port != PM_DNS_PORT { + return false; + } + + match key.remote_address { + IpAddress::Ipv4(_) => false, + IpAddress::Ipv6(a) => a.is_loopback(), + } + } + Verdict::RedirectTunnel => { + if key.remote_port != PM_SPN_PORT { + return false; + } + key.local_address.eq(&key.remote_address) + } + _ => false, + } + } + + fn get_protocol(&self) -> IpProtocol { + self.protocol + } + + fn get_verdict(&self) -> Verdict { + self.verdict + } + + fn get_local_address(&self) -> IpAddress { + IpAddress::Ipv6(self.local_address) + } + + fn get_local_port(&self) -> u16 { + self.local_port + } + + fn get_remote_address(&self) -> IpAddress { + IpAddress::Ipv6(self.remote_address) + } + + fn get_remote_port(&self) -> u16 { + self.remote_port + } + + fn is_ipv6(&self) -> bool { + true + } + + fn get_process_id(&self) -> u64 { + self.process_id + } + + fn get_direction(&self) -> Direction { + self.extra.direction + } + + fn end(&mut self, timestamp: u64) { + self.extra.end_timestamp = timestamp; + } + + fn get_end_time(&self) -> u64 { + self.extra.end_timestamp + } + + fn get_last_accessed_time(&self) -> u64 { + self.last_accessed_timestamp.load(Ordering::Relaxed) + } + + fn set_last_accessed_time(&self, timestamp: u64) { + self.last_accessed_timestamp + .store(timestamp, Ordering::Relaxed); + } +} + +impl Clone for ConnectionV6 { + fn clone(&self) -> Self { + Self { + protocol: self.protocol, + local_address: self.local_address, + local_port: self.local_port, + remote_address: self.remote_address, + remote_port: self.remote_port, + verdict: self.verdict, + process_id: self.process_id, + last_accessed_timestamp: AtomicU64::new( + self.last_accessed_timestamp.load(Ordering::Relaxed), + ), + extra: self.extra.clone(), + } + } +} diff --git a/windows_kext/driver/src/connection_cache.rs b/windows_kext/driver/src/connection_cache.rs new file mode 100644 index 00000000..6e149508 --- /dev/null +++ b/windows_kext/driver/src/connection_cache.rs @@ -0,0 +1,200 @@ +use core::time::Duration; + +use crate::{ + connection::{Connection, ConnectionV4, ConnectionV6, RedirectInfo, Verdict}, + connection_map::{ConnectionMap, Key}, +}; +use alloc::{format, string::String, vec::Vec}; + +use smoltcp::wire::IpProtocol; +use wdk::rw_spin_lock::RwSpinLock; + +pub struct ConnectionCache { + connections_v4: ConnectionMap, + connections_v6: ConnectionMap, + lock_v4: RwSpinLock, + lock_v6: RwSpinLock, +} + +impl ConnectionCache { + pub fn new() -> Self { + Self { + connections_v4: ConnectionMap::new(), + connections_v6: ConnectionMap::new(), + lock_v4: RwSpinLock::default(), + lock_v6: RwSpinLock::default(), + } + } + + pub fn add_connection_v4(&mut self, connection: ConnectionV4) { + let _guard = self.lock_v4.write_lock(); + self.connections_v4.add(connection); + } + + pub fn add_connection_v6(&mut self, connection: ConnectionV6) { + let _guard = self.lock_v6.write_lock(); + self.connections_v6.add(connection); + } + + pub fn update_connection(&mut self, key: Key, verdict: Verdict) -> Option { + if key.is_ipv6() { + let _guard = self.lock_v6.write_lock(); + if let Some(conn) = self.connections_v6.get_mut(&key) { + conn.verdict = verdict; + return conn.redirect_info(); + } + } else { + let _guard = self.lock_v4.write_lock(); + if let Some(conn) = self.connections_v4.get_mut(&key) { + conn.verdict = verdict; + return conn.redirect_info(); + } + } + None + } + + pub fn read_connection_v4( + &self, + key: &Key, + process_connection: fn(&ConnectionV4) -> Option, + ) -> Option { + let _guard = self.lock_v4.read_lock(); + self.connections_v4.read(&key, process_connection) + } + + pub fn read_connection_v6( + &self, + key: &Key, + process_connection: fn(&ConnectionV6) -> Option, + ) -> Option { + let _guard = self.lock_v6.read_lock(); + self.connections_v6.read(&key, process_connection) + } + + pub fn end_connection_v4(&mut self, key: Key) -> Option { + let _guard = self.lock_v4.write_lock(); + self.connections_v4.end(key) + } + + pub fn end_connection_v6(&mut self, key: Key) -> Option { + let _guard = self.lock_v6.write_lock(); + self.connections_v6.end(key) + } + + pub fn end_all_on_port_v4(&mut self, key: (IpProtocol, u16)) -> Option> { + let _guard = self.lock_v4.write_lock(); + self.connections_v4.end_all_on_port(key) + } + + pub fn end_all_on_port_v6(&mut self, key: (IpProtocol, u16)) -> Option> { + let _guard = self.lock_v6.write_lock(); + self.connections_v6.end_all_on_port(key) + } + + pub fn clean_ended_connections(&mut self) { + { + let _guard = self.lock_v4.write_lock(); + self.connections_v4.clean_ended_connections(); + } + { + let _guard = self.lock_v6.write_lock(); + self.connections_v6.clean_ended_connections(); + } + } + + pub fn clear(&mut self) { + { + let _guard = self.lock_v4.write_lock(); + self.connections_v4.clear(); + } + { + let _guard = self.lock_v6.write_lock(); + self.connections_v6.clear(); + } + } + + #[allow(dead_code)] + pub fn get_entries_count(&self) -> usize { + let mut size = 0; + { + let _guard = self.lock_v4.read_lock(); + size += self.connections_v4.get_count(); + } + + { + let _guard = self.lock_v6.read_lock(); + size += self.connections_v6.get_count(); + } + + return size; + } + + #[allow(dead_code)] + pub fn get_full_cache_info(&self) -> String { + let mut info = String::new(); + let now = wdk::utils::get_system_timestamp_ms(); + { + let _guard = self.lock_v4.read_lock(); + for ((protocol, port), connections) in self.connections_v4.iter() { + info.push_str(&format!("{} -> {}\n", protocol, port,)); + for conn in connections { + let active_time_seconds = + Duration::from_millis(now - conn.get_last_accessed_time()).as_secs(); + info.push_str(&format!( + "\t{}:{} -> {}:{} {} last active {}m {}s ago", + conn.local_address, + conn.local_port, + conn.remote_address, + conn.remote_port, + conn.verdict, + active_time_seconds / 60, + active_time_seconds % 60 + )); + if conn.has_ended() { + let end_time_seconds = + Duration::from_millis(now - conn.get_end_time()).as_secs(); + info.push_str(&format!( + "\t ended {}m {}s ago", + end_time_seconds / 60, + end_time_seconds % 60 + )); + } + info.push('\n'); + } + } + } + + { + let _guard = self.lock_v6.read_lock(); + for ((protocol, port), connections) in self.connections_v6.iter() { + info.push_str(&format!("{} -> {} \n", protocol, port)); + for conn in connections { + let active_time_seconds = + Duration::from_millis(now - conn.get_last_accessed_time()).as_secs(); + info.push_str(&format!( + "\t{}:{} -> {}:{} {} last active {}m {}s ago", + conn.local_address, + conn.local_port, + conn.remote_address, + conn.remote_port, + conn.verdict, + active_time_seconds / 60, + active_time_seconds % 60 + )); + if conn.has_ended() { + let end_time_seconds = + Duration::from_millis(now - conn.get_end_time()).as_secs(); + info.push_str(&format!( + "\t ended {}m {}s ago", + end_time_seconds / 60, + end_time_seconds % 60 + )); + } + info.push('\n'); + } + } + } + + return info; + } +} diff --git a/windows_kext/driver/src/connection_map.rs b/windows_kext/driver/src/connection_map.rs new file mode 100644 index 00000000..282da153 --- /dev/null +++ b/windows_kext/driver/src/connection_map.rs @@ -0,0 +1,179 @@ +use core::{fmt::Display, time::Duration}; + +use crate::connection::Connection; +use alloc::vec::Vec; +use hashbrown::HashMap; +use smoltcp::wire::{IpAddress, IpProtocol}; + +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] +pub struct Key { + pub(crate) protocol: IpProtocol, + pub(crate) local_address: IpAddress, + pub(crate) local_port: u16, + pub(crate) remote_address: IpAddress, + pub(crate) remote_port: u16, +} + +impl Display for Key { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "p: {} l: {}:{} r: {}:{}", + self.protocol, + self.local_address, + self.local_port, + self.remote_address, + self.remote_port + ) + } +} + +impl Key { + /// Returns the protocol and port as a tuple. + pub fn small(&self) -> (IpProtocol, u16) { + (self.protocol, self.local_port) + } + + /// Returns true if the local address is an IPv4 address. + pub fn is_ipv6(&self) -> bool { + match self.local_address { + IpAddress::Ipv4(_) => false, + IpAddress::Ipv6(_) => true, + } + } + + /// Returns true if the local address is a loopback address. + pub fn is_loopback(&self) -> bool { + match self.local_address { + IpAddress::Ipv4(ip) => ip.is_loopback(), + IpAddress::Ipv6(ip) => ip.is_loopback(), + } + } + + /// Returns a new key with the local and remote addresses and ports reversed. + #[allow(dead_code)] + pub fn reverse(&self) -> Key { + Key { + protocol: self.protocol, + local_address: self.remote_address, + local_port: self.remote_port, + remote_address: self.local_address, + remote_port: self.local_port, + } + } +} + +pub struct ConnectionMap(HashMap<(IpProtocol, u16), Vec>); + +impl ConnectionMap { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn add(&mut self, conn: T) { + let key = conn.get_key().small(); + if let Some(connections) = self.0.get_mut(&key) { + connections.push(conn); + } else { + self.0.insert(key, alloc::vec![conn]); + } + } + + pub fn get_mut(&mut self, key: &Key) -> Option<&mut T> { + if let Some(connections) = self.0.get_mut(&key.small()) { + for conn in connections { + if conn.remote_equals(key) { + conn.set_last_accessed_time(wdk::utils::get_system_timestamp_ms()); + return Some(conn); + } + } + } + + None + } + + pub fn read(&self, key: &Key, read_connection: fn(&T) -> Option) -> Option { + if let Some(connections) = self.0.get(&key.small()) { + for conn in connections { + if conn.remote_equals(key) { + conn.set_last_accessed_time(wdk::utils::get_system_timestamp_ms()); + return read_connection(conn); + } + if conn.redirect_equals(key) { + conn.set_last_accessed_time(wdk::utils::get_system_timestamp_ms()); + return read_connection(conn); + } + } + } + + None + } + + pub fn end(&mut self, key: Key) -> Option { + if let Some(connections) = self.0.get_mut(&key.small()) { + for (_, conn) in connections.iter_mut().enumerate() { + if conn.remote_equals(&key) { + conn.end(wdk::utils::get_system_timestamp_ms()); + return Some(conn.clone()); + } + } + } + return None; + } + + pub fn end_all_on_port(&mut self, key: (IpProtocol, u16)) -> Option> { + if let Some(connections) = self.0.get_mut(&key) { + let mut vec = Vec::with_capacity(connections.len()); + for (_, conn) in connections.iter_mut().enumerate() { + if !conn.has_ended() { + conn.end(wdk::utils::get_system_timestamp_ms()); + vec.push(conn.clone()); + } + } + return Some(vec); + } + return None; + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + pub fn clean_ended_connections(&mut self) { + let now = wdk::utils::get_system_timestamp_ms(); + const TEN_MINUETS: u64 = Duration::from_secs(60 * 10).as_millis() as u64; + let before_ten_minutes = now - TEN_MINUETS; + let before_one_minute = now - Duration::from_secs(60).as_millis() as u64; + + for (_, connections) in self.0.iter_mut() { + connections.retain(|c| { + if c.has_ended() && c.get_end_time() < before_one_minute { + // Ended more than 1 minute ago + return false; + } + + if c.get_last_accessed_time() < before_ten_minutes { + // Last active more than 10 minutes ago + return false; + } + + // Keep + return true; + }); + } + self.0.retain(|_, v| !v.is_empty()); + } + + #[allow(dead_code)] + pub fn get_count(&self) -> usize { + let mut count = 0; + for conn in self.0.values() { + count += conn.len(); + } + return count; + } + + pub fn iter(&self) -> hashbrown::hash_map::Iter<'_, (IpProtocol, u16), Vec> { + self.0.iter() + } +} diff --git a/windows_kext/driver/src/device.rs b/windows_kext/driver/src/device.rs new file mode 100644 index 00000000..e5eeca7d --- /dev/null +++ b/windows_kext/driver/src/device.rs @@ -0,0 +1,329 @@ +use alloc::string::String; +use num_traits::FromPrimitive; +use protocol::{command::CommandType, info::Info}; +use smoltcp::wire::{IpAddress, IpProtocol, Ipv4Address, Ipv6Address}; +use wdk::{ + driver::Driver, + filter_engine::{ + callout_data::ClassifyDefer, + net_buffer::{NetBufferList, NetworkAllocator}, + packet::{InjectInfo, Injector}, + FilterEngine, + }, + ioqueue::{self, IOQueue}, + irp_helpers::{ReadRequest, WriteRequest}, +}; + +use crate::{ + array_holder::ArrayHolder, bandwidth::Bandwidth, callouts, connection_cache::ConnectionCache, + connection_map::Key, dbg, err, id_cache::IdCache, logger, packet_util::Redirect, +}; + +pub enum Packet { + PacketLayer(NetBufferList, InjectInfo), + AleLayer(ClassifyDefer), +} + +// Device Context +pub struct Device { + pub(crate) filter_engine: FilterEngine, + pub(crate) read_leftover: ArrayHolder, + pub(crate) event_queue: IOQueue, + pub(crate) packet_cache: IdCache, + pub(crate) connection_cache: ConnectionCache, + pub(crate) injector: Injector, + pub(crate) network_allocator: NetworkAllocator, + pub(crate) bandwidth_stats: Bandwidth, +} + +impl Device { + /// Initialize all members of the device. Memory is handled by windows. + /// Make sure everything is initialized here. + pub fn new(driver: &Driver) -> Result { + let mut filter_engine = + match FilterEngine::new(driver, 0x7dab1057_8e2b_40c4_9b85_693e381d7896) { + Ok(fe) => fe, + Err(err) => return Err(alloc::format!("filter engine error: {}", err)), + }; + + if let Err(err) = filter_engine.commit(callouts::get_callout_vec()) { + return Err(err); + } + + Ok(Self { + filter_engine, + read_leftover: ArrayHolder::default(), + event_queue: IOQueue::new(), + packet_cache: IdCache::new(), + connection_cache: ConnectionCache::new(), + injector: Injector::new(), + network_allocator: NetworkAllocator::new(), + bandwidth_stats: Bandwidth::new(), + }) + } + + /// Cleanup is called just before drop. + // pub fn cleanup(&mut self) {} + + fn write_buffer(&mut self, read_request: &mut ReadRequest, info: Info) { + let bytes = info.as_bytes(); + let count = read_request.write(bytes); + + // Check if the full buffer was written. + if count < bytes.len() { + // Save the leftovers for later. + self.read_leftover.save(&bytes[count..]); + } + } + + /// Called when handle. Read is called from user-space. + pub fn read(&mut self, read_request: &mut ReadRequest) { + if let Some(data) = self.read_leftover.load() { + // There are leftovers from previous request. + let count = read_request.write(&data); + + // Check if full command was written. + if count < data.len() { + // Save the leftovers for later. + self.read_leftover.save(&data[count..]); + } + } else { + // Noting left from before. Wait for next commands. + match self.event_queue.wait_and_pop() { + Ok(info) => { + self.write_buffer(read_request, info); + } + Err(ioqueue::Status::Timeout) => { + // Timeout. This will only trigger if pop function is called with timeout. + read_request.timeout(); + return; + } + Err(err) => { + // Queue failed. Send EOF, to notify user-space. Usually happens on rundown. + err!("failed to pop value: {}", err); + read_request.end_of_file(); + return; + } + } + } + + // Check if we have more space. InfoType + data_size == 5 bytes + while read_request.free_space() > 5 { + match self.event_queue.pop() { + Ok(info) => { + self.write_buffer(read_request, info); + } + Err(_) => { + break; + } + } + } + read_request.complete(); + } + + // Called when handle.Write is called from user-space. + pub fn write(&mut self, write_request: &mut WriteRequest) { + // Try parsing the command. + let mut buffer = write_request.get_buffer(); + let command = protocol::command::parse_type(buffer); + let Some(command) = command else { + err!("Unknown command number: {}", buffer[0]); + return; + }; + buffer = &buffer[1..]; + + let mut _classify_defer = None; + + match command { + CommandType::Shutdown => { + wdk::dbg!("Shutdown command"); + self.shutdown(); + } + CommandType::Verdict => { + let verdict = protocol::command::parse_verdict(buffer); + wdk::dbg!("Verdict command"); + // Received verdict decision for a specific connection. + if let Some((key, mut packet)) = self.packet_cache.pop_id(verdict.id) { + if let Some(verdict) = FromPrimitive::from_u8(verdict.verdict) { + dbg!("Verdict received {}: {}", key, verdict); + // Add verdict in the cache. + let redirect_info = self.connection_cache.update_connection(key, verdict); + + // if verdict.is_permanent() { + // dbg!(self.logger, "resetting filters {}: {}", key, verdict); + // _ = self.filter_engine.reset_all_filters(); + // } + + match verdict { + crate::connection::Verdict::Accept + | crate::connection::Verdict::PermanentAccept => { + if let Err(err) = self.inject_packet(packet, false) { + err!("failed to inject packet: {}", err); + } else { + dbg!("packet injected: {}", key); + } + } + crate::connection::Verdict::RedirectNameServer + | crate::connection::Verdict::RedirectTunnel => { + if let Some(redirect_info) = redirect_info { + if let Err(err) = packet.redirect(redirect_info) { + err!("failed to redirect packet: {}", err); + } + if let Err(err) = self.inject_packet(packet, false) { + err!("failed to inject packet: {}", err); + } + } + } + _ => { + if let Err(err) = self.inject_packet(packet, true) { + err!("failed to inject packet: {}", err); + } + } + } + }; + } else { + // Id was not in the packet cache. + let id = verdict.id; + err!("Verdict invalid id: {}", id); + } + } + CommandType::UpdateV4 => { + let update = protocol::command::parse_update_v4(buffer); + // Build the new action. + if let Some(verdict) = FromPrimitive::from_u8(update.verdict) { + // Update with new action. + dbg!("Verdict update received {:?}: {}", update, verdict); + _classify_defer = self.connection_cache.update_connection( + Key { + protocol: IpProtocol::from(update.protocol), + local_address: IpAddress::Ipv4(Ipv4Address::from_bytes( + &update.local_address, + )), + local_port: update.local_port, + remote_address: IpAddress::Ipv4(Ipv4Address::from_bytes( + &update.remote_address, + )), + remote_port: update.remote_port, + }, + verdict, + ); + } else { + err!("invalid verdict value: {}", update.verdict); + } + } + CommandType::UpdateV6 => { + let update = protocol::command::parse_update_v6(buffer); + // Build the new action. + if let Some(verdict) = FromPrimitive::from_u8(update.verdict) { + // Update with new action. + dbg!("Verdict update received {:?}: {}", update, verdict); + _classify_defer = self.connection_cache.update_connection( + Key { + protocol: IpProtocol::from(update.protocol), + local_address: IpAddress::Ipv6(Ipv6Address::from_bytes( + &update.local_address, + )), + local_port: update.local_port, + remote_address: IpAddress::Ipv6(Ipv6Address::from_bytes( + &update.remote_address, + )), + remote_port: update.remote_port, + }, + verdict, + ); + } else { + err!("invalid verdict value: {}", update.verdict); + } + } + CommandType::ClearCache => { + wdk::dbg!("ClearCache command"); + self.connection_cache.clear(); + if let Err(err) = self.filter_engine.reset_all_filters() { + err!("failed to reset filters: {}", err); + } + } + CommandType::GetLogs => { + wdk::dbg!("GetLogs command"); + let lines_vec = logger::flush(); + for line in lines_vec { + let _ = self.event_queue.push(line); + } + } + CommandType::GetBandwidthStats => { + wdk::dbg!("GetBandwidthStats command"); + let stats = self.bandwidth_stats.get_all_updates_tcp_v4(); + if let Some(stats) = stats { + _ = self.event_queue.push(stats); + } + + let stats = self.bandwidth_stats.get_all_updates_tcp_v6(); + if let Some(stats) = stats { + _ = self.event_queue.push(stats); + } + + let stats = self.bandwidth_stats.get_all_updates_udp_v4(); + if let Some(stats) = stats { + _ = self.event_queue.push(stats); + } + + let stats = self.bandwidth_stats.get_all_updates_udp_v6(); + if let Some(stats) = stats { + _ = self.event_queue.push(stats); + } + } + CommandType::PrintMemoryStats => { + // Getting the information takes a long time and interferes with the callouts causing the device to crash. + // TODO(vladimir): Make more optimized version + // info!( + // "Packet cache: {} entries", + // self.packet_cache.get_entries_count() + // ); + // info!( + // "BandwidthStats cache: {} entries", + // self.bandwidth_stats.get_entries_count() + // ); + // info!( + // "Connection cache: {} entries\n {}", + // self.connection_cache.get_entries_count(), + // self.connection_cache.get_full_cache_info() + // ); + } + CommandType::CleanEndedConnections => { + wdk::dbg!("CleanEndedConnections command"); + self.connection_cache.clean_ended_connections(); + } + } + } + + pub fn shutdown(&self) { + // End blocking operations from the queue. This will end pending read requests. + self.event_queue.rundown(); + } + + pub fn inject_packet(&mut self, packet: Packet, blocked: bool) -> Result<(), String> { + match packet { + Packet::PacketLayer(nbl, inject_info) => { + if !blocked { + self.injector.inject_net_buffer_list(nbl, inject_info) + } else { + Ok(()) + } + } + Packet::AleLayer(defer) => { + let packet_list = defer.complete(&mut self.filter_engine)?; + if let Some(packet_list) = packet_list { + self.injector.inject_packet_list_transport(packet_list)?; + } + + Ok(()) + } + } + } +} + +impl Drop for Device { + fn drop(&mut self) { + _ = logger::flush(); + // dbg!("Device Context drop called."); + } +} diff --git a/windows_kext/driver/src/driver_hashmap.rs b/windows_kext/driver/src/driver_hashmap.rs new file mode 100644 index 00000000..1c8b706a --- /dev/null +++ b/windows_kext/driver/src/driver_hashmap.rs @@ -0,0 +1,25 @@ +use core::ops::{Deref, DerefMut}; + +use hashbrown::HashMap; + +pub struct DeviceHashMap(Option>); + +impl DeviceHashMap { + pub fn new() -> Self { + Self(Some(HashMap::new())) + } +} + +impl Deref for DeviceHashMap { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + self.0.as_ref().unwrap() + } +} + +impl DerefMut for DeviceHashMap { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut().unwrap() + } +} diff --git a/windows_kext/driver/src/entry.rs b/windows_kext/driver/src/entry.rs new file mode 100644 index 00000000..b52a2848 --- /dev/null +++ b/windows_kext/driver/src/entry.rs @@ -0,0 +1,135 @@ +use crate::common::ControlCode; +use crate::device; +use alloc::boxed::Box; +use num_traits::FromPrimitive; +use wdk::irp_helpers::{DeviceControlRequest, ReadRequest, WriteRequest}; +use wdk::{err, info, interface}; +use windows_sys::Wdk::Foundation::{DEVICE_OBJECT, DRIVER_OBJECT, IRP}; +use windows_sys::Win32::Foundation::{NTSTATUS, STATUS_SUCCESS}; + +static VERSION: [u8; 4] = include!("../../kext_interface/version.txt"); + +static mut DEVICE: *mut device::Device = core::ptr::null_mut(); +pub fn get_device() -> Option<&'static mut device::Device> { + return unsafe { DEVICE.as_mut() }; +} + +// DriverEntry is the entry point of the driver (main function). Will be called when driver is loaded. +// Name should not be changed +#[export_name = "DriverEntry"] +pub extern "system" fn driver_entry( + driver_object: *mut windows_sys::Wdk::Foundation::DRIVER_OBJECT, + registry_path: *mut windows_sys::Win32::Foundation::UNICODE_STRING, +) -> windows_sys::Win32::Foundation::NTSTATUS { + info!("Starting initialization..."); + + // Initialize driver object. + let mut driver = match interface::init_driver_object( + driver_object, + registry_path, + "PortmasterKext", + core::ptr::null_mut(), + ) { + Ok(driver) => driver, + Err(status) => { + err!("driver_entry: failed to initialize driver: {}", status); + return windows_sys::Win32::Foundation::STATUS_FAILED_DRIVER_ENTRY; + } + }; + + // Set driver functions. + driver.set_driver_unload(driver_unload); + driver.set_read_fn(driver_read); + driver.set_write_fn(driver_write); + driver.set_device_control_fn(device_control); + + // Initialize device. + unsafe { + let device = match device::Device::new(&driver) { + Ok(device) => Box::new(device), + Err(err) => { + wdk::err!("filed to initialize device: {}", err); + return -1; + } + }; + DEVICE = Box::into_raw(device); + } + + STATUS_SUCCESS +} + +// driver_unload function is called when service delete is called from user-space. +unsafe extern "system" fn driver_unload(_object: *const DRIVER_OBJECT) { + info!("Unloading complete"); + unsafe { + if !DEVICE.is_null() { + _ = Box::from_raw(DEVICE); + } + } +} + +// driver_read event triggered from user-space on file.Read. +unsafe extern "system" fn driver_read( + _device_object: &mut DEVICE_OBJECT, + irp: &mut IRP, +) -> NTSTATUS { + let mut read_request = ReadRequest::new(irp); + let Some(device) = get_device() else { + read_request.complete(); + + return read_request.get_status(); + }; + + device.read(&mut read_request); + read_request.get_status() +} + +/// driver_write event triggered from user-space on file.Write. +unsafe extern "system" fn driver_write( + _device_object: &mut DEVICE_OBJECT, + irp: &mut IRP, +) -> NTSTATUS { + let mut write_request = WriteRequest::new(irp); + let Some(device) = get_device() else { + write_request.complete(); + return write_request.get_status(); + }; + + device.write(&mut write_request); + + write_request.mark_all_as_read(); + write_request.complete(); + write_request.get_status() +} + +/// device_control event triggered from user-space on file.deviceIOControl. +unsafe extern "system" fn device_control( + _device_object: &mut DEVICE_OBJECT, + irp: &mut IRP, +) -> NTSTATUS { + let mut control_request = DeviceControlRequest::new(irp); + let Some(device) = get_device() else { + control_request.complete(); + return control_request.get_status(); + }; + + let Some(control_code): Option = + FromPrimitive::from_u32(control_request.get_control_code()) + else { + wdk::info!("Unknown IOCTL code: {}", control_request.get_control_code()); + control_request.not_implemented(); + return control_request.get_status(); + }; + + wdk::info!("IOCTL: {}", control_code); + + match control_code { + ControlCode::Version => { + control_request.write(&VERSION); + } + ControlCode::ShutdownRequest => device.shutdown(), + }; + + control_request.complete(); + control_request.get_status() +} diff --git a/windows_kext/driver/src/id_cache.rs b/windows_kext/driver/src/id_cache.rs new file mode 100644 index 00000000..e8474538 --- /dev/null +++ b/windows_kext/driver/src/id_cache.rs @@ -0,0 +1,131 @@ +use alloc::collections::VecDeque; +use protocol::info::Info; +use smoltcp::wire::{IpAddress, IpProtocol}; +use wdk::rw_spin_lock::RwSpinLock; + +use crate::{connection::Direction, connection_map::Key, device::Packet}; + +struct Entry { + value: T, + id: u64, +} + +pub struct IdCache { + values: VecDeque>, + lock: RwSpinLock, + next_id: u64, +} + +impl IdCache { + pub fn new() -> Self { + Self { + values: VecDeque::with_capacity(1000), + lock: RwSpinLock::default(), + next_id: 1, // 0 is invalid id + } + } + + pub fn push( + &mut self, + value: (Key, Packet), + process_id: u64, + direction: Direction, + ale_layer: bool, + ) -> Option { + let _guard = self.lock.write_lock(); + let id = self.next_id; + let info = build_info(&value.0, id, process_id, direction, &value.1, ale_layer); + self.values.push_back(Entry { value, id }); + self.next_id = self.next_id.wrapping_add(1); // Assuming this will not overflow. + + return info; + } + + pub fn pop_id(&mut self, id: u64) -> Option<(Key, Packet)> { + let _guard = self.lock.write_lock(); + if let Ok(index) = self.values.binary_search_by_key(&id, |val| val.id) { + return Some(self.values.remove(index).unwrap().value); + } + None + } + + #[allow(dead_code)] + pub fn get_entries_count(&self) -> usize { + let _guard = self.lock.read_lock(); + return self.values.len(); + } +} + +fn get_payload<'a>(packet: &'a Packet) -> Option<&'a [u8]> { + match packet { + Packet::PacketLayer(nbl, _) => nbl.get_data(), + Packet::AleLayer(defer) => { + let p = match defer { + wdk::filter_engine::callout_data::ClassifyDefer::Initial(_, p) => p, + wdk::filter_engine::callout_data::ClassifyDefer::Reauthorization(_, p) => p, + }; + if let Some(tpl) = p { + tpl.net_buffer_list_queue.get_data() + } else { + None + } + } + } +} + +fn build_info( + key: &Key, + packet_id: u64, + process_id: u64, + direction: Direction, + packet: &Packet, + ale_layer: bool, +) -> Option { + let (local_port, remote_port) = match key.protocol { + IpProtocol::Tcp | IpProtocol::Udp => (key.local_port, key.remote_port), + _ => (0, 0), + }; + + let payload_layer = if ale_layer { + 4 // Transport layer + } else { + 3 // Network layer + }; + + let mut payload = &[][..]; + if let Some(p) = get_payload(packet) { + payload = p; + } + + match (key.local_address, key.remote_address) { + (IpAddress::Ipv6(local_ip), IpAddress::Ipv6(remote_ip)) if key.is_ipv6() => { + Some(protocol::info::connection_info_v6( + packet_id, + process_id, + direction as u8, + u8::from(key.protocol), + local_ip.0, + remote_ip.0, + local_port, + remote_port, + payload_layer, + payload, + )) + } + (IpAddress::Ipv4(local_ip), IpAddress::Ipv4(remote_ip)) => { + Some(protocol::info::connection_info_v4( + packet_id, + process_id, + direction as u8, + u8::from(key.protocol), + local_ip.0, + remote_ip.0, + local_port, + remote_port, + payload_layer, + payload, + )) + } + _ => None, + } +} diff --git a/windows_kext/driver/src/lib.rs b/windows_kext/driver/src/lib.rs new file mode 100644 index 00000000..d13e9d3f --- /dev/null +++ b/windows_kext/driver/src/lib.rs @@ -0,0 +1,43 @@ +#![cfg_attr(not(test), no_std)] +#![no_main] +#![allow(clippy::needless_return)] + +extern crate alloc; + +mod ale_callouts; +mod array_holder; +mod bandwidth; +mod callouts; +mod common; +mod connection; +mod connection_cache; +mod connection_map; +mod device; +mod driver_hashmap; +mod entry; +mod id_cache; +pub mod logger; +mod packet_callouts; +mod packet_util; +mod stream_callouts; + +use wdk::allocator::WindowsAllocator; + +#[cfg(not(test))] +use core::panic::PanicInfo; + +// Declaration of the global memory allocator +#[global_allocator] +static HEAP: WindowsAllocator = WindowsAllocator {}; + +#[no_mangle] +pub extern "system" fn _DllMainCRTStartup() {} + +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + use wdk::err; + + err!("{}", info); + loop {} +} diff --git a/windows_kext/driver/src/logger.rs b/windows_kext/driver/src/logger.rs new file mode 100644 index 00000000..5a0440a2 --- /dev/null +++ b/windows_kext/driver/src/logger.rs @@ -0,0 +1,114 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::{ + mem::MaybeUninit, + sync::atomic::{AtomicPtr, AtomicUsize, Ordering}, +}; +use protocol::info::{Info, Severity}; + +#[cfg(not(debug_assertions))] +pub const LOG_LEVEL: u8 = Severity::Warning as u8; + +#[cfg(debug_assertions)] +pub const LOG_LEVEL: u8 = Severity::Trace as u8; + +pub const MAX_LOG_LINE_SIZE: usize = 150; + +static mut LOG_LINES: [AtomicPtr; 1024] = unsafe { MaybeUninit::zeroed().assume_init() }; +static START_INDEX: AtomicUsize = unsafe { MaybeUninit::zeroed().assume_init() }; +static END_INDEX: AtomicUsize = unsafe { MaybeUninit::zeroed().assume_init() }; + +pub fn add_line(log_line: Info) { + let mut index = END_INDEX.fetch_add(1, Ordering::Acquire); + unsafe { + index %= LOG_LINES.len(); + let ptr = &mut LOG_LINES[index]; + let line = Box::new(log_line); + let old = ptr.swap(Box::into_raw(line), Ordering::SeqCst); + if !old.is_null() { + _ = Box::from_raw(old); + } + } +} + +pub fn flush() -> Vec { + let mut vec = Vec::new(); + let end_index = END_INDEX.load(Ordering::Acquire); + let start_index = START_INDEX.load(Ordering::Acquire); + if end_index <= start_index { + return vec; + } + unsafe { + let count = end_index - start_index; + for i in start_index..start_index + count { + let index = i % LOG_LINES.len(); + let ptr = LOG_LINES[index].swap(core::ptr::null_mut(), Ordering::SeqCst); + if !ptr.is_null() { + vec.push(*Box::from_raw(ptr)); + } + } + } + + START_INDEX.store(end_index, Ordering::Release); + vec +} + +#[macro_export] +macro_rules! log_internal { + ($log_line:expr, $($arg:tt)*) => ({ + use core::fmt::Write; + _ = write!($log_line, "{}:{} ", file!(), line!()); + _ = write!($log_line, $($arg)*); + $crate::logger::add_line($log_line); + }); +} + +#[macro_export] +macro_rules! crit { + ($($arg:tt)*) => ({ + if protocol::info::Severity::Critical as u8 >= $crate::logger::LOG_LEVEL { + let message = alloc::format!($($arg)*); + $crate::logger::add_line(protocol::info::Severity::Critical, alloc::format!("{}:{} ", file!(), line!()), message) + } + }); +} + +#[macro_export] +macro_rules! err { + ($($arg:tt)*) => ({ + if protocol::info::Severity::Error as u8 >= $crate::logger::LOG_LEVEL { + let mut log_line = protocol::info::log_line(protocol::info::Severity::Error, $crate::logger::MAX_LOG_LINE_SIZE); + $crate::log_internal!(log_line, $($arg)*); + } + }); +} + +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => ({ + if protocol::info::Severity::Warning as u8 >= $crate::logger::LOG_LEVEL { + let mut log_line = protocol::info::log_line(protocol::info::Severity::Warning, $crate::logger::MAX_LOG_LINE_SIZE); + $crate::log_internal!(log_line, $($arg)*); + } + }); +} + +#[macro_export] +macro_rules! dbg { + ($($arg:tt)*) => ({ + if protocol::info::Severity::Debug as u8 >= $crate::logger::LOG_LEVEL { + let mut log_line = protocol::info::log_line(protocol::info::Severity::Debug, $crate::logger::MAX_LOG_LINE_SIZE); + $crate::log_internal!(log_line, $($arg)*); + } + }); +} + +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => ({ + if protocol::info::Severity::Info as u8 >= $crate::logger::LOG_LEVEL { + let mut log_line = protocol::info::log_line(protocol::info::Severity::Info, $crate::logger::MAX_LOG_LINE_SIZE); + $crate::log_internal!(log_line, $($arg)*); + } + }); +} diff --git a/windows_kext/driver/src/packet_callouts.rs b/windows_kext/driver/src/packet_callouts.rs new file mode 100644 index 00000000..9971d6af --- /dev/null +++ b/windows_kext/driver/src/packet_callouts.rs @@ -0,0 +1,298 @@ +use alloc::string::String; +use smoltcp::wire::{IPV4_HEADER_LEN, IPV6_HEADER_LEN}; +use wdk::filter_engine::callout_data::CalloutData; +use wdk::filter_engine::layer; +use wdk::filter_engine::net_buffer::{NetBufferList, NetBufferListIter}; +use wdk::filter_engine::packet::InjectInfo; + +use crate::connection::{ + Connection, ConnectionV4, ConnectionV6, Direction, RedirectInfo, Verdict, PM_DNS_PORT, + PM_SPN_PORT, +}; +use crate::connection_cache::ConnectionCache; +use crate::connection_map::Key; +use crate::device::{Device, Packet}; +use crate::packet_util::{get_key_from_nbl_v4, get_key_from_nbl_v6, Redirect}; +use crate::{err, warn}; + +// IP packet layers +pub fn ip_packet_layer_outbound_v4(data: CalloutData) { + type Fields = layer::FieldsOutboundIppacketV4; + let interface_index = data.get_value_u32(Fields::InterfaceIndex as usize); + let sub_interface_index = data.get_value_u32(Fields::SubInterfaceIndex as usize); + + ip_packet_layer( + data, + false, + Direction::Outbound, + interface_index, + sub_interface_index, + ); +} + +pub fn ip_packet_layer_inbound_v4(data: CalloutData) { + type Fields = layer::FieldsInboundIppacketV4; + let interface_index = data.get_value_u32(Fields::InterfaceIndex as usize); + let sub_interface_index = data.get_value_u32(Fields::SubInterfaceIndex as usize); + ip_packet_layer( + data, + false, + Direction::Inbound, + interface_index, + sub_interface_index, + ); +} + +pub fn ip_packet_layer_outbound_v6(data: CalloutData) { + type Fields = layer::FieldsOutboundIppacketV6; + let interface_index = data.get_value_u32(Fields::InterfaceIndex as usize); + let sub_interface_index = data.get_value_u32(Fields::SubInterfaceIndex as usize); + + ip_packet_layer( + data, + true, + Direction::Outbound, + interface_index, + sub_interface_index, + ); +} + +pub fn ip_packet_layer_inbound_v6(data: CalloutData) { + type Fields = layer::FieldsInboundIppacketV6; + let interface_index = data.get_value_u32(Fields::InterfaceIndex as usize); + let sub_interface_index = data.get_value_u32(Fields::SubInterfaceIndex as usize); + + ip_packet_layer( + data, + true, + Direction::Inbound, + interface_index, + sub_interface_index, + ); +} + +struct ConnectionInfo { + verdict: Verdict, + process_id: u64, + redirect_info: Option, +} + +impl ConnectionInfo { + fn from_connection(conn: &T) -> Self { + ConnectionInfo { + verdict: conn.get_verdict(), + process_id: conn.get_process_id(), + redirect_info: conn.redirect_info(), + } + } +} + +fn fast_track_pm_packets(key: &Key, direction: Direction) -> bool { + match direction { + Direction::Outbound => { + if key.local_port == PM_DNS_PORT || key.local_port == PM_SPN_PORT { + return key.local_address == key.remote_address; + } + } + Direction::Inbound => { + if key.local_port == PM_DNS_PORT || key.local_port == PM_SPN_PORT { + return key.local_address == key.remote_address; + } + } + } + + return false; +} + +fn ip_packet_layer( + mut data: CalloutData, + ipv6: bool, + direction: Direction, + interface_index: u32, + sub_interface_index: u32, +) { + let Some(device) = crate::entry::get_device() else { + return; + }; + if device + .injector + .was_network_packet_injected_by_self(data.get_layer_data() as _, ipv6) + { + data.action_permit(); + return; + } + + for mut nbl in NetBufferListIter::new(data.get_layer_data() as _) { + if let Direction::Inbound = direction { + // The header is not part of the NBL for incoming packets. Move the beginning of the buffer back so we get access to it. + // The NBL will auto advance after it loses scope. + if ipv6 { + nbl.retreat(IPV6_HEADER_LEN as u32, true); + } else { + nbl.retreat(IPV4_HEADER_LEN as u32, true); + } + } + + // Get key from packet. + let key = match if ipv6 { + get_key_from_nbl_v6(&nbl, direction) + } else { + get_key_from_nbl_v4(&nbl, direction) + } { + Ok(key) => key, + Err(err) => { + warn!("failed to get key from nbl: {}", err); + return; + } + }; + + if fast_track_pm_packets(&key, direction) { + data.action_permit(); + return; + } + + let mut is_tmp_verdict = false; + let mut process_id = 0; + + if matches!( + key.protocol, + smoltcp::wire::IpProtocol::Tcp | smoltcp::wire::IpProtocol::Udp + ) { + if let Some(mut conn_info) = + get_connection_info(&mut device.connection_cache, &key, ipv6) + { + process_id = conn_info.process_id; + // Check if there is action for this connection. + match conn_info.verdict { + Verdict::Undecided | Verdict::Accept | Verdict::Block | Verdict::Drop => { + is_tmp_verdict = true + } + Verdict::PermanentAccept => data.action_permit(), + Verdict::PermanentBlock => data.action_block(), + Verdict::Undeterminable | Verdict::PermanentDrop | Verdict::Failed => { + data.block_and_absorb() + } + Verdict::RedirectNameServer | Verdict::RedirectTunnel => { + if let Some(redirect_info) = conn_info.redirect_info.take() { + match clone_packet( + device, + nbl, + direction, + ipv6, + key.is_loopback(), + interface_index, + sub_interface_index, + ) { + Ok(mut packet) => { + let _ = packet.redirect(redirect_info); + if let Err(err) = device.inject_packet(packet, false) { + err!("failed to inject packet: {}", err); + } + } + Err(err) => err!("failed to clone packet: {}", err), + } + } + + // This will block the original packet. Even if injection failed. + data.block_and_absorb(); + continue; + } + } + } else { + // TCP and UDP always need to go through ALE layer first. + if matches!(direction, Direction::Inbound) { + // If it's an inbound packet and the connection is not found, we need to continue to ALE layer + data.action_permit(); + return; + } else { + // This happens sometimes. Leave the decision for portmaster. TODO(vladimir): Find out why. + err!("Invalid state for: {}", key); + is_tmp_verdict = true; + } + } + } else { + // Every other protocol treat as a tmp verdict. + is_tmp_verdict = true; + } + + // Clone packet and send to Portmaster if it's a temporary verdict. + if is_tmp_verdict { + let packet = match clone_packet( + device, + nbl, + direction, + ipv6, + key.is_loopback(), + interface_index, + sub_interface_index, + ) { + Ok(p) => p, + Err(err) => { + err!("failed to clone packet: {}", err); + return; + } + }; + + let info = device + .packet_cache + .push((key, packet), process_id, direction, false); + // Send to Portmaster + if let Some(info) = info { + let _ = device.event_queue.push(info); + } + data.block_and_absorb(); + } + } +} + +fn clone_packet( + device: &mut Device, + nbl: NetBufferList, + direction: Direction, + ipv6: bool, + loopback: bool, + interface_index: u32, + sub_interface_index: u32, +) -> Result { + let clone = nbl.clone(&device.network_allocator)?; + let inbound = match direction { + Direction::Outbound => false, + Direction::Inbound => true, + }; + Ok(Packet::PacketLayer( + clone, + InjectInfo { + ipv6, + inbound, + loopback, + interface_index, + sub_interface_index, + }, + )) +} + +fn get_connection_info( + connection_cache: &mut ConnectionCache, + key: &Key, + ipv6: bool, +) -> Option { + if ipv6 { + let conn_info = connection_cache.read_connection_v6( + &key, + |conn: &ConnectionV6| -> Option { + // Function is is behind spin lock. Just copy and return. + Some(ConnectionInfo::from_connection(conn)) + }, + ); + return conn_info; + } else { + let conn_info = connection_cache.read_connection_v4( + &key, + |conn: &ConnectionV4| -> Option { + // Function is is behind spin lock. Just copy and return. + Some(ConnectionInfo::from_connection(conn)) + }, + ); + return conn_info; + } +} diff --git a/windows_kext/driver/src/packet_util.rs b/windows_kext/driver/src/packet_util.rs new file mode 100644 index 00000000..42b4dac7 --- /dev/null +++ b/windows_kext/driver/src/packet_util.rs @@ -0,0 +1,399 @@ +use alloc::string::{String, ToString}; +use smoltcp::wire::{ + IpAddress, IpProtocol, Ipv4Address, Ipv4Packet, Ipv6Address, Ipv6Packet, TcpPacket, UdpPacket, +}; +use wdk::filter_engine::net_buffer::NetBufferList; + +use crate::connection_map::Key; +use crate::device::Packet; +use crate::{ + connection::{Direction, RedirectInfo}, + dbg, err, +}; + +/// `Redirect` is a trait that defines a method for redirecting network packets. +/// +/// This trait is used to implement different strategies for redirecting packets, +/// depending on the specific requirements of the application. +pub trait Redirect { + /// Redirects a network packet based on the provided `RedirectInfo`. + /// + /// # Arguments + /// + /// * `redirect_info` - A struct containing information about how to redirect the packet. + /// + /// # Returns + /// + /// * `Ok(())` if the packet was successfully redirected. + /// * `Err(String)` if there was an error redirecting the packet. + fn redirect(&mut self, redirect_info: RedirectInfo) -> Result<(), String>; +} + +impl Redirect for Packet { + fn redirect(&mut self, redirect_info: RedirectInfo) -> Result<(), String> { + if let Packet::PacketLayer(nbl, inject_info) = self { + let Some(data) = nbl.get_data_mut() else { + return Err("trying to redirect immutable NBL".to_string()); + }; + + if inject_info.inbound { + redirect_inbound_packet( + data, + redirect_info.local_address, + redirect_info.remote_address, + redirect_info.remote_port, + ) + } else { + redirect_outbound_packet( + data, + redirect_info.redirect_address, + redirect_info.redirect_port, + redirect_info.unify, + ) + } + return Ok(()); + } + // return Err("can't redirect from non packet layer".to_string()); + return Ok(()); + } +} + +/// Redirects an outbound packet to a specified remote address and port. +/// +/// # Arguments +/// +/// * `packet` - A mutable reference to the packet data. +/// * `remote_address` - The IP address to redirect the packet to. +/// * `remote_port` - The port to redirect the packet to. +/// * `unify` - If true, the source and destination addresses of the packet will be set to the same value. +/// +/// This function modifies the packet in-place to change its destination address and port. +/// It also updates the checksums for the IP and transport layer headers. +/// If the `unify` parameter is true, it sets the source and destination addresses to be the same. +/// If the remote address is a loopback address, it sets the source address to the loopback address. +fn redirect_outbound_packet( + packet: &mut [u8], + remote_address: IpAddress, + remote_port: u16, + unify: bool, +) { + match remote_address { + IpAddress::Ipv4(remote_address) => { + if let Ok(mut ip_packet) = Ipv4Packet::new_checked(packet) { + if unify { + ip_packet.set_dst_addr(ip_packet.src_addr()); + } else { + ip_packet.set_dst_addr(remote_address); + if remote_address.is_loopback() { + ip_packet.set_src_addr(Ipv4Address::new(127, 0, 0, 1)); + } + } + ip_packet.fill_checksum(); + let src_addr = ip_packet.src_addr(); + let dst_addr = ip_packet.dst_addr(); + if ip_packet.next_header() == IpProtocol::Udp { + if let Ok(mut udp_packet) = UdpPacket::new_checked(ip_packet.payload_mut()) { + udp_packet.set_dst_port(remote_port); + udp_packet + .fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr)); + } + } + if ip_packet.next_header() == IpProtocol::Tcp { + if let Ok(mut tcp_packet) = TcpPacket::new_checked(ip_packet.payload_mut()) { + tcp_packet.set_dst_port(remote_port); + tcp_packet + .fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr)); + } + } + } + } + IpAddress::Ipv6(remote_address) => { + if let Ok(mut ip_packet) = Ipv6Packet::new_checked(packet) { + ip_packet.set_dst_addr(remote_address); + if unify { + ip_packet.set_dst_addr(ip_packet.src_addr()); + } else { + ip_packet.set_dst_addr(remote_address); + if remote_address.is_loopback() { + ip_packet.set_src_addr(Ipv6Address::LOOPBACK); + } + } + let src_addr = ip_packet.src_addr(); + let dst_addr = ip_packet.dst_addr(); + if ip_packet.next_header() == IpProtocol::Udp { + if let Ok(mut udp_packet) = UdpPacket::new_checked(ip_packet.payload_mut()) { + udp_packet.set_dst_port(remote_port); + udp_packet + .fill_checksum(&IpAddress::Ipv6(src_addr), &IpAddress::Ipv6(dst_addr)); + } + } + if ip_packet.next_header() == IpProtocol::Tcp { + if let Ok(mut tcp_packet) = TcpPacket::new_checked(ip_packet.payload_mut()) { + tcp_packet.set_dst_port(remote_port); + tcp_packet + .fill_checksum(&IpAddress::Ipv6(src_addr), &IpAddress::Ipv6(dst_addr)); + } + } + } + } + } +} + +/// Redirects an inbound packet to a local address. +/// +/// This function takes a mutable reference to a packet and modifies it in place. +/// It changes the destination address to the provided local address and the source address +/// to the original remote address. It also sets the source port to the original remote port. +/// The function handles both IPv4 and IPv6 addresses. +/// +/// # Arguments +/// +/// * `packet` - A mutable reference to the packet data. +/// * `local_address` - The local IP address to redirect the packet to. +/// * `original_remote_address` - The original remote IP address of the packet. +/// * `original_remote_port` - The original remote port of the packet. +/// +fn redirect_inbound_packet( + packet: &mut [u8], + local_address: IpAddress, + original_remote_address: IpAddress, + original_remote_port: u16, +) { + match local_address { + IpAddress::Ipv4(local_address) => { + let IpAddress::Ipv4(original_remote_address) = original_remote_address else { + return; + }; + + if let Ok(mut ip_packet) = Ipv4Packet::new_checked(packet) { + ip_packet.set_dst_addr(local_address); + ip_packet.set_src_addr(original_remote_address); + ip_packet.fill_checksum(); + let src_addr = ip_packet.src_addr(); + let dst_addr = ip_packet.dst_addr(); + if ip_packet.next_header() == IpProtocol::Udp { + if let Ok(mut udp_packet) = UdpPacket::new_checked(ip_packet.payload_mut()) { + udp_packet.set_src_port(original_remote_port); + udp_packet + .fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr)); + } + } + if ip_packet.next_header() == IpProtocol::Tcp { + if let Ok(mut tcp_packet) = TcpPacket::new_checked(ip_packet.payload_mut()) { + tcp_packet.set_src_port(original_remote_port); + tcp_packet + .fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr)); + } + } + } + } + IpAddress::Ipv6(local_address) => { + if let Ok(mut ip_packet) = Ipv6Packet::new_checked(packet) { + let IpAddress::Ipv6(original_remote_address) = original_remote_address else { + return; + }; + ip_packet.set_dst_addr(local_address); + ip_packet.set_src_addr(original_remote_address); + let src_addr = ip_packet.src_addr(); + let dst_addr = ip_packet.dst_addr(); + if ip_packet.next_header() == IpProtocol::Udp { + if let Ok(mut udp_packet) = UdpPacket::new_checked(ip_packet.payload_mut()) { + udp_packet.set_src_port(original_remote_port); + udp_packet + .fill_checksum(&IpAddress::Ipv6(src_addr), &IpAddress::Ipv6(dst_addr)); + } + } + if ip_packet.next_header() == IpProtocol::Tcp { + if let Ok(mut tcp_packet) = TcpPacket::new_checked(ip_packet.payload_mut()) { + tcp_packet.set_src_port(original_remote_port); + tcp_packet + .fill_checksum(&IpAddress::Ipv6(src_addr), &IpAddress::Ipv6(dst_addr)); + } + } + } + } + } +} + +#[allow(dead_code)] +fn print_packet(packet: &[u8]) { + if let Ok(ip_packet) = Ipv4Packet::new_checked(packet) { + if ip_packet.next_header() == IpProtocol::Udp { + if let Ok(udp_packet) = UdpPacket::new_checked(ip_packet.payload()) { + dbg!("packet {} {}", ip_packet, udp_packet); + } + } + if ip_packet.next_header() == IpProtocol::Tcp { + if let Ok(tcp_packet) = TcpPacket::new_checked(ip_packet.payload()) { + dbg!("packet {} {}", ip_packet, tcp_packet); + } + } + } else { + err!("failed to print packet: invalid ip header: {:?}", packet); + } +} + +/// This function extracts a key from a given IPv4 network buffer list (NBL). +/// The key contains the protocol, local and remote addresses and ports. +/// +/// # Arguments +/// +/// * `nbl` - A reference to the network buffer list from which the key will be extracted. +/// * `direction` - The direction of the packet (Inbound or Outbound). +/// +/// # Returns +/// +/// * `Ok(Key)` - A key containing the protocol, local and remote addresses and ports. +/// * `Err(String)` - An error message if the function fails to get net_buffer data. +const HEADERS_LEN: usize = smoltcp::wire::IPV4_HEADER_LEN + smoltcp::wire::TCP_HEADER_LEN; + +fn get_ports(packet: &[u8], protocol: smoltcp::wire::IpProtocol) -> (u16, u16) { + match protocol { + smoltcp::wire::IpProtocol::Tcp => { + let tcp_packet = TcpPacket::new_unchecked(packet); + (tcp_packet.src_port(), tcp_packet.dst_port()) + } + smoltcp::wire::IpProtocol::Udp => { + let udp_packet = UdpPacket::new_unchecked(packet); + (udp_packet.src_port(), udp_packet.dst_port()) + } + _ => (0, 0), // No ports for other protocols + } +} + +pub fn get_key_from_nbl_v4(nbl: &NetBufferList, direction: Direction) -> Result { + // Get bytes + let mut headers = [0; HEADERS_LEN]; + if nbl.read_bytes(&mut headers).is_err() { + return Err("failed to get net_buffer data".to_string()); + } + + // Parse packet + let ip_packet = Ipv4Packet::new_unchecked(&headers); + let (src_port, dst_port) = get_ports( + &headers[smoltcp::wire::IPV4_HEADER_LEN..], + ip_packet.next_header(), + ); + + // Build key + match direction { + Direction::Outbound => Ok(Key { + protocol: ip_packet.next_header(), + local_address: IpAddress::Ipv4(ip_packet.src_addr()), + local_port: src_port, + remote_address: IpAddress::Ipv4(ip_packet.dst_addr()), + remote_port: dst_port, + }), + Direction::Inbound => Ok(Key { + protocol: ip_packet.next_header(), + local_address: IpAddress::Ipv4(ip_packet.dst_addr()), + local_port: dst_port, + remote_address: IpAddress::Ipv4(ip_packet.src_addr()), + remote_port: src_port, + }), + } +} + +/// This function extracts a key from a given IPv6 network buffer list (NBL). +/// The key contains the protocol, local and remote addresses and ports. +/// +/// # Arguments +/// +/// * `nbl` - A reference to the network buffer list from which the key will be extracted. +/// * `direction` - The direction of the packet (Inbound or Outbound). +/// +/// # Returns +/// +/// * `Ok(Key)` - A key containing the protocol, local and remote addresses and ports. +/// * `Err(String)` - An error message if the function fails to get net_buffer data. +pub fn get_key_from_nbl_v6(nbl: &NetBufferList, direction: Direction) -> Result { + // Get bytes + let mut headers = [0; smoltcp::wire::IPV6_HEADER_LEN + smoltcp::wire::TCP_HEADER_LEN]; + let Ok(()) = nbl.read_bytes(&mut headers) else { + return Err("failed to get net_buffer data".to_string()); + }; + // Parse packet + let ip_packet = Ipv6Packet::new_unchecked(&headers); + let (src_port, dst_port) = get_ports( + &headers[smoltcp::wire::IPV6_HEADER_LEN..], + ip_packet.next_header(), + ); + + // Build key + match direction { + Direction::Outbound => Ok(Key { + protocol: ip_packet.next_header(), + local_address: IpAddress::Ipv6(ip_packet.src_addr()), + local_port: src_port, + remote_address: IpAddress::Ipv6(ip_packet.dst_addr()), + remote_port: dst_port, + }), + Direction::Inbound => Ok(Key { + protocol: ip_packet.next_header(), + local_address: IpAddress::Ipv6(ip_packet.dst_addr()), + local_port: dst_port, + remote_address: IpAddress::Ipv6(ip_packet.src_addr()), + remote_port: src_port, + }), + } +} + +// Converts a given key into connection information. +// +// This function takes a key, packet id, process id, and direction as input. +// It then uses these to create a new `ConnectionInfoV6` or `ConnectionInfoV4` object, +// depending on whether the IP addresses in the key are IPv6 or IPv4 respectively. +// +// # Arguments +// +// * `key` - A reference to the key object containing the connection details. +// * `packet_id` - The id of the packet. +// * `process_id` - The id of the process. +// * `direction` - The direction of the connection. +// +// # Returns +// +// * `Some(Box)` - A boxed `Info` trait object if the key contains valid IPv4 or IPv6 addresses. +// * `None` - If the key does not contain valid IPv4 or IPv6 addresses. +// pub fn key_to_connection_info( +// key: &Key, +// packet_id: u64, +// process_id: u64, +// direction: Direction, +// payload: &[u8], +// ) -> Option { +// let (local_port, remote_port) = match key.protocol { +// IpProtocol::Tcp | IpProtocol::Udp => (key.local_port, key.remote_port), +// _ => (0, 0), +// }; + +// match (key.local_address, key.remote_address) { +// (IpAddress::Ipv6(local_ip), IpAddress::Ipv6(remote_ip)) if key.is_ipv6() => { +// Some(protocol::info::connection_info_v6( +// packet_id, +// process_id, +// direction as u8, +// u8::from(key.protocol), +// local_ip.0, +// remote_ip.0, +// local_port, +// remote_port, +// payload, +// )) +// } +// (IpAddress::Ipv4(local_ip), IpAddress::Ipv4(remote_ip)) => { +// Some(protocol::info::connection_info_v4( +// packet_id, +// process_id, +// direction as u8, +// u8::from(key.protocol), +// local_ip.0, +// remote_ip.0, +// local_port, +// remote_port, +// payload, +// )) +// } +// _ => None, +// } +// } diff --git a/windows_kext/driver/src/stream_callouts.rs b/windows_kext/driver/src/stream_callouts.rs new file mode 100644 index 00000000..a6393764 --- /dev/null +++ b/windows_kext/driver/src/stream_callouts.rs @@ -0,0 +1,203 @@ +use smoltcp::wire::{Ipv4Address, Ipv6Address}; +use wdk::filter_engine::{callout_data::CalloutData, layer, net_buffer::NetBufferListIter}; + +use crate::{bandwidth, connection::Direction}; + +pub fn stream_layer_tcp_v4(data: CalloutData) { + let Some(device) = crate::entry::get_device() else { + return; + }; + let mut direction = Direction::Outbound; + let data_length = if let Some(packet) = data.get_stream_callout_packet() { + if packet.is_receive() { + direction = Direction::Inbound; + } + packet.get_data_len() + } else { + return; + }; + type Fields = layer::FieldsStreamV4; + let local_ip = Ipv4Address::from_bytes( + &data + .get_value_u32(Fields::IpLocalAddress as usize) + .to_be_bytes(), + ); + let local_port = data.get_value_u16(Fields::IpLocalPort as usize); + let remote_ip = Ipv4Address::from_bytes( + &data + .get_value_u32(Fields::IpRemoteAddress as usize) + .to_be_bytes(), + ); + let remote_port = data.get_value_u16(Fields::IpRemotePort as usize); + match direction { + Direction::Outbound => { + device.bandwidth_stats.update_tcp_v4_tx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + Direction::Inbound => { + device.bandwidth_stats.update_tcp_v4_rx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + } +} + +pub fn stream_layer_tcp_v6(data: CalloutData) { + let Some(device) = crate::entry::get_device() else { + return; + }; + let mut direction = Direction::Outbound; + let data_length = if let Some(packet) = data.get_stream_callout_packet() { + if packet.is_receive() { + direction = Direction::Inbound; + } + packet.get_data_len() + } else { + return; + }; + type Fields = layer::FieldsStreamV6; + if data_length == 0 { + return; + } + let local_ip = + Ipv6Address::from_bytes(data.get_value_byte_array16(Fields::IpLocalAddress as usize)); + let local_port = data.get_value_u16(Fields::IpLocalPort as usize); + let remote_ip = + Ipv6Address::from_bytes(data.get_value_byte_array16(Fields::IpRemoteAddress as usize)); + let remote_port = data.get_value_u16(Fields::IpRemotePort as usize); + match direction { + Direction::Outbound => { + device.bandwidth_stats.update_tcp_v6_tx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + Direction::Inbound => { + device.bandwidth_stats.update_tcp_v6_rx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + } +} + +pub fn stream_layer_udp_v4(data: CalloutData) { + let Some(device) = crate::entry::get_device() else { + return; + }; + let mut data_length: usize = 0; + for nbl in NetBufferListIter::new(data.get_layer_data() as _) { + data_length += nbl.get_data_length() as usize; + } + type Fields = layer::FieldsDatagramDataV4; + let mut direction = Direction::Inbound; + if data.get_value_u8(Fields::Direction as usize) == 0 { + direction = Direction::Outbound; + } + + let local_ip = Ipv4Address::from_bytes( + &data + .get_value_u32(Fields::IpLocalAddress as usize) + .to_be_bytes(), + ); + let local_port = data.get_value_u16(Fields::IpLocalPort as usize); + let remote_ip = Ipv4Address::from_bytes( + &data + .get_value_u32(Fields::IpRemoteAddress as usize) + .to_be_bytes(), + ); + let remote_port = data.get_value_u16(Fields::IpRemotePort as usize); + match direction { + Direction::Outbound => { + device.bandwidth_stats.update_udp_v4_tx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + Direction::Inbound => { + device.bandwidth_stats.update_udp_v4_rx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + } +} + +pub fn stream_layer_udp_v6(data: CalloutData) { + let Some(device) = crate::entry::get_device() else { + return; + }; + let mut data_length: usize = 0; + for nbl in NetBufferListIter::new(data.get_layer_data() as _) { + data_length += nbl.get_data_length() as usize; + } + type Fields = layer::FieldsDatagramDataV6; + let mut direction = Direction::Inbound; + if data.get_value_u8(Fields::Direction as usize) == 0 { + direction = Direction::Outbound; + } + + let local_ip = + Ipv6Address::from_bytes(data.get_value_byte_array16(Fields::IpLocalAddress as usize)); + let local_port = data.get_value_u16(Fields::IpLocalPort as usize); + let remote_ip = + Ipv6Address::from_bytes(data.get_value_byte_array16(Fields::IpRemoteAddress as usize)); + let remote_port = data.get_value_u16(Fields::IpRemotePort as usize); + match direction { + Direction::Outbound => { + device.bandwidth_stats.update_udp_v6_tx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + Direction::Inbound => { + device.bandwidth_stats.update_udp_v6_rx( + bandwidth::Key { + local_ip, + local_port, + remote_ip, + remote_port, + }, + data_length, + ); + } + } +} diff --git a/windows_kext/kext_interface/command.go b/windows_kext/kext_interface/command.go new file mode 100644 index 00000000..45b5cc8c --- /dev/null +++ b/windows_kext/kext_interface/command.go @@ -0,0 +1,121 @@ +package kext_interface + +import ( + "encoding/binary" + "io" +) + +const ( + CommandShutdown = 0 + CommandVerdict = 1 + CommandUpdateV4 = 2 + CommandUpdateV6 = 3 + CommandClearCache = 4 + CommandGetLogs = 5 + CommandBandwidthStats = 6 + CommandPrintMemoryStats = 7 + CommandCleanEndedConnections = 8 +) + +type KextVerdict uint8 + +// Make sure this is in sync with the Rust version. +const ( + // VerdictUndecided is the default status of new connections. + VerdictUndecided KextVerdict = 0 + VerdictUndeterminable KextVerdict = 1 + VerdictAccept KextVerdict = 2 + VerdictPermanentAccept KextVerdict = 3 + VerdictBlock KextVerdict = 4 + VerdictPermanentBlock KextVerdict = 5 + VerdictDrop KextVerdict = 6 + VerdictPermanentDrop KextVerdict = 7 + VerdictRerouteToNameserver KextVerdict = 8 + VerdictRerouteToTunnel KextVerdict = 9 + VerdictFailed KextVerdict = 10 +) + +type Verdict struct { + command uint8 + Id uint64 + Verdict uint8 +} + +type RedirectV4 struct { + command uint8 + Id uint64 + RemoteAddress [4]byte + RemotePort uint16 +} + +type RedirectV6 struct { + command uint8 + Id uint64 + RemoteAddress [16]byte + RemotePort uint16 +} + +type UpdateV4 struct { + command uint8 + Protocol uint8 + LocalAddress [4]byte + LocalPort uint16 + RemoteAddress [4]byte + RemotePort uint16 + Verdict uint8 +} + +type UpdateV6 struct { + command uint8 + Protocol uint8 + LocalAddress [16]byte + LocalPort uint16 + RemoteAddress [16]byte + RemotePort uint16 + Verdict uint8 +} + +func SendShutdownCommand(writer io.Writer) error { + _, err := writer.Write([]byte{CommandShutdown}) + return err +} + +func SendVerdictCommand(writer io.Writer, verdict Verdict) error { + verdict.command = CommandVerdict + return binary.Write(writer, binary.LittleEndian, verdict) +} + +func SendUpdateV4Command(writer io.Writer, update UpdateV4) error { + update.command = CommandUpdateV4 + return binary.Write(writer, binary.LittleEndian, update) +} + +func SendUpdateV6Command(writer io.Writer, update UpdateV6) error { + update.command = CommandUpdateV6 + return binary.Write(writer, binary.LittleEndian, update) +} + +func SendClearCacheCommand(writer io.Writer) error { + _, err := writer.Write([]byte{CommandClearCache}) + return err +} + +func SendGetLogsCommand(writer io.Writer) error { + _, err := writer.Write([]byte{CommandGetLogs}) + return err +} + +func SendGetBandwidthStatsCommand(writer io.Writer) error { + _, err := writer.Write([]byte{CommandBandwidthStats}) + return err +} + +func SendPrintMemoryStatsCommand(writer io.Writer) error { + _, err := writer.Write([]byte{CommandPrintMemoryStats}) + return err +} + +func SendCleanEndedConnectionsCommand(writer io.Writer) error { + _, err := writer.Write([]byte{CommandCleanEndedConnections}) + return err +} diff --git a/windows_kext/kext_interface/info.go b/windows_kext/kext_interface/info.go new file mode 100644 index 00000000..efc7b56d --- /dev/null +++ b/windows_kext/kext_interface/info.go @@ -0,0 +1,263 @@ +package kext_interface + +import ( + "encoding/binary" + "errors" + "io" +) + +const ( + InfoLogLine = 0 + InfoConnectionIpv4 = 1 + InfoConnectionIpv6 = 2 + InfoConnectionEndEventV4 = 3 + InfoConnectionEndEventV6 = 4 + InfoBandwidthStatsV4 = 5 + InfoBandwidthStatsV6 = 6 +) + +var ErrorUnknownInfoType = errors.New("unknown info type") + +type connectionV4Internal struct { + Id uint64 + ProcessId uint64 + Direction byte + Protocol byte + LocalIp [4]byte + RemoteIp [4]byte + LocalPort uint16 + RemotePort uint16 + PayloadLayer uint8 +} + +type ConnectionV4 struct { + connectionV4Internal + Payload []byte +} + +func (c *ConnectionV4) Compare(other *ConnectionV4) bool { + return c.Id == other.Id && + c.ProcessId == other.ProcessId && + c.Direction == other.Direction && + c.Protocol == other.Protocol && + c.LocalIp == other.LocalIp && + c.RemoteIp == other.RemoteIp && + c.LocalPort == other.LocalPort && + c.RemotePort == other.RemotePort +} + +type connectionV6Internal struct { + Id uint64 + ProcessId uint64 + Direction byte + Protocol byte + LocalIp [16]byte + RemoteIp [16]byte + LocalPort uint16 + RemotePort uint16 + PayloadLayer uint8 +} + +type ConnectionV6 struct { + connectionV6Internal + Payload []byte +} + +func (c ConnectionV6) Compare(other *ConnectionV6) bool { + return c.Id == other.Id && + c.ProcessId == other.ProcessId && + c.Direction == other.Direction && + c.Protocol == other.Protocol && + c.LocalIp == other.LocalIp && + c.RemoteIp == other.RemoteIp && + c.LocalPort == other.LocalPort && + c.RemotePort == other.RemotePort +} + +type ConnectionEndV4 struct { + ProcessId uint64 + Direction byte + Protocol byte + LocalIp [4]byte + RemoteIp [4]byte + LocalPort uint16 + RemotePort uint16 +} + +type ConnectionEndV6 struct { + ProcessId uint64 + Direction byte + Protocol byte + LocalIp [16]byte + RemoteIp [16]byte + LocalPort uint16 + RemotePort uint16 +} + +type LogLine struct { + Severity byte + Line string +} + +type BandwidthValueV4 struct { + LocalIP [4]byte + LocalPort uint16 + RemoteIP [4]byte + RemotePort uint16 + TransmittedBytes uint64 + ReceivedBytes uint64 +} + +type BandwidthValueV6 struct { + LocalIP [16]byte + LocalPort uint16 + RemoteIP [16]byte + RemotePort uint16 + TransmittedBytes uint64 + ReceivedBytes uint64 +} + +type BandwidthStatsArray struct { + Protocol uint8 + ValuesV4 []BandwidthValueV4 + ValuesV6 []BandwidthValueV6 +} + +type Info struct { + ConnectionV4 *ConnectionV4 + ConnectionV6 *ConnectionV6 + ConnectionEndV4 *ConnectionEndV4 + ConnectionEndV6 *ConnectionEndV6 + LogLine *LogLine + BandwidthStats *BandwidthStatsArray +} + +func RecvInfo(reader io.Reader) (*Info, error) { + var infoType byte + err := binary.Read(reader, binary.LittleEndian, &infoType) + if err != nil { + return nil, err + } + + // Read size of data + var size uint32 + err = binary.Read(reader, binary.LittleEndian, &size) + + // Read data + switch infoType { + case InfoConnectionIpv4: + { + var fixedSizeValues connectionV4Internal + err = binary.Read(reader, binary.LittleEndian, &fixedSizeValues) + if err != nil { + return nil, err + } + // Read size of payload + var size uint32 + err = binary.Read(reader, binary.LittleEndian, &size) + if err != nil { + return nil, err + } + newInfo := ConnectionV4{connectionV4Internal: fixedSizeValues, Payload: make([]byte, size)} + err = binary.Read(reader, binary.LittleEndian, &newInfo.Payload) + return &Info{ConnectionV4: &newInfo}, nil + } + case InfoConnectionIpv6: + { + var fixedSizeValues connectionV6Internal + err = binary.Read(reader, binary.LittleEndian, &fixedSizeValues) + if err != nil { + return nil, err + } + // Read size of payload + var size uint32 + err = binary.Read(reader, binary.LittleEndian, &size) + if err != nil { + return nil, err + } + newInfo := ConnectionV6{connectionV6Internal: fixedSizeValues, Payload: make([]byte, size)} + err = binary.Read(reader, binary.LittleEndian, &newInfo.Payload) + return &Info{ConnectionV6: &newInfo}, nil + } + case InfoConnectionEndEventV4: + { + var new ConnectionEndV4 + err = binary.Read(reader, binary.LittleEndian, &new) + if err != nil { + return nil, err + } + return &Info{ConnectionEndV4: &new}, nil + } + case InfoConnectionEndEventV6: + { + var new ConnectionEndV6 + err = binary.Read(reader, binary.LittleEndian, &new) + if err != nil { + return nil, err + } + return &Info{ConnectionEndV6: &new}, nil + } + case InfoLogLine: + { + var logLine = LogLine{} + // Read severity + err = binary.Read(reader, binary.LittleEndian, &logLine.Severity) + if err != nil { + return nil, err + } + // Read string + var line = make([]byte, size-1) // -1 for the severity enum. + err = binary.Read(reader, binary.LittleEndian, &line) + logLine.Line = string(line) + return &Info{LogLine: &logLine}, nil + } + case InfoBandwidthStatsV4: + { + // Read Protocol + var protocol uint8 + err = binary.Read(reader, binary.LittleEndian, &protocol) + if err != nil { + return nil, err + } + // Read size of array + var size uint32 + err = binary.Read(reader, binary.LittleEndian, &size) + if err != nil { + return nil, err + } + // Read array + var stats_array = make([]BandwidthValueV4, size) + for i := 0; i < int(size); i++ { + binary.Read(reader, binary.LittleEndian, &stats_array[i]) + } + + return &Info{BandwidthStats: &BandwidthStatsArray{Protocol: protocol, ValuesV4: stats_array}}, nil + } + case InfoBandwidthStatsV6: + { + // Read Protocol + var protocol uint8 + err = binary.Read(reader, binary.LittleEndian, &protocol) + if err != nil { + return nil, err + } + // Read size of array + var size uint32 + err = binary.Read(reader, binary.LittleEndian, &size) + if err != nil { + return nil, err + } + // Read array + var stats_array = make([]BandwidthValueV6, size) + for i := 0; i < int(size); i++ { + binary.Read(reader, binary.LittleEndian, &stats_array[i]) + } + + return &Info{BandwidthStats: &BandwidthStatsArray{Protocol: protocol, ValuesV6: stats_array}}, nil + } + } + + unknownData := make([]byte, size) + reader.Read(unknownData) + return nil, ErrorUnknownInfoType +} diff --git a/windows_kext/kext_interface/ioctl.go b/windows_kext/kext_interface/ioctl.go new file mode 100644 index 00000000..ef165a88 --- /dev/null +++ b/windows_kext/kext_interface/ioctl.go @@ -0,0 +1,36 @@ +//go:build windows +// +build windows + +package kext_interface + +import ( + "golang.org/x/sys/windows" +) + +const ( + METHOD_BUFFERED = 0 + METHOD_IN_DIRECT = 1 + METHOD_OUT_DIRECT = 2 + METHOD_NEITHER = 3 + + SIOCTL_TYPE = 40000 +) + +func ctlCode(device_type, function, method, access uint32) uint32 { + return (device_type << 16) | (access << 14) | (function << 2) | method +} + +var ( + IOCTL_VERSION = ctlCode(SIOCTL_TYPE, 0x800, METHOD_BUFFERED, windows.FILE_READ_DATA|windows.FILE_WRITE_DATA) + IOCTL_SHUTDOWN_REQUEST = ctlCode(SIOCTL_TYPE, 0x801, METHOD_BUFFERED, windows.FILE_READ_DATA|windows.FILE_WRITE_DATA) +) + +func ReadVersion(file *KextFile) ([]uint8, error) { + data := make([]uint8, 4) + _, err := file.deviceIOControl(IOCTL_VERSION, nil, data) + + if err != nil { + return nil, err + } + return data, nil +} diff --git a/windows_kext/kext_interface/kext.go b/windows_kext/kext_interface/kext.go new file mode 100644 index 00000000..d065887b --- /dev/null +++ b/windows_kext/kext_interface/kext.go @@ -0,0 +1,247 @@ +//go:build windows +// +build windows + +package kext_interface + +import ( + _ "embed" + "fmt" + "strconv" + "strings" + "syscall" + "time" + + "golang.org/x/sys/windows" +) + +var ( + //go:embed version.txt + versionTxt string + + // 4 byte version of the Kext interface + InterfaceVersion = func() (v [4]byte) { + // Parse version from file "version.txt". Expected format: [0, 1, 2, 3] + s := strings.TrimSpace(versionTxt) + s = strings.TrimPrefix(s, "[") + s = strings.TrimSuffix(s, "]") + str_ver := strings.Split(s, ",") + for i := range v { + n, err := strconv.Atoi(strings.TrimSpace(str_ver[i])) + if err != nil { + panic(err) + } + v[i] = byte(n) + } + return + }() +) + +const winInvalidHandleValue = windows.Handle(^uintptr(0)) // Max value +const stopServiceTimeoutDuration = time.Duration(30 * time.Second) + +type KextService struct { + handle windows.Handle + driverName string +} + +func (s *KextService) isValid() bool { + return s != nil && s.handle != winInvalidHandleValue && s.handle != 0 +} + +func (s *KextService) isRunning() (bool, error) { + if !s.isValid() { + return false, fmt.Errorf("kext service not initialized") + } + var status windows.SERVICE_STATUS + err := windows.QueryServiceStatus(s.handle, &status) + if err != nil { + return false, err + } + return status.CurrentState == windows.SERVICE_RUNNING, nil +} + +func (s *KextService) waitForServiceStatus(neededStatus uint32, timeLimit time.Duration) (bool, error) { + var status windows.SERVICE_STATUS + status.CurrentState = windows.SERVICE_NO_CHANGE + start := time.Now() + for status.CurrentState == neededStatus { + err := windows.QueryServiceStatus(s.handle, &status) + if err != nil { + return false, fmt.Errorf("failed while waiting for service to start: %w", err) + } + + if time.Since(start) > timeLimit { + return false, fmt.Errorf("time limit reached") + } + + // Sleep for 1/10 of the wait hint, recommended time from microsoft + time.Sleep(time.Duration((status.WaitHint / 10)) * time.Millisecond) + } + + return true, nil +} + +func (s *KextService) Start(wait bool) error { + if !s.isValid() { + return fmt.Errorf("kext service not initialized") + } + + // Start the service: + err := windows.StartService(s.handle, 0, nil) + + if err != nil { + err = windows.GetLastError() + if err != windows.ERROR_SERVICE_ALREADY_RUNNING { + // Failed to start service; clean-up: + var status windows.SERVICE_STATUS + _ = windows.ControlService(s.handle, windows.SERVICE_CONTROL_STOP, &status) + _ = windows.DeleteService(s.handle) + _ = windows.CloseServiceHandle(s.handle) + s.handle = winInvalidHandleValue + return err + } + } + + // Wait for service to start + if wait { + success, err := s.waitForServiceStatus(windows.SERVICE_RUNNING, stopServiceTimeoutDuration) + if err != nil || !success { + return fmt.Errorf("service did not start: %w", err) + } + } + + return nil +} + +func (s *KextService) GetHandle() windows.Handle { + return s.handle +} + +func (s *KextService) Stop(wait bool) error { + if !s.isValid() { + return fmt.Errorf("kext service not initialized") + } + + // Stop the service + var status windows.SERVICE_STATUS + err := windows.ControlService(s.handle, windows.SERVICE_CONTROL_STOP, &status) + if err != nil { + return fmt.Errorf("service failed to stop: %w", err) + } + + // Wait for service to stop + if wait { + success, err := s.waitForServiceStatus(windows.SERVICE_STOPPED, time.Duration(10*time.Second)) + if err != nil || !success { + return fmt.Errorf("service did not stop: %w", err) + } + } + + return nil +} + +func (s *KextService) Delete() error { + if !s.isValid() { + return fmt.Errorf("kext service not initialized") + } + + err := windows.DeleteService(s.handle) + if err != nil { + return fmt.Errorf("failed to delete service: %s", err) + } + + // Service wont be deleted until all handles are closed. + err = windows.CloseServiceHandle(s.handle) + if err != nil { + return fmt.Errorf("failed to close service handle: %s", err) + } + + s.handle = winInvalidHandleValue + return nil +} + +func (s *KextService) WaitUntilDeleted(serviceManager windows.Handle) error { + driverNameU16, err := syscall.UTF16FromString(s.driverName) + if err != nil { + return fmt.Errorf("failed to convert driver name to UTF16 string: %w", err) + } + // Wait until we can no longer open the old service. + // Not very efficient but NotifyServiceStatusChange cannot be used with driver service. + start := time.Now() + timeLimit := time.Duration(30 * time.Second) + for { + handle, err := windows.OpenService(serviceManager, &driverNameU16[0], windows.SERVICE_ALL_ACCESS) + if err != nil { + break + } + _ = windows.CloseServiceHandle(handle) + + if time.Since(start) > timeLimit { + return fmt.Errorf("time limit reached") + } + + time.Sleep(100 * time.Millisecond) + } + + return nil +} + +func (s *KextService) OpenFile(readBufferSize int) (*KextFile, error) { + if !s.isValid() { + return nil, fmt.Errorf("invalid kext object") + } + + driverNameU16, err := syscall.UTF16FromString(`\\.\` + s.driverName) + if err != nil { + return nil, fmt.Errorf("failed to convert driver driverName to UTF16 string %w", err) + } + + handle, err := windows.CreateFile(&driverNameU16[0], windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OVERLAPPED, 0) + if err != nil { + return nil, err + } + + return &KextFile{handle: handle, buffer: make([]byte, readBufferSize)}, nil +} + +func CreateKextService(driverName string, driverPath string) (*KextService, error) { + // Open the service manager: + manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_ALL_ACCESS) + if err != nil { + return nil, fmt.Errorf("failed to open service manager: %d", err) + } + defer windows.CloseServiceHandle(manager) + + driverNameU16, err := syscall.UTF16FromString(driverName) + if err != nil { + return nil, fmt.Errorf("failed to convert driver name to UTF16 string: %w", err) + } + + // Check if there is an old service. + service, err := windows.OpenService(manager, &driverNameU16[0], windows.SERVICE_ALL_ACCESS) + if err == nil { + fmt.Println("kext: old driver service was found") + oldService := &KextService{handle: service, driverName: driverName} + oldService.Stop(true) + err = oldService.Delete() + if err != nil { + return nil, err + } + err := oldService.WaitUntilDeleted(manager) + if err != nil { + return nil, err + } + + service = winInvalidHandleValue + fmt.Println("kext: old driver service was deleted successfully") + } + + driverPathU16, err := syscall.UTF16FromString(driverPath) + + // Create the service + service, err = windows.CreateService(manager, &driverNameU16[0], &driverNameU16[0], windows.SERVICE_ALL_ACCESS, windows.SERVICE_KERNEL_DRIVER, windows.SERVICE_DEMAND_START, windows.SERVICE_ERROR_NORMAL, &driverPathU16[0], nil, nil, nil, nil, nil) + if err != nil { + return nil, err + } + return &KextService{handle: service, driverName: driverName}, nil +} diff --git a/windows_kext/kext_interface/kext_file.go b/windows_kext/kext_interface/kext_file.go new file mode 100644 index 00000000..2dba124e --- /dev/null +++ b/windows_kext/kext_interface/kext_file.go @@ -0,0 +1,98 @@ +//go:build windows +// +build windows + +package kext_interface + +import ( + "golang.org/x/sys/windows" +) + +type KextFile struct { + handle windows.Handle + buffer []byte + read_slice []byte +} + +func (f *KextFile) Read(buffer []byte) (int, error) { + if f.read_slice == nil || len(f.read_slice) == 0 { + err := f.refill_read_buffer() + if err != nil { + return 0, err + } + } + + if len(f.read_slice) >= len(buffer) { + // Write all requested bytes. + copy(buffer, f.read_slice[0:len(buffer)]) + f.read_slice = f.read_slice[len(buffer):] + } else { + // Write all available bytes and read again. + copy(buffer[0:len(f.read_slice)], f.read_slice) + copiedBytes := len(f.read_slice) + f.read_slice = nil + _, err := f.Read(buffer[copiedBytes:]) + if err != nil { + return 0, err + } + } + + return len(buffer), nil +} + +func (f *KextFile) refill_read_buffer() error { + var count uint32 = 0 + overlapped := &windows.Overlapped{} + err := windows.ReadFile(f.handle, f.buffer[:], &count, overlapped) + if err != nil { + return err + } + f.read_slice = f.buffer[0:count] + + return nil +} + +func (f *KextFile) Write(buffer []byte) (int, error) { + var count uint32 = 0 + overlapped := &windows.Overlapped{} + err := windows.WriteFile(f.handle, buffer, &count, overlapped) + return int(count), err +} + +func (f *KextFile) Close() error { + err := windows.CloseHandle(f.handle) + f.handle = winInvalidHandleValue + return err +} + +func (f *KextFile) deviceIOControl(code uint32, inData []byte, outData []byte) (*windows.Overlapped, error) { + var inDataPtr *byte = nil + var inDataSize uint32 = 0 + if inData != nil { + inDataPtr = &inData[0] + inDataSize = uint32(len(inData)) + } + + var outDataPtr *byte = nil + var outDataSize uint32 = 0 + if outData != nil { + outDataPtr = &outData[0] + outDataSize = uint32(len(outData)) + } + + overlapped := &windows.Overlapped{} + err := windows.DeviceIoControl(f.handle, + code, + inDataPtr, inDataSize, + outDataPtr, outDataSize, + nil, overlapped) + + if err != nil { + return nil, err + } + + return overlapped, nil +} + +func (f *KextFile) GetHandle() windows.Handle { + return f.handle +} diff --git a/windows_kext/kext_interface/kext_file_test.go b/windows_kext/kext_interface/kext_file_test.go new file mode 100644 index 00000000..0731df7c --- /dev/null +++ b/windows_kext/kext_interface/kext_file_test.go @@ -0,0 +1,12 @@ +//go:build linux +// +build linux + +package kext_interface + +type KextFile struct{} + +func (f *KextFile) Read(buffer []byte) (int, error) { + return 0, nil +} + +func (f *KextFile) flush_buffer() {} diff --git a/windows_kext/kext_interface/protocol_test.go b/windows_kext/kext_interface/protocol_test.go new file mode 100644 index 00000000..5807bf3e --- /dev/null +++ b/windows_kext/kext_interface/protocol_test.go @@ -0,0 +1,246 @@ +package kext_interface + +import ( + "bytes" + "io" + "math/rand" + "os" + "testing" +) + +func TestRustInfoFile(t *testing.T) { + file, err := os.Open("../protocol/rust_info_test.bin") + if err != nil { + panic(err) + } + defer file.Close() + for { + info, err := RecvInfo(file) + if err != nil { + if err != io.EOF { + t.Errorf("unexpected error: %s\n", err) + } + return + } + if info.LogLine != nil { + if info.LogLine.Severity != 1 { + t.Errorf("unexpected Log severity: %d\n", info.LogLine.Severity) + } + + if info.LogLine.Line != "prefix: test log" { + t.Errorf("unexpected Log line: %s\n", info.LogLine.Line) + } + } else if info.ConnectionV4 != nil { + conn := info.ConnectionV4 + expected := connectionV4Internal{ + Id: 1, + ProcessId: 2, + Direction: 3, + Protocol: 4, + LocalIp: [4]byte{1, 2, 3, 4}, + RemoteIp: [4]byte{2, 3, 4, 5}, + LocalPort: 5, + RemotePort: 6, + PayloadLayer: 7, + } + if conn.connectionV4Internal != expected { + t.Errorf("unexpected ConnectionV4: %+v\n", conn) + } + if !bytes.Equal(conn.Payload, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) { + t.Errorf("unexpected ConnectionV4 payload: %+v\n", conn.Payload) + } + } else if info.ConnectionV6 != nil { + conn := info.ConnectionV6 + expected := connectionV6Internal{ + Id: 1, + ProcessId: 2, + Direction: 3, + Protocol: 4, + LocalIp: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + RemoteIp: [16]byte{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + LocalPort: 5, + RemotePort: 6, + PayloadLayer: 7, + } + if conn.connectionV6Internal != expected { + t.Errorf("unexpected ConnectionV6: %+v\n", conn) + } + if !bytes.Equal(conn.Payload, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) { + t.Errorf("unexpected ConnectionV6 payload: %+v\n", conn.Payload) + } + } else if info.ConnectionEndV4 != nil { + endEvent := info.ConnectionEndV4 + expected := ConnectionEndV4{ + ProcessId: 1, + Direction: 2, + Protocol: 3, + LocalIp: [4]byte{1, 2, 3, 4}, + RemoteIp: [4]byte{2, 3, 4, 5}, + LocalPort: 4, + RemotePort: 5, + } + if *endEvent != expected { + t.Errorf("unexpected ConnectionEndV4: %+v\n", endEvent) + } + } else if info.ConnectionEndV6 != nil { + endEvent := info.ConnectionEndV6 + expected := ConnectionEndV6{ + ProcessId: 1, + Direction: 2, + Protocol: 3, + LocalIp: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + RemoteIp: [16]byte{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + LocalPort: 4, + RemotePort: 5, + } + if *endEvent != expected { + t.Errorf("unexpected ConnectionEndV6: %+v\n", endEvent) + } + } else if info.BandwidthStats != nil { + stats := info.BandwidthStats + if stats.Protocol != 1 { + t.Errorf("unexpected Bandwidth stats protocol: %d\n", stats.Protocol) + } + + if stats.ValuesV4 != nil { + if len(stats.ValuesV4) != 2 { + t.Errorf("unexpected Bandwidth stats value length: %d\n", len(stats.ValuesV4)) + } + expected1 := BandwidthValueV4{ + LocalIP: [4]byte{1, 2, 3, 4}, + LocalPort: 1, + RemoteIP: [4]byte{2, 3, 4, 5}, + RemotePort: 2, + TransmittedBytes: 3, + ReceivedBytes: 4, + } + if stats.ValuesV4[0] != expected1 { + t.Errorf("unexpected Bandwidth stats value: %+v expected: %+v\n", stats.ValuesV4[0], expected1) + } + expected2 := BandwidthValueV4{ + LocalIP: [4]byte{1, 2, 3, 4}, + LocalPort: 5, + RemoteIP: [4]byte{2, 3, 4, 5}, + RemotePort: 6, + TransmittedBytes: 7, + ReceivedBytes: 8, + } + if stats.ValuesV4[1] != expected2 { + t.Errorf("unexpected Bandwidth stats value: %+v expected: %+v\n", stats.ValuesV4[1], expected2) + } + + } else if stats.ValuesV6 != nil { + if len(stats.ValuesV6) != 2 { + t.Errorf("unexpected Bandwidth stats value length: %d\n", len(stats.ValuesV6)) + } + + expected1 := BandwidthValueV6{ + LocalIP: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + LocalPort: 1, + RemoteIP: [16]byte{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + RemotePort: 2, + TransmittedBytes: 3, + ReceivedBytes: 4, + } + if stats.ValuesV6[0] != expected1 { + t.Errorf("unexpected Bandwidth stats value: %+v expected: %+v\n", stats.ValuesV6[0], expected1) + } + expected2 := BandwidthValueV6{ + LocalIP: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + LocalPort: 5, + RemoteIP: [16]byte{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + RemotePort: 6, + TransmittedBytes: 7, + ReceivedBytes: 8, + } + if stats.ValuesV6[1] != expected2 { + t.Errorf("unexpected Bandwidth stats value: %+v expected: %+v\n", stats.ValuesV6[1], expected2) + } + + } + } + } +} + +func TestGenerateCommandFile(t *testing.T) { + file, err := os.Create("go_command_test.bin") + if err != nil { + t.Errorf("failed to create file: %s", err) + } + defer file.Close() + enums := []byte{ + CommandShutdown, + CommandVerdict, + CommandUpdateV4, + CommandUpdateV6, + CommandClearCache, + CommandGetLogs, + CommandBandwidthStats, + CommandCleanEndedConnections, + } + + selected := make([]byte, 5000) + for i := range selected { + selected[i] = enums[rand.Intn(len(enums))] + } + + for _, value := range selected { + switch value { + case CommandShutdown: + { + SendShutdownCommand(file) + } + case CommandVerdict: + { + SendVerdictCommand(file, Verdict{ + Id: 1, + Verdict: 2, + }) + } + case CommandUpdateV4: + { + SendUpdateV4Command(file, UpdateV4{ + Protocol: 1, + LocalAddress: [4]byte{1, 2, 3, 4}, + LocalPort: 2, + RemoteAddress: [4]byte{2, 3, 4, 5}, + RemotePort: 3, + Verdict: 4, + }) + } + case CommandUpdateV6: + { + + SendUpdateV6Command(file, UpdateV6{ + Protocol: 1, + LocalAddress: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + LocalPort: 2, + RemoteAddress: [16]byte{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + RemotePort: 3, + Verdict: 4, + }) + } + case CommandClearCache: + { + SendClearCacheCommand(file) + } + case CommandGetLogs: + { + SendGetLogsCommand(file) + } + case CommandBandwidthStats: + { + SendGetBandwidthStatsCommand(file) + } + case CommandPrintMemoryStats: + { + SendPrintMemoryStatsCommand(file) + } + case CommandCleanEndedConnections: + { + SendCleanEndedConnectionsCommand(file) + } + } + } + +} diff --git a/windows_kext/kext_interface/version.txt b/windows_kext/kext_interface/version.txt new file mode 100644 index 00000000..9475e7df --- /dev/null +++ b/windows_kext/kext_interface/version.txt @@ -0,0 +1 @@ +[2, 0, 0, 0] \ No newline at end of file diff --git a/windows_kext/protocol/Cargo.lock b/windows_kext/protocol/Cargo.lock new file mode 100644 index 00000000..857aaf93 --- /dev/null +++ b/windows_kext/protocol/Cargo.lock @@ -0,0 +1,193 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protocol" +version = "0.1.0" +dependencies = [ + "num", + "num-derive", + "num-traits", + "rand", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/windows_kext/protocol/Cargo.toml b/windows_kext/protocol/Cargo.toml new file mode 100644 index 00000000..03e14f2a --- /dev/null +++ b/windows_kext/protocol/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "protocol" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +num = { version = "0.4", default-features = false } +num-derive = { version = "0.4", default-features = false } +num-traits = { version = "0.2", default-features = false } + +[dev-dependencies] +rand = "0.8.5" diff --git a/windows_kext/protocol/README.md b/windows_kext/protocol/README.md new file mode 100644 index 00000000..58de329d --- /dev/null +++ b/windows_kext/protocol/README.md @@ -0,0 +1,4 @@ +# Protocol + +Defines protocol that communicates with `kext_interface` / Portmaster. + diff --git a/windows_kext/protocol/src/command.rs b/windows_kext/protocol/src/command.rs new file mode 100644 index 00000000..6f17e2fc --- /dev/null +++ b/windows_kext/protocol/src/command.rs @@ -0,0 +1,158 @@ +// Commands from user space + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +#[repr(u8)] +#[derive(Clone, Copy, FromPrimitive)] +#[rustfmt::skip] +pub enum CommandType { + Shutdown = 0, + Verdict = 1, + UpdateV4 = 2, + UpdateV6 = 3, + ClearCache = 4, + GetLogs = 5, + GetBandwidthStats = 6, + PrintMemoryStats = 7, + CleanEndedConnections = 8, +} + +#[repr(C, packed)] +pub struct Command { + pub command_type: CommandType, + value: [u8; 0], +} + +#[repr(C, packed)] +#[derive(Debug, PartialEq, Eq)] +pub struct Verdict { + pub id: u64, + pub verdict: u8, +} + +#[repr(C, packed)] +#[derive(Debug, PartialEq, Eq)] +pub struct UpdateV4 { + pub protocol: u8, + pub local_address: [u8; 4], + pub local_port: u16, + pub remote_address: [u8; 4], + pub remote_port: u16, + pub verdict: u8, +} + +#[repr(C, packed)] +#[derive(Debug, PartialEq, Eq)] +pub struct UpdateV6 { + pub protocol: u8, + pub local_address: [u8; 16], + pub local_port: u16, + pub remote_address: [u8; 16], + pub remote_port: u16, + pub verdict: u8, +} + +pub fn parse_type(bytes: &[u8]) -> Option { + FromPrimitive::from_u8(bytes[0]) +} + +pub fn parse_verdict(bytes: &[u8]) -> &Verdict { + as_type(bytes) +} + +pub fn parse_update_v4(bytes: &[u8]) -> &UpdateV4 { + as_type(bytes) +} + +pub fn parse_update_v6(bytes: &[u8]) -> &UpdateV6 { + as_type(bytes) +} + +fn as_type(bytes: &[u8]) -> &T { + let ptr: *const u8 = &bytes[0]; + let t_ptr: *const T = ptr as _; + unsafe { t_ptr.as_ref().unwrap() } +} + +#[cfg(test)] +use std::fs::File; +#[cfg(test)] +use std::io::Read; +#[cfg(test)] +use std::mem::size_of; +#[cfg(test)] +use std::panic; + +#[test] +fn test_go_command_file() { + let mut file = File::open("../kext_interface/go_command_test.bin").unwrap(); + loop { + let mut command: [u8; 1] = [0]; + let bytes_count = file.read(&mut command).unwrap(); + if bytes_count == 0 { + return; + } + if let Some(command) = parse_type(&command) { + match command { + CommandType::Shutdown => {} + CommandType::Verdict => { + let mut buf = [0; size_of::()]; + let bytes_count = file.read(&mut buf).unwrap(); + if bytes_count != size_of::() { + panic!("unexpected bytes count") + } + + assert_eq!(parse_verdict(&buf), &Verdict { id: 1, verdict: 2 }) + } + CommandType::UpdateV4 => { + let mut buf = [0; size_of::()]; + let bytes_count = file.read(&mut buf).unwrap(); + if bytes_count != size_of::() { + panic!("unexpected bytes count") + } + + assert_eq!( + parse_update_v4(&buf), + &UpdateV4 { + protocol: 1, + local_address: [1, 2, 3, 4], + local_port: 2, + remote_address: [2, 3, 4, 5], + remote_port: 3, + verdict: 4 + } + ) + } + CommandType::UpdateV6 => { + let mut buf = [0; size_of::()]; + let bytes_count = file.read(&mut buf).unwrap(); + if bytes_count != size_of::() { + panic!("unexpected bytes count") + } + + assert_eq!( + parse_update_v6(&buf), + &UpdateV6 { + protocol: 1, + local_address: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + local_port: 2, + remote_address: [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 + ], + remote_port: 3, + verdict: 4 + } + ) + } + CommandType::ClearCache => {} + CommandType::GetLogs => {} + CommandType::GetBandwidthStats => {} + CommandType::PrintMemoryStats => {} + CommandType::CleanEndedConnections => {} + } + } else { + panic!("Unknown command: {}", command[0]); + } + } +} diff --git a/windows_kext/protocol/src/info.rs b/windows_kext/protocol/src/info.rs new file mode 100644 index 00000000..5018c511 --- /dev/null +++ b/windows_kext/protocol/src/info.rs @@ -0,0 +1,552 @@ +use alloc::vec::Vec; + +#[repr(u8)] +#[derive(Clone, Copy)] +enum InfoType { + LogLine = 0, + ConnectionIpv4 = 1, + ConnectionIpv6 = 2, + ConnectionEndEventV4 = 3, + ConnectionEndEventV6 = 4, + BandwidthStatsV4 = 5, + BandwidthStatsV6 = 6, +} + +// Fallow this pattern when adding new packets: [InfoType: u8, data_size_in_bytes: u32, data: ...] + +trait PushBytes { + fn push(self, vec: &mut Vec); +} + +impl PushBytes for u8 { + fn push(self, vec: &mut Vec) { + vec.push(self); + } +} + +impl PushBytes for InfoType { + fn push(self, vec: &mut Vec) { + vec.push(self as u8); + } +} + +impl PushBytes for u16 { + fn push(self, vec: &mut Vec) { + vec.extend_from_slice(&u16::to_le_bytes(self)); + } +} + +impl PushBytes for u32 { + fn push(self, vec: &mut Vec) { + vec.extend_from_slice(&u32::to_le_bytes(self)); + } +} + +impl PushBytes for u64 { + fn push(self, vec: &mut Vec) { + vec.extend_from_slice(&u64::to_le_bytes(self)); + } +} + +impl PushBytes for usize { + fn push(self, vec: &mut Vec) { + vec.extend_from_slice(&usize::to_le_bytes(self)); + } +} + +impl PushBytes for [u8; 4] { + fn push(self, vec: &mut Vec) { + vec.extend_from_slice(&self); + } +} + +impl PushBytes for [u8; 16] { + fn push(self, vec: &mut Vec) { + vec.extend_from_slice(&self); + } +} + +impl PushBytes for &[u8] { + fn push(self, vec: &mut Vec) { + vec.extend_from_slice(self); + } +} + +macro_rules! push_bytes { + ($vec:expr,$value:expr) => { + PushBytes::push($value, $vec); + }; +} + +macro_rules! get_combined_size{ + ($($a:expr),*)=>{{0 $(+core::mem::size_of_val(&$a))*}} +} + +pub struct Info(Vec); + +impl Info { + fn new(info_type: InfoType, size: usize) -> Self { + let mut vec = Vec::with_capacity(size + 5); // +1 for the info type +4 for the size. + push_bytes!(&mut vec, info_type); + push_bytes!(&mut vec, size as u32); + Self(vec) + } + + fn with_capacity(info_type: InfoType, capacity: usize) -> Self { + let mut vec = Vec::with_capacity(capacity + 5); // +1 for the info type + 4 for the size. + push_bytes!(&mut vec, info_type); + push_bytes!(&mut vec, 0 as u32); + Self(vec) + } + + #[cfg(test)] + fn assert_size(&self) { + let size = u32::from_le_bytes([self.0[1], self.0[2], self.0[3], self.0[4]]) as usize; + assert_eq!(size, self.0.len() - 5); + } + + fn update_size(&mut self) { + let size = self.0.len() - 5; + let bytes = &mut self.0; + bytes[1] = size as u8; + bytes[2] = (size >> 8) as u8; + bytes[3] = (size >> 16) as u8; + bytes[4] = (size >> 24) as u8; + } + + pub fn as_bytes(&self) -> &[u8] { + return self.0.as_slice(); + } +} + +impl core::fmt::Write for Info { + fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { + const MAX_CAPACITY: usize = 500; + + let space_left = self.0.capacity() - self.0.len(); + if s.len() > space_left { + if self.0.capacity() < MAX_CAPACITY { + self.0.reserve(MAX_CAPACITY); + } else { + return Ok(()); + } + } + + self.0.extend_from_slice(s.as_bytes()); + self.update_size(); + Ok(()) + } +} + +pub fn connection_info_v4( + id: u64, + process_id: u64, + direction: u8, + protocol: u8, + local_ip: [u8; 4], + remote_ip: [u8; 4], + local_port: u16, + remote_port: u16, + payload_layer: u8, + payload: &[u8], +) -> Info { + let mut size = get_combined_size!( + id, + process_id, + direction, + protocol, + local_ip, + remote_ip, + local_port, + remote_port, + payload_layer, + payload.len() as u32 + ); + size += payload.len(); + + let mut info = Info::new(InfoType::ConnectionIpv4, size); + let vec = &mut info.0; + push_bytes!(vec, id); + push_bytes!(vec, process_id); + push_bytes!(vec, direction); + push_bytes!(vec, protocol); + push_bytes!(vec, local_ip); + push_bytes!(vec, remote_ip); + push_bytes!(vec, local_port); + push_bytes!(vec, remote_port); + push_bytes!(vec, payload_layer); + push_bytes!(vec, payload.len() as u32); + push_bytes!(vec, payload); + info +} + +pub fn connection_info_v6( + id: u64, + process_id: u64, + direction: u8, + protocol: u8, + local_ip: [u8; 16], + remote_ip: [u8; 16], + local_port: u16, + remote_port: u16, + payload_layer: u8, + payload: &[u8], +) -> Info { + let mut size = get_combined_size!( + id, + process_id, + direction, + protocol, + local_ip, + remote_ip, + local_port, + remote_port, + payload_layer, + payload.len() as u32 + ); + size += payload.len(); + let mut info = Info::new(InfoType::ConnectionIpv6, size); + let vec = &mut info.0; + push_bytes!(vec, id); + push_bytes!(vec, process_id); + push_bytes!(vec, direction); + push_bytes!(vec, protocol); + push_bytes!(vec, local_ip); + push_bytes!(vec, remote_ip); + push_bytes!(vec, local_port); + push_bytes!(vec, remote_port); + push_bytes!(vec, payload_layer); + push_bytes!(vec, payload.len() as u32); + if !payload.is_empty() { + push_bytes!(vec, payload); + } + info +} + +pub fn connection_end_event_v4_info( + process_id: u64, + direction: u8, + protocol: u8, + local_ip: [u8; 4], + remote_ip: [u8; 4], + local_port: u16, + remote_port: u16, +) -> Info { + let size = get_combined_size!( + process_id, + direction, + protocol, + local_ip, + remote_ip, + local_port, + remote_port + ); + let mut info = Info::new(InfoType::ConnectionEndEventV4, size); + let vec = &mut info.0; + push_bytes!(vec, process_id); + push_bytes!(vec, direction); + push_bytes!(vec, protocol); + push_bytes!(vec, local_ip); + push_bytes!(vec, remote_ip); + push_bytes!(vec, local_port); + push_bytes!(vec, remote_port); + info +} + +pub fn connection_end_event_v6_info( + process_id: u64, + direction: u8, + protocol: u8, + local_ip: [u8; 16], + remote_ip: [u8; 16], + local_port: u16, + remote_port: u16, +) -> Info { + let size = get_combined_size!( + process_id, + direction, + protocol, + local_ip, + remote_ip, + local_port, + remote_port + ); + let mut info = Info::new(InfoType::ConnectionEndEventV6, size); + let vec = &mut info.0; + push_bytes!(vec, process_id); + push_bytes!(vec, direction); + push_bytes!(vec, protocol); + push_bytes!(vec, local_ip); + push_bytes!(vec, remote_ip); + push_bytes!(vec, local_port); + push_bytes!(vec, remote_port); + info +} + +#[repr(u8)] +#[derive(Clone, Copy)] +pub enum Severity { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6, + Disabled = 7, +} + +// pub fn log_line(severity: Severity, prefix: String, line: String) -> Info { +// let mut size = get_combined_size!(severity); +// size += prefix.len() + line.len(); + +// let mut info = Info::new(InfoType::LogLine, size); +// let vec = &mut info.0; +// push_bytes!(vec, severity as u8); +// push_bytes!(vec, prefix.as_bytes()); +// push_bytes!(vec, line.as_bytes()); +// info +// } + +pub fn log_line(severity: Severity, capacity: usize) -> Info { + let mut info = Info::with_capacity(InfoType::LogLine, capacity); + let vec = &mut info.0; + push_bytes!(vec, severity as u8); + info +} + +// Special struct for Bandwidth stats +pub struct BandwidthValueV4 { + pub local_ip: [u8; 4], + pub local_port: u16, + pub remote_ip: [u8; 4], + pub remote_port: u16, + pub transmitted_bytes: u64, + pub received_bytes: u64, +} + +impl BandwidthValueV4 { + fn get_size(&self) -> usize { + get_combined_size!( + self.local_ip, + self.local_port, + self.remote_ip, + self.remote_port, + self.transmitted_bytes, + self.received_bytes + ) + } +} + +impl PushBytes for BandwidthValueV4 { + fn push(self, vec: &mut Vec) { + push_bytes!(vec, self.local_ip); + push_bytes!(vec, self.local_port); + push_bytes!(vec, self.remote_ip); + push_bytes!(vec, self.remote_port); + push_bytes!(vec, self.transmitted_bytes); + push_bytes!(vec, self.received_bytes); + } +} + +pub struct BandwidthValueV6 { + pub local_ip: [u8; 16], + pub local_port: u16, + pub remote_ip: [u8; 16], + pub remote_port: u16, + pub transmitted_bytes: u64, + pub received_bytes: u64, +} + +impl BandwidthValueV6 { + fn get_size(&self) -> usize { + get_combined_size!( + self.local_ip, + self.local_port, + self.remote_ip, + self.remote_port, + self.transmitted_bytes, + self.received_bytes + ) + } +} + +impl PushBytes for BandwidthValueV6 { + fn push(self, vec: &mut Vec) { + push_bytes!(vec, self.local_ip); + push_bytes!(vec, self.local_port); + push_bytes!(vec, self.remote_ip); + push_bytes!(vec, self.remote_port); + push_bytes!(vec, self.transmitted_bytes); + push_bytes!(vec, self.received_bytes); + } +} + +pub fn bandiwth_stats_array_v4(protocol: u8, values: Vec) -> Info { + let mut size = get_combined_size!(protocol, values.len() as u32); + + if !values.is_empty() { + size += values[0].get_size() * values.len(); + } + + let mut info = Info::new(InfoType::BandwidthStatsV4, size); + let vec = &mut info.0; + push_bytes!(vec, protocol); + push_bytes!(vec, values.len() as u32); + for v in values { + push_bytes!(vec, v); + } + info +} + +pub fn bandiwth_stats_array_v6(protocol: u8, values: Vec) -> Info { + let mut size = get_combined_size!(protocol, values.len() as u32); + + if !values.is_empty() { + size += values[0].get_size() * values.len(); + } + + let mut info = Info::new(InfoType::BandwidthStatsV6, size); + let vec = &mut info.0; + push_bytes!(vec, protocol); + push_bytes!(vec, values.len() as u32); + for v in values { + push_bytes!(vec, v); + } + info +} + +#[cfg(test)] +use std::fs::File; +#[cfg(test)] +use std::io::Write; + +#[cfg(test)] +use rand::seq::SliceRandom; + +#[test] +fn generate_test_info_file() -> Result<(), std::io::Error> { + let mut file = File::create("rust_info_test.bin")?; + let enums = [ + InfoType::LogLine, + InfoType::ConnectionIpv4, + InfoType::ConnectionIpv6, + InfoType::ConnectionEndEventV4, + InfoType::ConnectionEndEventV6, + InfoType::BandwidthStatsV4, + InfoType::BandwidthStatsV6, + ]; + + let mut selected: Vec = Vec::with_capacity(1000); + let mut rng = rand::thread_rng(); + for _ in 0..selected.capacity() { + selected.push(enums.choose(&mut rng).unwrap().clone()); + } + + for value in selected { + file.write_all(&match value { + InfoType::LogLine => { + let mut info = log_line(Severity::Trace, 5); + use std::fmt::Write; + _ = write!(info, "prefix: test log"); + info.assert_size(); + info.0 + } + InfoType::ConnectionIpv4 => { + let info = connection_info_v4( + 1, + 2, + 3, + 4, + [1, 2, 3, 4], + [2, 3, 4, 5], + 5, + 6, + 7, + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ); + info.assert_size(); + info.0 + } + + InfoType::ConnectionIpv6 => { + let info = connection_info_v6( + 1, + 2, + 3, + 4, + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + 5, + 6, + 7, + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ); + info.assert_size(); + info.0 + } + InfoType::ConnectionEndEventV4 => { + let info = connection_end_event_v4_info(1, 2, 3, [1, 2, 3, 4], [2, 3, 4, 5], 4, 5); + info.assert_size(); + info.0 + } + InfoType::ConnectionEndEventV6 => { + let info = connection_end_event_v6_info( + 1, + 2, + 3, + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + 4, + 5, + ); + info.assert_size(); + info.0 + } + InfoType::BandwidthStatsV4 => { + let mut vec = Vec::new(); + vec.push(BandwidthValueV4 { + local_ip: [1, 2, 3, 4], + local_port: 1, + remote_ip: [2, 3, 4, 5], + remote_port: 2, + transmitted_bytes: 3, + received_bytes: 4, + }); + vec.push(BandwidthValueV4 { + local_ip: [1, 2, 3, 4], + local_port: 5, + remote_ip: [2, 3, 4, 5], + remote_port: 6, + transmitted_bytes: 7, + received_bytes: 8, + }); + let info = bandiwth_stats_array_v4(1, vec); + info.assert_size(); + info.0 + } + InfoType::BandwidthStatsV6 => { + let mut vec = Vec::new(); + vec.push(BandwidthValueV6 { + local_ip: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + local_port: 1, + remote_ip: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + remote_port: 2, + transmitted_bytes: 3, + received_bytes: 4, + }); + vec.push(BandwidthValueV6 { + local_ip: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + local_port: 5, + remote_ip: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + remote_port: 6, + transmitted_bytes: 7, + received_bytes: 8, + }); + let info = bandiwth_stats_array_v6(1, vec); + info.assert_size(); + info.0 + } + })?; + } + return Ok(()); +} diff --git a/windows_kext/protocol/src/lib.rs b/windows_kext/protocol/src/lib.rs new file mode 100644 index 00000000..d4d09d51 --- /dev/null +++ b/windows_kext/protocol/src/lib.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(test), no_std)] +extern crate alloc; + +pub mod command; +pub mod info; diff --git a/windows_kext/release/Cargo.lock b/windows_kext/release/Cargo.lock new file mode 100644 index 00000000..f71c78b4 --- /dev/null +++ b/windows_kext/release/Cargo.lock @@ -0,0 +1,525 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "handlebars" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab283476b99e66691dee3f1640fea91487a8d81f50fb5ecc75538f8f8879a1e4" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "release" +version = "0.1.0" +dependencies = [ + "chrono", + "handlebars", + "serde", + "serde_derive", + "serde_json", + "zip", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", +] diff --git a/windows_kext/release/Cargo.toml b/windows_kext/release/Cargo.toml new file mode 100644 index 00000000..c0256880 --- /dev/null +++ b/windows_kext/release/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "release" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +handlebars = "5.1.0" +serde = "1.0.197" +serde_derive = "1.0.197" +serde_json = "1.0.114" +chrono = "0.4.35" +zip = { version = "0.6.6", default-features = false } diff --git a/windows_kext/release/README.md b/windows_kext/release/README.md new file mode 100644 index 00000000..a66c03a9 --- /dev/null +++ b/windows_kext/release/README.md @@ -0,0 +1,27 @@ +# Kext release tool + +### Generate the zip file +- Make sure `kext_interface/version.txt` is up to date +- Execute: `cargo run` + * This will generate release `kext_release_vX-X-X.zip` file. Which contains all the necessary files to make the release. + +### Generate the cab file +- Copy the zip and extract it on a windows machine. + * Some version Visual Studio needs to be installed. +- From VS Command Prompt / PowerShell run: +``` +cd kext_release_v.../ +./build_cab.bat +``` + +3. Sing the cab file + +### Let Microsoft Sign +- Go to https://partner.microsoft.com/en-us/dashboard/hardware/driver/New +- Enter "PortmasterKext vX.X.X #1" as the product name +- Upload `portmaster-kext_vX-X-X.cab` +- Select the Windows 10 versions that you compiled and tested on +- Wait for the process to finish, download the `.zip`. + +The zip will contain the release files. +> Optionally sign the .sys file. \ No newline at end of file diff --git a/windows_kext/release/kext_release_v2-0-0.zip b/windows_kext/release/kext_release_v2-0-0.zip new file mode 100644 index 0000000000000000000000000000000000000000..6285f04ce8cf7eaea389c9f9bbd5d24e01b0becd GIT binary patch literal 6382594 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