diff options
| author | Emīls <emils@mullvad.net> | 2025-03-26 16:12:45 +0100 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2025-03-26 16:12:45 +0100 |
| commit | df5128939e8eaafb6ba0cec7039e8f14f47301f2 (patch) | |
| tree | 8dc348dba8d3f0686efca689f4f29f052bb038ac | |
| parent | 771e2c4f15ff0a4689bfc25dce08a9f6fc0f1f57 (diff) | |
| parent | 76ec2d6b88db62dd013cfb59a721136798e73e89 (diff) | |
| download | mullvadvpn-df5128939e8eaafb6ba0cec7039e8f14f47301f2.tar.xz mullvadvpn-df5128939e8eaafb6ba0cec7039e8f14f47301f2.zip | |
Merge branch 'ios-test-router'
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | ci/ios/test-router/README.md | 49 | ||||
| -rw-r--r-- | ci/ios/test-router/app-team-ios-lab.nix | 44 | ||||
| -rw-r--r-- | ci/ios/test-router/flake.lock | 27 | ||||
| -rw-r--r-- | ci/ios/test-router/flake.nix | 48 | ||||
| -rw-r--r-- | ci/ios/test-router/nftables.nix | 129 | ||||
| -rw-r--r-- | ci/ios/test-router/raas.nix | 12 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/Cargo.lock | 1353 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/Cargo.toml | 32 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/block_list/mod.rs | 94 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/block_list/rule.rs | 82 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/capture/cleanup.rs | 58 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/capture/mod.rs | 166 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/capture/parse.rs | 241 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/main.rs | 67 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/web/firewall.rs | 94 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/web/ip.rs | 7 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/web/mod.rs | 37 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/web/routes.rs | 114 | ||||
| -rw-r--r-- | ci/ios/test-router/router-config.nix | 233 |
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"; + }; +} |
