summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2025-03-26 16:12:45 +0100
committerEmīls <emils@mullvad.net>2025-03-26 16:12:45 +0100
commitdf5128939e8eaafb6ba0cec7039e8f14f47301f2 (patch)
tree8dc348dba8d3f0686efca689f4f29f052bb038ac
parent771e2c4f15ff0a4689bfc25dce08a9f6fc0f1f57 (diff)
parent76ec2d6b88db62dd013cfb59a721136798e73e89 (diff)
downloadmullvadvpn-df5128939e8eaafb6ba0cec7039e8f14f47301f2.tar.xz
mullvadvpn-df5128939e8eaafb6ba0cec7039e8f14f47301f2.zip
Merge branch 'ios-test-router'
-rw-r--r--Cargo.toml1
-rw-r--r--ci/ios/test-router/README.md49
-rw-r--r--ci/ios/test-router/app-team-ios-lab.nix44
-rw-r--r--ci/ios/test-router/flake.lock27
-rw-r--r--ci/ios/test-router/flake.nix48
-rw-r--r--ci/ios/test-router/nftables.nix129
-rw-r--r--ci/ios/test-router/raas.nix12
-rw-r--r--ci/ios/test-router/raas/Cargo.lock1353
-rw-r--r--ci/ios/test-router/raas/Cargo.toml32
-rw-r--r--ci/ios/test-router/raas/src/block_list/mod.rs94
-rw-r--r--ci/ios/test-router/raas/src/block_list/rule.rs82
-rw-r--r--ci/ios/test-router/raas/src/capture/cleanup.rs58
-rw-r--r--ci/ios/test-router/raas/src/capture/mod.rs166
-rw-r--r--ci/ios/test-router/raas/src/capture/parse.rs241
-rw-r--r--ci/ios/test-router/raas/src/main.rs67
-rw-r--r--ci/ios/test-router/raas/src/web/firewall.rs94
-rw-r--r--ci/ios/test-router/raas/src/web/ip.rs7
-rw-r--r--ci/ios/test-router/raas/src/web/mod.rs37
-rw-r--r--ci/ios/test-router/raas/src/web/routes.rs114
-rw-r--r--ci/ios/test-router/router-config.nix233
20 files changed, 2888 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml
index a52f66781e..92c04574cf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,7 @@ rust-version = "1.83.0"
[workspace]
resolver = "2"
+exclude = [ "ci/ios/test-router/raas" ]
members = [
"android/translations-converter",
"desktop/packages/nseventforwarder",
diff --git a/ci/ios/test-router/README.md b/ci/ios/test-router/README.md
new file mode 100644
index 0000000000..919fa84dee
--- /dev/null
+++ b/ci/ios/test-router/README.md
@@ -0,0 +1,49 @@
+# Router setup
+## Installing on a new router/computer
+- Obtain an x86 computer with 2 ethernet interfaces.
+- Install NixOS on the hardware following the [NixOS installation guide]
+- Copy the generated `/etc/nixos/hardware-config.nix` file to the flake repo, add it to git.
+- Add a new _nixosConfiguration_ entry in `flake.nix`, following `app-team-ios-lab` as an example, making sure to import
+ the hardware config.
+ * Be sure to include the `hardware-config.nix` file as it contains the mount config for the partitions.
+ * Set the appropriate args for the `./router-config.nix` import, as to not clash with existing SSIDs.
+
+- Apply the new configuration either via SSH or by copying the flake over to the nix machine
+ * `nixos-reubild switch .#$newMachine --target-host root@$newMachine-ip` if one can SSH into the machine
+ * `nixos-reubild switch .$pathToFlake#$newMachine` if flake is copied to nix machine, with `$pathToFlake` being the
+ path to this flake directory.
+
+## Livebooting
+One can create an ISO to live-boot a router needing to permanently install this config. There are two drawbacks:
+* Still need to know the MAC addresses of the interfaces upfront.
+* Any updates to the running system will not persist.
+
+To do this, add a `nixosConfiguration` with an extra import of the installer ISO profile like so:
+```nix
+ nixosConfigurations.app-team-ios-lab-iso = nixpkgs.lib.nixosSystem {
+ system = "x86_64-linux";
+ modules = [
+ (import ./router-config.nix {
+ ssid = "app-team-ios-tests";
+ lanMac = "48:21:0b:36:bb:52";
+ wanMac = "48:21:0b:36:43:a3";
+ lanIp = "192.168.105.1/24";
+ })
+ "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix"
+ {
+ isoImage.squashfsCompression = "lz4";
+ }
+ ];
+ };
+```
+
+And build it like so:
+`nix build .#nixosConfigurations.app-team-ios-lab-iso.config.system.build.isoImage`
+
+
+## Quirks & features
+- Since Apple doesn't allow access to LAN without the user accepting a privacy
+ dialog, TCP connections to `8.8.8.8:80` are NAT'ed to the gateway address.
+
+
+[NixOS installation guide]: https://nixos.org/manual/nixos/stable/#sec-installation-graphical
diff --git a/ci/ios/test-router/app-team-ios-lab.nix b/ci/ios/test-router/app-team-ios-lab.nix
new file mode 100644
index 0000000000..3946403480
--- /dev/null
+++ b/ci/ios/test-router/app-team-ios-lab.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+ imports =
+ [ (modulesPath + "/installer/scan/not-detected.nix")
+ ];
+
+ nixpkgs.config.allowUnfree = true;
+ hardware.enableAllFirmware = true;
+ hardware.firmware = [ pkgs.wireless-regdb ];
+ boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sdhci_pci" ];
+ boot.initrd.kernelModules = [ ];
+ boot.kernelModules = [ "kvm-intel" ];
+ boot.extraModulePackages = [ ];
+ boot.extraModprobeConfig = ''
+ options iwlmvm power_scheme=1
+ options iwlwifi disable_11ac=1
+ options iwlwifi disable_11ax= 1
+ '';
+ boot.loader.systemd-boot.enable = true;
+ boot.loader.efi.canTouchEfiVariables = true;
+ boot.kernelPackages = pkgs.linuxPackages_6_6;
+
+ services.fwupd.enable = true;
+
+
+ fileSystems."/" =
+ { device = "/dev/disk/by-uuid/40974b12-1be6-4e2b-b8b2-57123f4d60ce";
+ fsType = "ext4";
+ };
+
+ fileSystems."/boot" =
+ { device = "/dev/disk/by-uuid/0C9E-5CDB";
+ fsType = "vfat";
+ options = [ "fmask=0077" "dmask=0077" ];
+ };
+
+ swapDevices = [ ];
+
+ networking.useDHCP = lib.mkDefault false;
+ nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
+ hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+}
+
diff --git a/ci/ios/test-router/flake.lock b/ci/ios/test-router/flake.lock
new file mode 100644
index 0000000000..064ee7a736
--- /dev/null
+++ b/ci/ios/test-router/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1720535198,
+ "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-23.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/ci/ios/test-router/flake.nix b/ci/ios/test-router/flake.nix
new file mode 100644
index 0000000000..7d7593907c
--- /dev/null
+++ b/ci/ios/test-router/flake.nix
@@ -0,0 +1,48 @@
+{
+ description = "Config for our testing router";
+
+ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; };
+
+ outputs = { self, nixpkgs }: {
+ nixosConfigurations.app-team-ios-lab = nixpkgs.lib.nixosSystem {
+ system = "x86_64-linux";
+ modules = [
+ (import ./router-config.nix {
+ hostname = "app-team-ios-tests";
+ lanMac = "a0:ce:c8:ab:bd:2d";
+ wanMac = "88:ae:dd:64:e1:55";
+ lanIp = "192.168.105.1/24";
+ })
+ ./app-team-ios-lab.nix
+ {
+ boot.loader.systemd-boot.enable = true;
+ boot.loader.efi.canTouchEfiVariables = true;
+ hardware = {
+ cpu.intel.updateMicrocode = true;
+ enableRedistributableFirmware = true;
+ };
+ }
+ ];
+ };
+
+ nixosConfigurations.app-team-ios-lab-iso = nixpkgs.lib.nixosSystem {
+ system = "x86_64-linux";
+ modules = [
+ (import ./router-config.nix {
+ hostname = "app-team-ios-tests";
+ lanMac = "48:21:0b:36:bb:52";
+ wanMac = "48:21:0b:36:43:a3";
+ lanIp = "192.168.105.1/24";
+ })
+ "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix"
+ {
+ isoImage.squashfsCompression = "lz4";
+ }
+ ];
+ };
+
+ packages.x86_64-linux.raas =
+ with import nixpkgs { system = "x86_64-linux"; };
+ pkgs.callPackage ./raas.nix {};
+ };
+}
diff --git a/ci/ios/test-router/nftables.nix b/ci/ios/test-router/nftables.nix
new file mode 100644
index 0000000000..41e78f1e45
--- /dev/null
+++ b/ci/ios/test-router/nftables.nix
@@ -0,0 +1,129 @@
+{ lib, config, ... }:
+with lib; let
+ cfg = config.services.nftables;
+in
+{
+ options.services.nftables.internetHostOverride = mkOption {
+ type = types.str;
+ default = false;
+ description = ''
+ Gateway address to which traffic to 8.8.8.8:80 will be forwarded to.
+ '';
+ };
+
+ options.services.nftables.lanInterfaces = mkOption {
+ type = types.str;
+ default = false;
+ description = ''
+ A string representing the interfaces on the LAN side of the network.
+ '';
+ };
+
+ config.systemd.services.nftables = {
+ before = lib.mkForce [ ];
+ bindsTo = [
+ "sys-subsystem-net-devices-wan.device"
+ "sys-subsystem-net-devices-lan.device"
+ ];
+ after = [
+ "systemd-networkd.service"
+ "sys-subsystem-net-devices-wan.device"
+ "sys-subsystem-net-devices-lan.device"
+ ];
+ };
+
+ config.networking.nftables = {
+ enable = true;
+ preCheckRuleset = ''
+ sed 's/lan/lo/g' -i ruleset.conf
+ sed 's/wan/lo/g' -i ruleset.conf
+ sed 's/wifi/lo/g' -i ruleset.conf
+ '';
+ ruleset = ''
+ table inet filter {
+ chain output {
+ type filter hook output priority 100; policy accept;
+ }
+
+
+ chain input {
+ type filter hook input priority filter; policy drop;
+
+ # allow reaching systemd-resolve
+ ip saddr 127.0.0.1 ip daddr 127.0.0.53 accept
+ iifname lo accept;
+ oifname lo accept;
+ # Allow trusted networks to access the router
+ iifname { lan } counter accept;
+ # Allow WiFi clients reach the following:
+ # - DNS
+ # - DHCP
+ # - DHCPv6
+ iifname { wan, ${cfg.lanInterfaces} } udp dport 53 counter accept
+ iifname { wan, ${cfg.lanInterfaces} } tcp dport 53 counter accept
+ iifname { wifi } udp sport 68 udp dport 67 counter accept
+ iifname { wifi } ip6 saddr fe80::/10 udp sport 546 ip6 daddr fe80::/10 udp dport 547 accept
+
+ iifname wan meta nfproto ipv6 accept
+
+
+ # allow SSH from WAN
+ iifname "wan" tcp dport 2021 counter accept
+ # allow WG from WAN
+ iifname "wan" udp dport 6070 counter accept
+
+
+ # allow random traffic for testing purposes
+ iifname "wan" udp dport {9090, 9091} counter accept
+ iifname "wan" tcp dport {9090, 9091} counter accept
+
+ iifname { "wan", "staging" } ct state vmap { established : accept, related : accept, invalid : drop }
+ iifname "wan" udp sport 67 udp dport 68 counter accept;
+ iifname "wan" ip6 saddr fe80::/10 udp sport 547 ip6 daddr fe80::/10 udp dport 546 counter accept
+
+ icmpv6 code no-route counter accept
+ iifname "wan" icmpv6 mtu > 0 counter accept comment "Allow ALL ICMP from wan"
+ icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } counter accept
+
+ }
+
+ chain forward {
+ type filter hook forward priority filter; policy drop;
+
+ ip daddr 192.168.0.0/23 drop
+
+ # offload established HTTP connections
+ # ip protocol { tcp, udp } ct state established flow offload @internetNat counter
+
+ # Allow traffic from established and related packets, drop invalid
+ ct state vmap { established : accept, related : accept, invalid : drop }
+
+ # Allow trusted network WAN access
+ iifname {
+ lo, ${cfg.lanInterfaces}
+ } oifname {
+ "wan", "staging"
+ } counter accept comment "Allow trusted LAN to WAN and staging interface"
+
+ iifname "lan" oifname "wifi" counter accept comment "Allow LAN to IoS WiFi"
+
+ # Allow established WAN to return
+ iifname { "wan", "wifi" } oifname { ${cfg.lanInterfaces} } ct state established,related counter accept comment "Allow established back to LANs"
+ iifname {"wan", "staging" } oifname { "lan", "wifi"} ct mark 1919 accept comment "Allow DNAtted traffic"
+ }
+
+ chain srcnat {
+ type nat hook postrouting priority srcnat; policy accept;
+ iifname { ${cfg.lanInterfaces} } masquerade comment "Masquerade all traffic"
+ }
+
+ chain dstnat {
+ type nat hook prerouting priority dstnat; policy accept;
+ ip daddr 8.8.8.8 tcp dport 80 dnat to ${cfg.internetHostOverride};
+ # host of the bridge IP address
+ ip daddr 85.203.53.200 tcp dport 443 dnat to ${cfg.internetHostOverride};
+ }
+ }
+ '';
+ };
+}
diff --git a/ci/ios/test-router/raas.nix b/ci/ios/test-router/raas.nix
new file mode 100644
index 0000000000..8af141332a
--- /dev/null
+++ b/ci/ios/test-router/raas.nix
@@ -0,0 +1,12 @@
+{ pkgs, rustPlatform, pkg-config, libmnl, libnftnl, libpcap, ... }:
+
+rustPlatform.buildRustPackage rec {
+ pname = "raas";
+ version = "0.0.1";
+
+ src = ./raas;
+ cargoLock.lockFile = ./raas/Cargo.lock;
+
+ nativeBuildInputs = [ pkg-config ];
+ buildInputs = [ libmnl libnftnl libpcap ];
+}
diff --git a/ci/ios/test-router/raas/Cargo.lock b/ci/ios/test-router/raas/Cargo.lock
new file mode 100644
index 0000000000..ec9f29813b
--- /dev/null
+++ b/ci/ios/test-router/raas/Cargo.lock
@@ -0,0 +1,1353 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower 0.5.2",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder_slice"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b294e30387378958e8bf8f4242131b930ea615ff81e8cac2440cea0a6013190"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "derive-into-owned"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d94d81e3819a7b06a8638f448bc6339371ca9b6076a99d4a43eece3c4c923"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+
+[[package]]
+name = "libloading"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "mnl"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1a5469630da93e1813bb257964c0ccee3b26b6879dd858039ddec35cc8681ed"
+dependencies = [
+ "libc",
+ "log",
+ "mnl-sys",
+]
+
+[[package]]
+name = "mnl-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9750685b201e1ecfaaf7aa5d0387829170fa565989cc481b49080aa155f70457"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "nftnl"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06a7491dd91b71643f65546389f25506da70723d1f1ec8c8d6d20444d1c23f27"
+dependencies = [
+ "bitflags 2.9.0",
+ "log",
+ "nftnl-sys",
+]
+
+[[package]]
+name = "nftnl-sys"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b193f2c2a70e6421534c3f3b75eaaed4e4b9df45281b3d94f5bc8c32fb346cbb"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "no-std-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "pcap"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99e935fc73d54a89fff576526c2ccd42bbf8247aae05b358693475b14fd4ff79"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "futures",
+ "libc",
+ "libloading",
+ "pkg-config",
+ "regex",
+ "tokio",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "pcap-file"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc1f139757b058f9f37b76c48501799d12c9aa0aa4c0d4c980b062ee925d1b2"
+dependencies = [
+ "byteorder_slice",
+ "derive-into-owned",
+ "thiserror",
+]
+
+[[package]]
+name = "pcap-file-tokio"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee4f08e375f9aabbb17f4c031a2f0af1397835ce8d7909b167ada1dd8b572e6"
+dependencies = [
+ "async-trait",
+ "byteorder",
+ "derive-into-owned",
+ "pcap-file",
+ "thiserror",
+ "tokio",
+ "tokio-byteorder",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "pnet_base"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c"
+dependencies = [
+ "no-std-net",
+]
+
+[[package]]
+name = "pnet_macros"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "pnet_macros_support"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56"
+dependencies = [
+ "pnet_base",
+]
+
+[[package]]
+name = "pnet_packet"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba"
+dependencies = [
+ "glob",
+ "pnet_base",
+ "pnet_macros",
+ "pnet_macros_support",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "raas"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "env_logger",
+ "futures",
+ "log",
+ "mnl",
+ "nftnl",
+ "once_cell",
+ "pcap",
+ "pcap-file-tokio",
+ "pnet_packet",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tokio-util",
+ "tower 0.4.13",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+ "uuid",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
+dependencies = [
+ "bitflags 2.9.0",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+
+[[package]]
+name = "socket2"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[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.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tokio"
+version = "1.44.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-byteorder"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cf347e8ae1d1ffd16c8aed569172a71bd81098a001d0f4964d476c0097aba4a"
+dependencies = [
+ "byteorder",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
+dependencies = [
+ "bitflags 2.9.0",
+ "bytes",
+ "http",
+ "http-body",
+ "http-body-util",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "uuid"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/ci/ios/test-router/raas/Cargo.toml b/ci/ios/test-router/raas/Cargo.toml
new file mode 100644
index 0000000000..5bc0eacf6e
--- /dev/null
+++ b/ci/ios/test-router/raas/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "raas"
+version = "0.1.0"
+edition = "2021"
+description = "RaaS stands for Router as a Service"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+nftnl = { version = "0.7", features = ["nftnl-1-1-0"] }
+mnl = { version = "0.2", features = ["mnl-1-0-4"] }
+once_cell = "1"
+log = "0.4"
+env_logger = "0.10"
+
+axum = "0.7"
+pnet_packet = "0.34"
+pcap = { version = "1.2.0", features = ["capture-stream"] }
+pcap-file-tokio = "0.1"
+thiserror = "1"
+tower = "0.4"
+tower-http = { version = "0.5", features = [ "trace" ] }
+tokio = { version = "1.0", features = ["full"] }
+tokio-util = { version = "0.7", features = ["io"] }
+futures = "0.3"
+tracing = "0.1.35"
+tracing-subscriber = "0.3.14"
+serde = { version = "1.0.138", features = ["derive"] }
+serde_json = "1.0"
+uuid = { version = "1", features = [ "serde" ] }
+anyhow = "1"
+
diff --git a/ci/ios/test-router/raas/src/block_list/mod.rs b/ci/ios/test-router/raas/src/block_list/mod.rs
new file mode 100644
index 0000000000..8aab004126
--- /dev/null
+++ b/ci/ios/test-router/raas/src/block_list/mod.rs
@@ -0,0 +1,94 @@
+use nftnl::{Batch, Chain, FinalizedBatch, ProtoFamily, Rule, Table};
+use once_cell::sync::Lazy;
+use std::{collections::BTreeMap, ffi::CString, io};
+
+static TABLE_NAME: Lazy<CString> = Lazy::new(|| CString::new("raas").unwrap());
+static FORWARD_CHAIN_NAME: Lazy<CString> = Lazy::new(|| CString::new("forward").unwrap());
+
+mod rule;
+pub use rule::BlockRule;
+
+#[derive(Default)]
+pub struct BlockList {
+ rules: BTreeMap<uuid::Uuid, Vec<BlockRule>>,
+}
+
+impl BlockList {
+ pub fn add_rule(&mut self, rule: BlockRule, label: uuid::Uuid) -> io::Result<()> {
+ {
+ let rules = self.rules.entry(label).or_default();
+ rules.push(rule);
+ }
+ self.apply_rules()
+ }
+
+ pub fn clear_rules_with_label(&mut self, label: &uuid::Uuid) -> io::Result<()> {
+ let _ = self.rules.remove(label);
+ self.apply_rules()
+ }
+
+ pub fn rules(&self) -> &BTreeMap<uuid::Uuid, Vec<BlockRule>> {
+ &self.rules
+ }
+
+ fn apply_rules(&mut self) -> io::Result<()> {
+ let table = Table::new(&*TABLE_NAME, ProtoFamily::Inet);
+ let batch = self.create_batch(&table);
+ self.send_netlink_batch(&batch)
+ }
+
+ fn create_batch(&mut self, table: &Table) -> FinalizedBatch {
+ let mut batch = Batch::new();
+
+ // Create the table if it does not exist and clear it otherwise.
+ batch.add(table, nftnl::MsgType::Add);
+ batch.add(table, nftnl::MsgType::Del);
+ batch.add(table, nftnl::MsgType::Add);
+
+ let mut forward_chain = Chain::new(&*FORWARD_CHAIN_NAME, table);
+ forward_chain.set_hook(nftnl::Hook::Forward, 0);
+ forward_chain.set_policy(nftnl::Policy::Accept);
+ batch.add(&forward_chain, nftnl::MsgType::Add);
+ for rule in self.nft_forward_rules(&forward_chain) {
+ batch.add(&rule, nftnl::MsgType::Add);
+ }
+
+ batch.finalize()
+ }
+
+ fn send_netlink_batch(&self, batch: &FinalizedBatch) -> io::Result<()> {
+ let socket = mnl::Socket::new(mnl::Bus::Netfilter)?;
+ socket.send_all(batch)?;
+
+ let portid = socket.portid();
+ let mut buffer = vec![0; nftnl::nft_nlmsg_maxsize() as usize];
+
+ let seq = 0;
+ while let Some(message) = Self::socket_recv(&socket, &mut buffer[..])? {
+ match mnl::cb_run(message, seq, portid)? {
+ mnl::CbResult::Stop => {
+ break;
+ }
+ mnl::CbResult::Ok => (),
+ };
+ }
+
+ Ok(())
+ }
+
+ fn socket_recv<'a>(socket: &mnl::Socket, buf: &'a mut [u8]) -> io::Result<Option<&'a [u8]>> {
+ let ret = socket.recv(buf)?;
+ if ret > 0 {
+ Ok(Some(&buf[..ret]))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn nft_forward_rules<'a>(&'a self, chain: &'a Chain<'a>) -> impl Iterator<Item = Rule<'a>> {
+ self.rules
+ .values()
+ .flatten()
+ .flat_map(move |rule| rule.create_nft_rule(chain))
+ }
+}
diff --git a/ci/ios/test-router/raas/src/block_list/rule.rs b/ci/ios/test-router/raas/src/block_list/rule.rs
new file mode 100644
index 0000000000..e6ce90f20c
--- /dev/null
+++ b/ci/ios/test-router/raas/src/block_list/rule.rs
@@ -0,0 +1,82 @@
+use crate::web::routes::TransportProtocol;
+use mnl::mnl_sys::libc;
+use nftnl::{expr, nft_expr, Chain, Rule};
+
+use std::{collections::BTreeSet, iter, net::IpAddr};
+
+#[derive(Clone, serde::Serialize)]
+pub struct BlockRule {
+ pub src: IpAddr,
+ pub dst: IpAddr,
+ pub protocols: BTreeSet<TransportProtocol>,
+}
+
+impl BlockRule {
+ pub fn create_nft_rule<'a>(
+ &'a self,
+ chain: &'a Chain<'a>,
+ ) -> Box<dyn Iterator<Item = Rule<'a>> + 'a> {
+ if self.protocols.is_empty() {
+ return Box::new(iter::once(self.create_nft_rule_inner(chain, None)));
+ }
+
+ let iter = self
+ .protocols
+ .iter()
+ .map(|protocol| self.create_nft_rule_inner(chain, Some(*protocol)));
+ Box::new(iter)
+ }
+
+ fn create_nft_rule_inner<'a>(
+ &self,
+ chain: &'a Chain<'a>,
+ transport_protocol: Option<TransportProtocol>,
+ ) -> Rule<'a> {
+ let mut rule = Rule::new(chain);
+ check_l3proto(&mut rule, self.src);
+ if let Some(protocol) = transport_protocol {
+ check_l4proto(&mut rule, protocol);
+ };
+
+ // Add source checking
+ rule.add_expr(match self.src {
+ IpAddr::V4(_) => &nft_expr!(payload ipv4 saddr),
+ IpAddr::V6(_) => &nft_expr!(payload ipv6 saddr),
+ });
+ match self.src {
+ IpAddr::V4(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ IpAddr::V6(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ };
+
+ // Add destination check
+ rule.add_expr(match self.dst {
+ IpAddr::V4(_) => &nft_expr!(payload ipv4 daddr),
+ IpAddr::V6(_) => &nft_expr!(payload ipv6 daddr),
+ });
+ match self.dst {
+ IpAddr::V4(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ IpAddr::V6(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ };
+ rule.add_expr(&nft_expr!(counter));
+ rule.add_expr(&expr::Verdict::Drop);
+
+ rule
+ }
+}
+
+fn check_l3proto(rule: &mut Rule<'_>, ip: IpAddr) {
+ rule.add_expr(&nft_expr!(meta nfproto));
+ rule.add_expr(&nft_expr!(cmp == l3proto(ip)));
+}
+
+fn l3proto(addr: IpAddr) -> u8 {
+ match addr {
+ IpAddr::V4(_) => libc::NFPROTO_IPV4 as u8,
+ IpAddr::V6(_) => libc::NFPROTO_IPV6 as u8,
+ }
+}
+
+fn check_l4proto(rule: &mut Rule<'_>, protocol: TransportProtocol) {
+ rule.add_expr(&nft_expr!(meta l4proto));
+ rule.add_expr(&nft_expr!(cmp == protocol.as_ipproto()));
+}
diff --git a/ci/ios/test-router/raas/src/capture/cleanup.rs b/ci/ios/test-router/raas/src/capture/cleanup.rs
new file mode 100644
index 0000000000..13189da464
--- /dev/null
+++ b/ci/ios/test-router/raas/src/capture/cleanup.rs
@@ -0,0 +1,58 @@
+use std::path::Path;
+use tokio::{fs, time::Duration};
+use uuid::Uuid;
+
+const ONE_DAY_AGO: Duration = Duration::from_secs(24 * 60 * 60); // 86400 seconds in a day
+
+pub async fn delete_old_captures() -> std::io::Result<()> {
+ delete_old_captures_inner(&super::Capture::capture_dir_path()).await
+}
+
+async fn delete_old_captures_inner(dir: &Path) -> std::io::Result<()> {
+ let mut entries = fs::read_dir(dir).await?;
+
+ // Collecting tasks to join later
+ let mut delete_tasks = vec![];
+
+ // Iterate over directory entries
+ while let Some(entry) = entries.next_entry().await? {
+ let path = entry.path();
+
+ if path.is_file() && should_delete_capture_file(&path).await {
+ // Spawn a task to delete the file
+ let path = path.clone();
+ delete_tasks.push(tokio::spawn(async move {
+ if let Err(e) = fs::remove_file(&path).await {
+ eprintln!("Failed to delete {:?}: {}", path, e);
+ }
+ }));
+ }
+ }
+
+ // Wait for all delete tasks to complete
+ for task in delete_tasks {
+ let _ = task.await;
+ }
+
+ Ok(())
+}
+
+//
+async fn should_delete_capture_file(path: &Path) -> bool {
+ // Check if the file name is a valid UUID
+ if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) {
+ if Uuid::parse_str(file_name).is_ok() {
+ // Check the file's metadata
+ let Some(metadata) = fs::metadata(&path).await.ok() else {
+ return false;
+ };
+ if let Ok(modified_time) = metadata.modified() {
+ // Calculate the elapsed time since the file was modified
+ if let Ok(duration) = modified_time.elapsed() {
+ return duration > ONE_DAY_AGO;
+ }
+ }
+ }
+ }
+ false
+}
diff --git a/ci/ios/test-router/raas/src/capture/mod.rs b/ci/ios/test-router/raas/src/capture/mod.rs
new file mode 100644
index 0000000000..992da8395a
--- /dev/null
+++ b/ci/ios/test-router/raas/src/capture/mod.rs
@@ -0,0 +1,166 @@
+use futures::StreamExt;
+use pcap::{Device, Packet, PacketCodec, PacketHeader};
+use std::{collections::BTreeMap, io, path::PathBuf, sync::mpsc as sync_mpsc};
+use tokio::{fs::File, io::BufReader, sync::oneshot};
+
+mod parse;
+pub use parse::parse_pcap;
+mod cleanup;
+pub use cleanup::delete_old_captures;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("The capture is in progress")]
+ CaptureInProgress,
+ #[error("Failed to capture handle for device")]
+ OpenHandle(#[source] pcap::Error),
+ #[error("Failed to make capture nonblocking")]
+ EnableNonblock(#[source] pcap::Error),
+ #[error("Failed to begin capture")]
+ BeginCapture(#[source] pcap::Error),
+ #[error("Failed to create pcap file")]
+ CreateDump(#[source] pcap::Error),
+ #[error("Failed to create packet stream")]
+ CreateStream(#[source] pcap::Error),
+ #[error("Packet stream returned an error")]
+ StreamFailed(#[source] pcap::Error),
+ #[error("Could not find the specified label")]
+ CaptureNotFound,
+ #[error("Failed to open pcap file")]
+ ReadPcap(#[source] io::Error),
+}
+
+// Maximum capture size should be 100mb
+const MAX_CAPTURE_SIZE: u32 = 1024 * 1024 * 100;
+
+#[derive(Default)]
+pub struct Capture {
+ captures: BTreeMap<uuid::Uuid, Context>,
+}
+
+struct Context {
+ capture: tokio::task::JoinHandle<Result<(), Error>>,
+ stop_tx: oneshot::Sender<()>,
+}
+
+pub struct CloneCodec;
+
+impl PacketCodec for CloneCodec {
+ type Item = (PacketHeader, Box<[u8]>);
+
+ fn decode(&mut self, packet: Packet) -> Self::Item {
+ (packet.header.to_owned(), packet.data.into())
+ }
+}
+
+const RAAS_TMP_DIR: &'static str = "raas";
+
+impl Capture {
+ fn capture_file_path(label: &uuid::Uuid) -> PathBuf {
+ Self::capture_dir_path().join(label.to_string())
+ }
+
+ fn capture_dir_path() -> PathBuf {
+ std::env::temp_dir().join(RAAS_TMP_DIR)
+ }
+
+ pub async fn start(&mut self, label: uuid::Uuid) -> Result<(), Error> {
+ if self.captures.contains_key(&label) {
+ return Err(Error::CaptureInProgress);
+ }
+
+ // Use the magic any device.
+ // This will remove the ethernet frames from the packets.
+ let device = Device {
+ name: "any".into(),
+ desc: None,
+ addresses: vec![],
+ flags: pcap::DeviceFlags::empty(),
+ };
+
+ let capture = pcap::Capture::from_device(device)
+ .map_err(Error::OpenHandle)?
+ .immediate_mode(true)
+ .open()
+ .map_err(Error::BeginCapture)?
+ .setnonblock()
+ .map_err(Error::EnableNonblock)?;
+
+ let dump_path = Self::capture_file_path(&label);
+ let mut dump = capture.savefile(dump_path).map_err(Error::CreateDump)?;
+
+ let (stop_tx, mut stop_rx) = oneshot::channel();
+
+ let mut stream = capture.stream(CloneCodec).map_err(Error::CreateStream)?;
+
+ let capture = tokio::spawn(async move {
+ let (pcap_tx, pcap_rx): (_, sync_mpsc::Receiver<(PacketHeader, Box<[u8]>)>) =
+ sync_mpsc::channel();
+ tokio::task::spawn_blocking(move || {
+ while let Ok((header, data)) = pcap_rx.recv() {
+ let packet = Packet {
+ header: &header,
+ data: &data,
+ };
+ dump.write(&packet);
+ }
+ if let Err(error) = dump.flush() {
+ log::error!("Failed to flush pcap dump: {error}");
+ }
+ });
+
+ let mut capture_size = 0;
+ loop {
+ tokio::select! {
+ _ = &mut stop_rx => {
+ break;
+ }
+ packet = stream.next() => {
+ let Some(result) = packet else {
+ break;
+ };
+ let (header, data) = result.map_err(Error::StreamFailed)?;
+ let _ = pcap_tx.send((header, data));
+ capture_size += header.caplen;
+
+ if capture_size >= MAX_CAPTURE_SIZE {
+ break;
+ }
+ }
+ }
+ }
+
+ Ok(())
+ });
+
+ let context = Context { capture, stop_tx };
+
+ self.captures.insert(label, context);
+
+ Ok(())
+ }
+
+ pub async fn stop(&mut self, label: uuid::Uuid) -> Result<(), Error> {
+ if let Some(context) = self.captures.remove(&label) {
+ let _ = context.stop_tx.send(());
+ if let Ok(result) = context.capture.await {
+ result?;
+ }
+ }
+ Ok(())
+ }
+
+ pub async fn get(&self, label: uuid::Uuid) -> Result<BufReader<File>, Error> {
+ if self.captures.contains_key(&label) {
+ return Err(Error::CaptureInProgress);
+ }
+
+ let dump_path = Self::capture_file_path(&label);
+ if !dump_path.exists() {
+ return Err(Error::CaptureNotFound);
+ }
+
+ let file = File::open(dump_path).await.map_err(Error::ReadPcap)?;
+ Ok(BufReader::new(file))
+ }
+}
diff --git a/ci/ios/test-router/raas/src/capture/parse.rs b/ci/ios/test-router/raas/src/capture/parse.rs
new file mode 100644
index 0000000000..8da51d1f59
--- /dev/null
+++ b/ci/ios/test-router/raas/src/capture/parse.rs
@@ -0,0 +1,241 @@
+// use packet::ip::{v4, v6, Packet};
+use pcap_file_tokio::{
+ pcap::{PcapPacket, PcapReader},
+ PcapError,
+};
+use pnet_packet::{
+ ethernet::EthernetPacket,
+ ip::{IpNextHeaderProtocol, IpNextHeaderProtocols},
+ ipv4::Ipv4Packet,
+ ipv6::Ipv6Packet,
+ tcp::TcpPacket,
+ udp::UdpPacket,
+ Packet,
+};
+use std::{
+ collections::{BTreeMap, BTreeSet},
+ net::{IpAddr, SocketAddr},
+};
+
+pub async fn parse_pcap<F: tokio::io::AsyncRead + Unpin>(
+ file: F,
+ peer_addrs: BTreeSet<IpAddr>,
+) -> Result<Vec<Connection>, PcapError> {
+ let mut reader = PcapReader::new(file).await?;
+ let mut connections = ParsedConnections::new(peer_addrs);
+
+ while let Some(block) = reader.next_packet().await {
+ match block {
+ Ok(block) => {
+ connections.parse_pcap_packet(&block);
+ }
+ Err(err) => {
+ log::error!("Failed to parse a packet: {err}");
+ continue;
+ }
+ }
+ }
+
+ Ok(connections.to_vec())
+}
+
+#[derive(serde::Serialize, Clone, Debug)]
+pub struct Connection {
+ #[serde(flatten)]
+ pub id: ConnectionId,
+ pub packets: Vec<PacketTransmission>,
+}
+
+#[derive(serde::Serialize, PartialOrd, Hash, PartialEq, Clone, Copy, Ord, Eq, Debug)]
+pub struct ConnectionId {
+ pub peer_addr: SocketAddr,
+ pub other_addr: SocketAddr,
+ pub flow_id: Option<u32>,
+ pub transport_protocol: TransportProtocol,
+}
+
+#[derive(serde::Serialize, Clone, Copy, Debug)]
+pub struct PacketTransmission {
+ from_peer: bool,
+ timestamp: u64,
+}
+
+#[derive(Default, Debug)]
+struct ParsedConnections {
+ /// Peer addresses, only packets associated with these addreseses will be accounted for.
+ /// TODO: reconsider the name peer in this context
+ peer_addrs: BTreeSet<IpAddr>,
+ /// The connections are mapped to a tuple of the peer address, associated address and the
+ /// transport protocol. The behavior is undefined for peer to peer connections, it is assumed
+ /// peers will never need to send traffic amongst themselves.
+ connections: BTreeMap<ConnectionId, Connection>,
+}
+
+impl ParsedConnections {
+ fn new(peer_addrs: BTreeSet<IpAddr>) -> Self {
+ Self {
+ peer_addrs,
+ connections: Default::default(),
+ }
+ }
+
+ fn parse_pcap_packet(&mut self, packet: &PcapPacket<'_>) {
+ let timestamp = packet.timestamp.as_micros() as u64;
+ if packet.data.len() < 3 {
+ return;
+ }
+ // Parse the ethernet packet and truncate the pcap header.
+ let Some(eth_packet) = EthernetPacket::new(&packet.data[2..]) else {
+ return;
+ };
+ if let Some(ipv4_packet) = Ipv4Packet::new(eth_packet.payload()) {
+ self.parse_ip_packet(&ipv4_packet, timestamp);
+ return;
+ }
+
+ if let Some(ipv6_packet) = Ipv6Packet::new(eth_packet.payload()) {
+ self.parse_ip_packet(&ipv6_packet, timestamp);
+ }
+ }
+
+ fn parse_ip_packet(&mut self, packet: &dyn IpPacket, timestamp: u64) {
+ // if packet is not associated with any of our peers, we do not care about it
+ let source = packet.source();
+ let destination = packet.destination();
+
+ if !self.ip_matches_peer(source) && !self.ip_matches_peer(destination) {
+ return;
+ }
+
+ let transport_protocol = packet.transport_protocol();
+ let Some((source_port, destination_port)) =
+ packet_ports(packet.payload(), transport_protocol)
+ else {
+ log::debug!("Failed to parse an IP packet from {source} to {destination}");
+ return;
+ };
+
+ let (peer_addr, other_addr) = if self.ip_matches_peer(source) {
+ (
+ SocketAddr::new(source, source_port),
+ SocketAddr::new(destination, destination_port),
+ )
+ } else {
+ (
+ SocketAddr::new(destination, destination_port),
+ SocketAddr::new(source, source_port),
+ )
+ };
+
+ let connection_id = ConnectionId {
+ peer_addr,
+ other_addr,
+ flow_id: packet.flow_id(),
+ transport_protocol,
+ };
+
+ let packet_transmission = PacketTransmission {
+ from_peer: self.ip_matches_peer(source),
+ timestamp,
+ };
+
+ self.connections
+ .entry(connection_id)
+ .and_modify(|c| {
+ c.packets.push(packet_transmission);
+ })
+ .or_insert_with(|| Connection {
+ id: connection_id,
+ packets: vec![packet_transmission],
+ });
+ }
+
+ fn ip_matches_peer(&self, ip: impl Into<IpAddr>) -> bool {
+ let ip = ip.into();
+ self.peer_addrs.iter().any(|peer| *peer == ip)
+ }
+
+ fn to_vec(&self) -> Vec<Connection> {
+ self.connections.values().cloned().collect()
+ }
+}
+
+/// Represents a layer 4 protocol
+#[derive(serde::Serialize, PartialOrd, PartialEq, Hash, Clone, Copy, Eq, Ord, Debug)]
+#[serde(rename_all = "lowercase")]
+pub enum TransportProtocol {
+ Tcp,
+ Udp,
+ Icmp,
+ Icmp6,
+ Unkown,
+}
+
+impl From<IpNextHeaderProtocol> for TransportProtocol {
+ fn from(value: IpNextHeaderProtocol) -> Self {
+ match value {
+ IpNextHeaderProtocols::Udp => Self::Udp,
+ IpNextHeaderProtocols::Tcp => Self::Tcp,
+ IpNextHeaderProtocols::Icmp => Self::Icmp,
+ IpNextHeaderProtocols::Icmpv6 => Self::Icmp6,
+ _ => Self::Unkown,
+ }
+ }
+}
+
+trait IpPacket: pnet_packet::Packet {
+ fn source(&self) -> IpAddr;
+ fn destination(&self) -> IpAddr;
+ fn transport_protocol(&self) -> TransportProtocol;
+ fn flow_id(&self) -> Option<u32> {
+ None
+ }
+}
+
+impl<'a> IpPacket for Ipv4Packet<'a> {
+ fn source(&self) -> IpAddr {
+ self.get_source().into()
+ }
+
+ fn destination(&self) -> IpAddr {
+ self.get_destination().into()
+ }
+
+ fn transport_protocol(&self) -> TransportProtocol {
+ self.get_next_level_protocol().into()
+ }
+}
+
+impl<'a> IpPacket for Ipv6Packet<'a> {
+ fn source(&self) -> IpAddr {
+ self.get_source().into()
+ }
+
+ fn destination(&self) -> IpAddr {
+ self.get_destination().into()
+ }
+
+ fn transport_protocol(&self) -> TransportProtocol {
+ self.get_next_header().into()
+ }
+
+ fn flow_id(&self) -> Option<u32> {
+ Some(self.get_flow_label())
+ }
+}
+
+/// Returns a tuple representing the source and destination ports for a given packet if the
+/// transport protocol has ports.
+fn packet_ports(payload: &[u8], transport_protocol: TransportProtocol) -> Option<(u16, u16)> {
+ match transport_protocol {
+ TransportProtocol::Tcp => {
+ let packet = TcpPacket::new(payload)?;
+ Some((packet.get_source(), packet.get_destination()))
+ }
+ TransportProtocol::Udp => {
+ let packet = UdpPacket::new(payload)?;
+ Some((packet.get_source(), packet.get_destination()))
+ }
+ _ => Some((0, 0)),
+ }
+}
diff --git a/ci/ios/test-router/raas/src/main.rs b/ci/ios/test-router/raas/src/main.rs
new file mode 100644
index 0000000000..11efac5187
--- /dev/null
+++ b/ci/ios/test-router/raas/src/main.rs
@@ -0,0 +1,67 @@
+use std::{fs, io, net::SocketAddr, path::Path, time::Duration};
+
+mod block_list;
+mod capture;
+mod web;
+
+#[tokio::main]
+async fn main() {
+ init_logging();
+ create_temp_dir();
+
+ let mut args = std::env::args().skip(1);
+ let bind_address = args.next().expect("First arg must be listening address");
+
+ let router =
+ web::router(Default::default()).into_make_service_with_connect_info::<SocketAddr>();
+ let listener = tokio::net::TcpListener::bind(bind_address)
+ .await
+ .expect("Failed to bind to listening socket");
+ log::info!(
+ "listening on {}",
+ listener
+ .local_addr()
+ .expect("Failed to get local address of TCP socket")
+ );
+
+ tokio::spawn(async {
+ loop {
+ tokio::time::sleep(Duration::from_secs(60 * 60 * 24)).await;
+
+ if let Err(err) = capture::delete_old_captures().await {
+ log::error!("Failed to delete old captures: {err}");
+ }
+ }
+ });
+
+ axum::serve(listener, router).await.unwrap();
+}
+
+fn init_logging() {
+ let mut builder = env_logger::Builder::from_env(env_logger::DEFAULT_FILTER_ENV);
+ builder
+ .filter(None, log::LevelFilter::Info)
+ .write_style(env_logger::WriteStyle::Always)
+ .format_timestamp(None)
+ .init();
+}
+
+fn create_temp_dir() {
+ let tmp_dir = std::env::temp_dir().join("raas");
+ create_dir_if_not_exist(tmp_dir).expect("Failed to create tmp directory");
+}
+
+fn create_dir_if_not_exist<P: AsRef<Path>>(path: P) -> io::Result<()> {
+ let path = path.as_ref();
+
+ if path.exists() {
+ return Ok(());
+ }
+
+ if let Some(parent) = path.parent() {
+ create_dir_if_not_exist(parent)?;
+ }
+
+ fs::create_dir(path)?;
+ Ok(())
+}
diff --git a/ci/ios/test-router/raas/src/web/firewall.rs b/ci/ios/test-router/raas/src/web/firewall.rs
new file mode 100644
index 0000000000..0bba64a859
--- /dev/null
+++ b/ci/ios/test-router/raas/src/web/firewall.rs
@@ -0,0 +1,94 @@
+use std::{collections::BTreeSet, net::IpAddr};
+
+use axum::{
+ body::Body,
+ extract::{Json, Path, State},
+ http::{header, StatusCode},
+ response::IntoResponse,
+};
+use uuid::Uuid;
+
+#[derive(serde::Deserialize, Clone)]
+pub struct NewCapture {
+ pub label: Uuid,
+}
+
+pub async fn start(
+ State(state): State<super::State>,
+ Json(capture): Json<NewCapture>,
+) -> impl IntoResponse {
+ let label = capture.label;
+
+ let result = async {
+ let mut state = state.capture.lock().await;
+ state.start(label).await?;
+ log::info!("Started capture for label {label}");
+ Ok(())
+ }
+ .await;
+
+ respond_with_result(result, StatusCode::OK)
+}
+
+pub async fn stop(Path(label): Path<Uuid>, State(state): State<super::State>) -> impl IntoResponse {
+ let result = async {
+ let mut state = state.capture.lock().await;
+ state.stop(label).await?;
+ log::info!("Stopped capture for label {label}");
+ Ok(())
+ }
+ .await;
+
+ respond_with_result(result, StatusCode::OK)
+}
+
+pub async fn get(Path(label): Path<Uuid>, State(state): State<super::State>) -> impl IntoResponse {
+ let state = state.capture.lock().await;
+
+ let stream = match state.get(label).await {
+ Ok(stream) => stream,
+ Err(err) => {
+ return (StatusCode::SERVICE_UNAVAILABLE, format!("{err}\n")).into_response();
+ }
+ };
+
+ let body = Body::from_stream(tokio_util::io::ReaderStream::new(stream));
+ let mut headers = header::HeaderMap::new();
+ headers.insert(
+ header::CONTENT_TYPE,
+ header::HeaderValue::from_static("application/pcap"),
+ );
+ headers.insert(
+ header::CONTENT_DISPOSITION,
+ header::HeaderValue::from_static("attachment; filename=\"dump.pcap\""),
+ );
+
+ (headers, body).into_response()
+}
+
+pub async fn parse(
+ Path(label): Path<Uuid>,
+ State(state): State<super::State>,
+ Json(host_addrs): Json<BTreeSet<IpAddr>>,
+) -> impl IntoResponse {
+ let state = state.capture.lock().await;
+
+ let stream = match state.get(label).await {
+ Ok(stream) => stream,
+ Err(err) => {
+ return (StatusCode::SERVICE_UNAVAILABLE, format!("{err}\n")).into_response();
+ }
+ };
+
+ match crate::capture::parse_pcap(stream, host_addrs).await {
+ Ok(parsed_connections) => (StatusCode::OK, Json(parsed_connections)).into_response(),
+ Err(err) => (StatusCode::SERVICE_UNAVAILABLE, format!("{err}\n")).into_response(),
+ }
+}
+
+fn respond_with_result(result: anyhow::Result<()>, success_code: StatusCode) -> impl IntoResponse {
+ match result {
+ Ok(_) => (success_code, String::new()),
+ Err(err) => (StatusCode::SERVICE_UNAVAILABLE, format!("{err}\n")),
+ }
+}
diff --git a/ci/ios/test-router/raas/src/web/ip.rs b/ci/ios/test-router/raas/src/web/ip.rs
new file mode 100644
index 0000000000..eefe08fb0d
--- /dev/null
+++ b/ci/ios/test-router/raas/src/web/ip.rs
@@ -0,0 +1,7 @@
+use axum::{extract::ConnectInfo, response::IntoResponse};
+use std::net::SocketAddr;
+
+/// Returns IP address of caller as a string
+pub async fn host_ip(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> impl IntoResponse {
+ addr.ip().to_string()
+}
diff --git a/ci/ios/test-router/raas/src/web/mod.rs b/ci/ios/test-router/raas/src/web/mod.rs
new file mode 100644
index 0000000000..7c48ef7b74
--- /dev/null
+++ b/ci/ios/test-router/raas/src/web/mod.rs
@@ -0,0 +1,37 @@
+use std::sync::{Arc, Mutex};
+
+use axum::{
+ routing::{delete, get, post, put},
+ Router,
+};
+use tower::ServiceBuilder;
+use tower_http::trace::TraceLayer;
+
+use crate::{block_list::BlockList, capture::Capture};
+
+mod firewall;
+mod ip;
+pub mod routes;
+
+pub fn router(block_list: BlockList) -> Router {
+ Router::new()
+ .route("/own-ip", get(ip::host_ip))
+ .route("/rules", get(routes::list_all_rules))
+ .route("/rule", post(routes::add_rule))
+ .route("/remove-rules/:label", delete(routes::delete_rules))
+ .route("/capture", post(firewall::start))
+ .route("/stop-capture/:label", post(firewall::stop))
+ .route("/last-capture/:label", get(firewall::get))
+ .route("/parse-capture/:label", put(firewall::parse))
+ .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
+ .with_state(State {
+ block_list: Arc::new(Mutex::new(block_list)),
+ capture: Arc::new(tokio::sync::Mutex::new(Capture::default())),
+ })
+}
+
+#[derive(Clone)]
+pub struct State {
+ block_list: Arc<Mutex<BlockList>>,
+ capture: Arc<tokio::sync::Mutex<Capture>>,
+}
diff --git a/ci/ios/test-router/raas/src/web/routes.rs b/ci/ios/test-router/raas/src/web/routes.rs
new file mode 100644
index 0000000000..a2fae72739
--- /dev/null
+++ b/ci/ios/test-router/raas/src/web/routes.rs
@@ -0,0 +1,114 @@
+use std::{
+ collections::{BTreeMap, BTreeSet},
+ net::IpAddr,
+};
+
+use axum::{
+ extract::{Json, Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+};
+use mnl::mnl_sys::libc;
+use uuid::Uuid;
+
+use crate::block_list::BlockRule;
+
+#[derive(serde::Deserialize, Clone)]
+pub struct NewRule {
+ pub src: IpAddr,
+ pub dst: IpAddr,
+ pub protocols: Option<BTreeSet<TransportProtocol>>,
+ pub label: Uuid,
+}
+
+#[derive(serde::Deserialize, serde::Serialize, PartialOrd, Ord, PartialEq, Eq, Clone, Copy)]
+#[serde(rename_all = "snake_case")]
+pub enum TransportProtocol {
+ Tcp,
+ Udp,
+ Icmp,
+ IcmpV6,
+}
+
+impl TransportProtocol {
+ pub fn as_ipproto(&self) -> u8 {
+ match self {
+ TransportProtocol::Udp => libc::IPPROTO_UDP as u8,
+ TransportProtocol::Tcp => libc::IPPROTO_TCP as u8,
+ TransportProtocol::Icmp => libc::IPPROTO_ICMP as u8,
+ TransportProtocol::IcmpV6 => libc::IPPROTO_ICMPV6 as u8,
+ }
+ }
+}
+
+pub async fn add_rule(
+ State(state): State<super::State>,
+ Json(rule): Json<NewRule>,
+) -> impl IntoResponse {
+ let result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
+ let label = rule.label;
+ let rule = BlockRule {
+ src: rule.src,
+ dst: rule.dst,
+ protocols: rule.protocols.unwrap_or_default(),
+ };
+ let Ok(mut fw) = state.block_list.lock() else {
+ return Err(anyhow::anyhow!("Firewall thread panicked"));
+ };
+
+ fw.add_rule(rule.clone(), label)?;
+ log::info!(
+ "Successfully added a rule to block {} from {} for test {}",
+ rule.src,
+ rule.dst,
+ label,
+ );
+ Ok(())
+ })
+ .await
+ .expect("failed to join blocking task");
+
+ respond_with_result(result, StatusCode::CREATED)
+}
+
+pub async fn delete_rules(
+ Path(label): Path<Uuid>,
+ State(state): State<super::State>,
+) -> impl IntoResponse {
+ let result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
+ let Ok(mut fw) = state.block_list.lock() else {
+ return Err(anyhow::anyhow!("Firewall thread panicked"));
+ };
+
+ fw.clear_rules_with_label(&label)?;
+ log::info!("Successfully removed all rules for test {label}",);
+ Ok(())
+ })
+ .await
+ .expect("failed to join blocking task");
+ respond_with_result(result, StatusCode::OK)
+}
+
+fn respond_with_result(result: anyhow::Result<()>, success_code: StatusCode) -> impl IntoResponse {
+ match result {
+ Ok(_) => (success_code, String::new()),
+ Err(err) => (StatusCode::SERVICE_UNAVAILABLE, format!("{err}\n")),
+ }
+}
+
+pub async fn list_all_rules(State(state): State<super::State>) -> impl IntoResponse {
+ let all_rules = tokio::task::spawn_blocking(move || -> anyhow::Result<BTreeMap<_, _>> {
+ let Ok(fw) = state.block_list.lock() else {
+ return Err(anyhow::anyhow!("Firewall thread panicked"));
+ };
+
+ Ok(fw.rules().clone())
+ })
+ .await
+ .expect("failed to join blocking task");
+
+ match all_rules {
+ Ok(all_rules) => axum::Json(all_rules).into_response(),
+ Err(err) => (StatusCode::SERVICE_UNAVAILABLE, format!("{err}\n")).into_response(),
+ }
+}
diff --git a/ci/ios/test-router/router-config.nix b/ci/ios/test-router/router-config.nix
new file mode 100644
index 0000000000..c10a817145
--- /dev/null
+++ b/ci/ios/test-router/router-config.nix
@@ -0,0 +1,233 @@
+args@{ hostname
+, # hostname of the router
+ lanMac ? null
+, # MAC address of the local area network interface
+ wanMac
+, # MAC address of the upstream interface
+ lanIp
+, # IP adderss/subnet
+}:
+
+{ config, pkgs, lib, ... }:
+let
+ ifNotNull = maybeNull: attrSet: lib.attrsets.optionalAttrs (!builtins.isNull maybeNull) attrSet;
+in
+
+let
+ raas = pkgs.callPackage ./raas.nix { };
+
+ gatewayIpGroup = builtins.match "([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)/[0-9]+" args.lanIp;
+ gatewayAddress = builtins.elemAt gatewayIpGroup 0;
+
+in
+{
+ imports = [
+ ./nftables.nix
+ ];
+
+ services.nftables.internetHostOverride = gatewayAddress;
+ services.nftables.lanInterfaces = "lan";
+
+ environment.systemPackages = with pkgs; [ htop vim curl dig tcpdump cargo ];
+
+ networking.hostName = args.hostname;
+ networking.useDHCP = true;
+
+ system.stateVersion = "23.11";
+
+ systemd.network.netdevs."1-lanBridge" = {
+ netdevConfig = {
+ Kind = "bridge";
+ Name = "lan";
+ };
+ };
+
+ systemd.network.links = {
+ "1-lanIface" = ifNotNull lanMac {
+ matchConfig.PermanentMACAddress = args.lanMac;
+ linkConfig.Name = "lanEth";
+ };
+
+ "1-wanIface" = {
+ matchConfig.PermanentMACAddress = args.wanMac;
+ linkConfig.Name = "wan";
+ };
+ };
+
+ networking = { firewall.enable = false; };
+ hardware.bluetooth.enable = false;
+
+ boot.kernel.sysctl = {
+ # if you use ipv4, this is all you need
+ "net.ipv4.conf.all.forwarding" = true;
+
+ # If you want to use it for ipv6
+ "net.ipv6.conf.all.forwarding" = true;
+
+ # source: https://github.com/mdlayher/homelab/blob/master/nixos/routnerr-2/configuration.nix#L52
+ # By default, not automatically configure any IPv6 addresses.
+ "net.ipv6.conf.default.accept_ra" = 0;
+ "net.ipv6.conf.default.autoconf" = 0;
+ };
+
+ # when the above sysctl script is executed, wan is not renamed yet
+ systemd.services.sysctl_fixup_after_boot = {
+ enable = true;
+ bindsTo = [ "sys-subsystem-net-devices-wan.device" ];
+ before = [ "systemd-networkd.service" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig.ExecStart = ''
+ ${pkgs.sysctl}/bin/sysctl net.ipv6.conf.wan.accept_ra=2 net.ipv6.conf.wan.autoconf=1 net.ipv6.conf.all.use_tempaddr=1
+ '';
+ };
+
+ networking.useNetworkd = true;
+
+ networking.wireguard.interfaces.staging = {
+ privateKeyFile = "/staging-wg-private-key";
+ ips = [ "10.64.9.184/32" "fc00:bbbb:bbbb:bb01::a40:9b8/128" ];
+ allowedIPsAsRoutes = true;
+ # postSetup could be used to dynamically fetch the IP of the staging API and set up the route to that IP through this interface too.
+ # postSetup = '''';
+ peers = [{
+ publicKey = "2KS+F8ZAOUSMwygl2CYqkqFhbi3L5u58b3kIpaylaEk=";
+ name = "se-sto-wg-001-staging";
+ endpoint = "85.203.53.81:51820";
+ allowedIPs = [
+ # api.stagemole.eu
+ "185.217.116.129/32"
+ # api-app.stagemole.eu
+ "185.217.116.132/32"
+ # api-partners.stagemole.eu
+ "185.217.116.131/32"
+ ];
+ }];
+ };
+
+ systemd.network.enable = true;
+
+ systemd.network.networks.wan = {
+ name = "wan";
+ DHCP = "yes";
+
+ networkConfig = {
+ IPv6AcceptRA = true;
+ DHCP = "yes";
+ };
+
+ ipv6AcceptRAConfig = {
+ DHCPv6Client = "always";
+ UseDNS = true;
+ };
+
+ dhcpV4Config = {
+ Hostname = args.hostname;
+ UseDNS = true;
+ };
+
+ dhcpV6Config = { UseDNS = true; };
+ };
+
+ # obtain all leases
+ # if=lan; \
+ # link_id="$(ip --oneline link show dev "$if" | cut -f 1 -d:)"; \
+ # busctl -j get-property org.freedesktop.network1 \
+ # "/org/freedesktop/network1/link/${link_id}" \
+ # org.freedesktop.network1.DHCPServer \
+ # Leases
+
+ systemd.network.networks."lanEth" = ifNotNull lanMac {
+ matchConfig.Name = "lanEth";
+ networkConfig.Bridge = "lan";
+ linkConfig.RequiredForOnline = "enslaved";
+ };
+
+
+ systemd.network.networks.lan = {
+ name = "lan";
+ address = [ "192.168.105.1/24" ];
+
+ networkConfig = {
+ DHCPServer = true;
+ IPv6AcceptRA = false;
+ IPv6SendRA = true;
+ DHCPPrefixDelegation = true;
+ ConfigureWithoutCarrier = true;
+ };
+
+ dhcpServerConfig = {
+ ServerAddress = "192.168.105.1/24";
+ DNS = [ "1.1.1.1" "1.0.0.1" ];
+ PoolOffset = 128;
+ EmitDNS = true;
+ EmitNTP = true;
+ UplinkInterface = "wan";
+ };
+
+ dhcpServerStaticLeases = [
+ # {
+ # If we ever want a specific MAC to receive a static IP, add them here :)
+ # dhcpServerStaticLeaseConfig = {
+ # Address = "192.168.105.2";
+ # MACAddress = "78:45:58:C3:75:5E";
+ # };
+ # }
+ ];
+
+ ipv6SendRAConfig = {
+ UplinkInterface = [ "wan" ];
+ EmitDNS = true;
+ };
+
+ dhcpPrefixDelegationConfig = {
+ UplinkInterface = "wan";
+ Announce = true;
+ Assign = true;
+ };
+ };
+
+ services.resolved.enable = true;
+
+ # disable logging forever
+ services.journald.extraConfig = ''
+ Storage=Volatile;
+ SystemMaxUse=50M
+ RuntimeMaxUse=50M
+ '';
+
+ services.openssh = {
+ enable = true;
+ ports = [ 22 2021 ];
+ settings.PermitRootLogin = "yes";
+ };
+
+ services.avahi = {
+ enable = true;
+ reflector = true;
+ allowInterfaces = [ "lan" ];
+ };
+
+ systemd.services.raas =
+
+ let
+ listenIpGroup = builtins.match "([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)/[0-9]+" args.lanIp;
+ listenAddress = builtins.elemAt listenIpGroup 0;
+ in
+ {
+ enable = true;
+ description = "Web service to apply blocking firewall rules";
+ bindsTo = [ "sys-subsystem-net-devices-lan.device" ];
+ after = [ "systemd-networkd.service" "network-online.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig.ExecStart = ''
+ ${raas}/bin/raas ${listenAddress}:80
+ '';
+ };
+
+ services.shadowsocks = {
+ enable = true;
+ port = 443;
+ encryptionMethod = "aes-256-gcm";
+ password = "mullvad";
+ };
+}