From 7bdf40fe7fc1d0c458c94b4dbf6f5152cb8985b6 Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 21 Apr 2023 11:37:30 -0700 Subject: [PATCH 01/54] add connlib rust logic. This commit convert this repo into a rust workspace. Divides into 2 types of crates, libs/clients. Clients are thin wrappers used directly by native clients. Libs implement the logic. We have 4 logic crates: - Gateway: Gateway-specific logic. - Clients: Client-specific logic. - Tunnel: General logic for wireguard/ice tunnels. - Common: Types shared by all crates. --- .gitignore | 16 +- Cargo.toml | 35 +- android/gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 0 bytes apple/Cargo.lock | 1240 ----------------- apple/src/lib.rs | 88 -- {android => clients/android}/.gitignore | 0 {android => clients/android}/Cargo.toml | 2 +- {android => clients/android}/build.gradle.kts | 0 .../android}/consumer-rules.pro | 0 .../android}/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 {android => clients/android}/gradlew | 0 {android => clients/android}/gradlew.bat | 0 .../android}/lib/build.gradle.kts | 0 .../android}/lib/src/main/AndroidManifest.xml | 0 .../main/java/dev/firezone/connlib/Logger.kt | 0 .../main/java/dev/firezone/connlib/Session.kt | 0 .../java/dev/firezone/connlib/VpnService.kt | 0 .../java/dev/firezone/connlib/ConnlibTest.kt | 0 .../java/dev/firezone/connlib/SessionTest.kt | 0 .../dev/firezone/connlib/VpnServiceTest.kt | 0 .../android}/proguard-rules.pro | 0 .../android}/settings.gradle.kts | 0 {android => clients/android}/src/lib.rs | 53 +- {apple => clients/apple}/.gitignore | 0 {apple => clients/apple}/Cargo.toml | 6 +- {apple => clients/apple}/README.md | 0 .../apple}/Sources/Connlib/Adapter.swift | 0 .../apple}/Sources/Connlib/BridgingHeader.h | 0 .../Sources/Connlib/CallbackHandler.swift | 0 .../Sources/Connlib/Generated/.gitignore | 0 .../apple}/Sources/Connlib/connlib.h | 0 .../apple}/Tests/connlibTests/.gitkeep | 0 {apple => clients/apple}/build-rust.sh | 0 {apple => clients/apple}/build-xcframework.sh | 0 {apple => clients/apple}/build.rs | 0 .../apple}/connlib.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Connlib.xcscheme | 0 clients/apple/src/lib.rs | 115 ++ gateway/Cargo.toml | 13 + gateway/src/main.rs | 42 + libs/client/Cargo.toml | 14 + libs/client/src/control.rs | 194 +++ libs/client/src/lib.rs | 20 + libs/client/src/messages.rs | 52 + libs/common/Cargo.toml | 32 + libs/common/src/control.rs | 227 +++ libs/common/src/error.rs | 92 ++ libs/common/src/error_type.rs | 20 + libs/common/src/lib.rs | 18 + libs/common/src/messages.rs | 75 + libs/common/src/messages/key.rs | 54 + libs/common/src/session.rs | 219 +++ libs/gateway/Cargo.toml | 14 + libs/gateway/src/control.rs | 146 ++ libs/gateway/src/lib.rs | 16 + libs/gateway/src/messages.rs | 87 ++ libs/tunnel/Cargo.toml | 39 + libs/tunnel/src/control_protocol.rs | 302 ++++ libs/tunnel/src/index.rs | 61 + libs/tunnel/src/lib.rs | 487 +++++++ libs/tunnel/src/multimap.rs | 80 ++ libs/tunnel/src/peer.rs | 81 ++ {src => libs/tunnel/src}/platform.rs | 0 {src => libs/tunnel/src}/platform/android.rs | 0 {src => libs/tunnel/src}/platform/apple.rs | 0 {src => libs/tunnel/src}/platform/linux.rs | 0 {src => libs/tunnel/src}/platform/windows.rs | 0 libs/tunnel/src/tun_darwin.rs | 75 + libs/tunnel/src/tun_linux.rs | 0 macros/Cargo.toml | 13 + macros/src/lib.rs | 108 ++ src/lib.rs | 33 - 75 files changed, 2746 insertions(+), 1423 deletions(-) delete mode 100644 android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 apple/Cargo.lock delete mode 100644 apple/src/lib.rs rename {android => clients/android}/.gitignore (100%) rename {android => clients/android}/Cargo.toml (79%) rename {android => clients/android}/build.gradle.kts (100%) rename {android => clients/android}/consumer-rules.pro (100%) rename {android => clients/android}/gradle.properties (100%) rename {android => clients/android}/gradle/wrapper/gradle-wrapper.properties (100%) rename {android => clients/android}/gradlew (100%) rename {android => clients/android}/gradlew.bat (100%) rename {android => clients/android}/lib/build.gradle.kts (100%) rename {android => clients/android}/lib/src/main/AndroidManifest.xml (100%) rename {android => clients/android}/lib/src/main/java/dev/firezone/connlib/Logger.kt (100%) rename {android => clients/android}/lib/src/main/java/dev/firezone/connlib/Session.kt (100%) rename {android => clients/android}/lib/src/main/java/dev/firezone/connlib/VpnService.kt (100%) rename {android => clients/android}/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt (100%) rename {android => clients/android}/lib/src/test/java/dev/firezone/connlib/SessionTest.kt (100%) rename {android => clients/android}/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt (100%) rename {android => clients/android}/proguard-rules.pro (100%) rename {android => clients/android}/settings.gradle.kts (100%) rename {android => clients/android}/src/lib.rs (71%) rename {apple => clients/apple}/.gitignore (100%) rename {apple => clients/apple}/Cargo.toml (50%) rename {apple => clients/apple}/README.md (100%) rename {apple => clients/apple}/Sources/Connlib/Adapter.swift (100%) rename {apple => clients/apple}/Sources/Connlib/BridgingHeader.h (100%) rename {apple => clients/apple}/Sources/Connlib/CallbackHandler.swift (100%) rename {apple => clients/apple}/Sources/Connlib/Generated/.gitignore (100%) rename {apple => clients/apple}/Sources/Connlib/connlib.h (100%) rename {apple => clients/apple}/Tests/connlibTests/.gitkeep (100%) rename {apple => clients/apple}/build-rust.sh (100%) rename {apple => clients/apple}/build-xcframework.sh (100%) rename {apple => clients/apple}/build.rs (100%) rename {apple => clients/apple}/connlib.xcodeproj/project.pbxproj (100%) rename {apple => clients/apple}/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {apple => clients/apple}/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {apple => clients/apple}/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme (100%) create mode 100644 clients/apple/src/lib.rs create mode 100644 gateway/Cargo.toml create mode 100644 gateway/src/main.rs create mode 100644 libs/client/Cargo.toml create mode 100644 libs/client/src/control.rs create mode 100644 libs/client/src/lib.rs create mode 100644 libs/client/src/messages.rs create mode 100644 libs/common/Cargo.toml create mode 100644 libs/common/src/control.rs create mode 100644 libs/common/src/error.rs create mode 100644 libs/common/src/error_type.rs create mode 100644 libs/common/src/lib.rs create mode 100644 libs/common/src/messages.rs create mode 100644 libs/common/src/messages/key.rs create mode 100644 libs/common/src/session.rs create mode 100644 libs/gateway/Cargo.toml create mode 100644 libs/gateway/src/control.rs create mode 100644 libs/gateway/src/lib.rs create mode 100644 libs/gateway/src/messages.rs create mode 100644 libs/tunnel/Cargo.toml create mode 100644 libs/tunnel/src/control_protocol.rs create mode 100644 libs/tunnel/src/index.rs create mode 100644 libs/tunnel/src/lib.rs create mode 100644 libs/tunnel/src/multimap.rs create mode 100644 libs/tunnel/src/peer.rs rename {src => libs/tunnel/src}/platform.rs (100%) rename {src => libs/tunnel/src}/platform/android.rs (100%) rename {src => libs/tunnel/src}/platform/apple.rs (100%) rename {src => libs/tunnel/src}/platform/linux.rs (100%) rename {src => libs/tunnel/src}/platform/windows.rs (100%) create mode 100644 libs/tunnel/src/tun_darwin.rs create mode 100644 libs/tunnel/src/tun_linux.rs create mode 100644 macros/Cargo.toml create mode 100644 macros/src/lib.rs delete mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 0bcad19..49605b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,12 @@ ### Rust ### -/target +target/ # Libraries shouldn't lock their dependencies -/Cargo.lock +Cargo.lock ### Android ### # Gradle files .gradle/ build/ -android/target/ # Local configuration file (sdk path, etc) local.properties @@ -101,11 +100,10 @@ proguard/ # Log Files # Android Studio -/*/build/ +build/ /*/local.properties -/*/out -/*/*/build -/*/*/production +out/ +production/ .navigation/ *.ipr *~ @@ -126,7 +124,6 @@ obj/ # IntelliJ IDEA *.iws -/out/ # User-specific configurations .idea/caches/ @@ -172,9 +169,8 @@ fabric.properties ### Apple ### .DS_Store .build/ -build/ DerivedData/ xcuserdata/ -**/*.xcuserstate +*.xcuserstate Firezone/Developer.xcconfig diff --git a/Cargo.toml b/Cargo.toml index fe3de85..11c0607 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,11 @@ -[package] -name = "firezone-connlib" -version = "0.1.6" -edition = "2021" - -[dependencies] -# Apple tunnel dependencies -[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", features = ["device"] } - -# Linux tunnel dependencies -[target.'cfg(target_os = "linux")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", features = ["device"] } - -# Android tunnel dependencies -[target.'cfg(target_os = "android")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", features = ["jni-bindings"] } -android_logger = "0.13" -log = "0.4.14" - -# Windows tunnel dependencies -[target.'cfg(target_os = "windows")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f" } -wintun = "0.2.1" +[workspace] +members = [ + "clients/android", + "clients/apple", + "libs/tunnel", + "libs/client", + "libs/gateway", + "libs/common", + "gateway", + "macros", +] diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ccebba7710deaf9f98673a68957ea02138b60d0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61608 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 diff --git a/apple/Cargo.lock b/apple/Cargo.lock deleted file mode 100644 index 14afc01..0000000 --- a/apple/Cargo.lock +++ /dev/null @@ -1,1240 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aead" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_log-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f0fc03f560e1aebde41c2398b691cb98b5ea5996a6184a7a67bbbb77448969" - -[[package]] -name = "android_logger" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa490e751f3878eb9accb9f18988eca52c2337ce000a8bf31ef50d4c723ca9e" -dependencies = [ - "android_log-sys", - "env_logger", - "log", - "once_cell", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "boringtun" -version = "0.5.2" -source = "git+https://github.com/cloudflare/boringtun?rev=878385f#878385f171d60effac4ad1a9d4dee41e777528b8" -dependencies = [ - "aead", - "base64", - "blake2", - "chacha20poly1305", - "hex", - "hmac", - "ip_network", - "ip_network_table", - "jni", - "libc", - "nix", - "parking_lot", - "rand_core", - "ring", - "socket2", - "thiserror", - "tracing", - "tracing-subscriber", - "untrusted 0.9.0", - "x25519-dalek", -] - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chacha20" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "combine" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "connlib-apple" -version = "0.1.6" -dependencies = [ - "firezone-connlib", - "libc", - "swift-bridge", - "swift-bridge-build", -] - -[[package]] -name = "cpufeatures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.0.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" -dependencies = [ - "cfg-if", - "fiat-crypto", - "packed_simd_2", - "platforms", - "subtle", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "env_logger" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "log", - "regex", -] - -[[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 = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - -[[package]] -name = "firezone-connlib" -version = "0.1.6" -dependencies = [ - "android_logger", - "boringtun", - "log", - "wintun", -] - -[[package]] -name = "generic-array" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "ip_network" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" - -[[package]] -name = "ip_network_table" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" -dependencies = [ - "ip_network", - "ip_network_table-deps-treebitmap", -] - -[[package]] -name = "ip_network_table-deps-treebitmap" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags", - "cfg-if", - "libc", -] - -[[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 = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if", - "libm", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.45.0", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "platforms" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - -[[package]] -name = "rustix" -version = "0.36.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.45.0", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "serde" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.12", -] - -[[package]] -name = "sharded-slab" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "swift-bridge" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa07c7cd2b2d7ca48d96f5abd159e3fd3eee3457e7bd03adc1994bfbdabd2f" -dependencies = [ - "swift-bridge-build", - "swift-bridge-macro", -] - -[[package]] -name = "swift-bridge-build" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286f727dc922736a1ed74c06bebf43d08b8295a7ba38c77326c74e2b9dfd43df" -dependencies = [ - "proc-macro2", - "swift-bridge-ir", - "syn 1.0.109", - "tempfile", -] - -[[package]] -name = "swift-bridge-ir" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4de97e9abde20abc1c01f6d4faa8072d723c73aba288264481a83a1e2787dc" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "swift-bridge-macro" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fabad38a0fc643ceeafefed79e08408c30eeec325b629e426a15b13d055d0a" -dependencies = [ - "proc-macro2", - "quote", - "swift-bridge-ir", - "syn 1.0.109", -] - -[[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.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - -[[package]] -name = "tempfile" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.42.0", -] - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.12", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "tracing-core" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "universal-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "widestring" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" - -[[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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[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.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "wintun" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "094235c21dfb805c6870b00d0d80f65b727d8296ab88ae6b506e827e4b4116de" -dependencies = [ - "itertools", - "libloading", - "log", - "once_cell", - "rand", - "widestring", - "winapi", -] - -[[package]] -name = "x25519-dalek" -version = "2.0.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95" -dependencies = [ - "curve25519-dalek", - "rand_core", - "serde", - "zeroize", -] - -[[package]] -name = "zeroize" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", -] diff --git a/apple/src/lib.rs b/apple/src/lib.rs deleted file mode 100644 index 13f8905..0000000 --- a/apple/src/lib.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Swift bridge generated code triggers this below -#![allow(improper_ctypes)] - -use firezone_connlib::Session; -use std::sync::Arc; - -#[swift_bridge::bridge] -mod ffi { - // TODO: Allegedly not FFI safe, but works - #[swift_bridge(swift_repr = "struct")] - struct ResourceList { - resources: String, - } - - // TODO: Allegedly not FFI safe, but works - #[swift_bridge(swift_repr = "struct")] - struct TunnelAddresses { - address4: String, - address6: String, - } - - extern "Rust" { - type WrappedSession; - - #[swift_bridge(associated_to = WrappedSession)] - fn connect( - portal_url: String, - token: String, - callback_handler: CallbackHandler, - ) -> WrappedSession; - - #[swift_bridge(swift_name = "bumpSockets")] - fn bump_sockets(&self) -> bool; - - #[swift_bridge(swift_name = "disableSomeRoamingForBrokenMobileSemantics")] - fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool; - - fn disconnect(&self) -> bool; - } - - extern "Swift" { - type CallbackHandler; - - #[swift_bridge(swift_name = "onUpdateResources")] - fn on_update_resources(&self, resourceList: ResourceList) -> bool; - - #[swift_bridge(swift_name = "onSetTunnelAddresses")] - fn on_set_tunnel_addresses(&self, tunnelAddresses: TunnelAddresses) -> bool; - } -} - -pub struct WrappedSession { - session: Session, -} - -impl WrappedSession { - fn connect(portal_url: String, token: String, callback_handler: ffi::CallbackHandler) -> Self { - let session = Session::connect(portal_url, token); - - let resources = "[]".to_string(); - let cb = Arc::new(callback_handler); - let callback_handler = Arc::clone(&cb); - - callback_handler.on_update_resources(ffi::ResourceList { resources }); - callback_handler.on_set_tunnel_addresses(ffi::TunnelAddresses { - address4: "100.100.1.1".to_string(), - address6: "fd00:0222:2021:1111:0000:0000:0001:0002".to_string(), - }); - - WrappedSession { - session: session.unwrap(), - } - } - - fn bump_sockets(&self) -> bool { - // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#L177 - return true; - } - - fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { - // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 - return true; - } - - fn disconnect(&self) -> bool { - self.session.disconnect() - } -} diff --git a/android/.gitignore b/clients/android/.gitignore similarity index 100% rename from android/.gitignore rename to clients/android/.gitignore diff --git a/android/Cargo.toml b/clients/android/Cargo.toml similarity index 79% rename from android/Cargo.toml rename to clients/android/Cargo.toml index 85b9a56..6526daa 100644 --- a/android/Cargo.toml +++ b/clients/android/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] jni = { version = "0.21.1", features = ["invocation"] } -firezone-connlib = { path = "../" } +firezone-client-connlib = { path = "../../libs/client" } log = "0.4" android_logger = "0.13" diff --git a/android/build.gradle.kts b/clients/android/build.gradle.kts similarity index 100% rename from android/build.gradle.kts rename to clients/android/build.gradle.kts diff --git a/android/consumer-rules.pro b/clients/android/consumer-rules.pro similarity index 100% rename from android/consumer-rules.pro rename to clients/android/consumer-rules.pro diff --git a/android/gradle.properties b/clients/android/gradle.properties similarity index 100% rename from android/gradle.properties rename to clients/android/gradle.properties diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/clients/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.properties rename to clients/android/gradle/wrapper/gradle-wrapper.properties diff --git a/android/gradlew b/clients/android/gradlew similarity index 100% rename from android/gradlew rename to clients/android/gradlew diff --git a/android/gradlew.bat b/clients/android/gradlew.bat similarity index 100% rename from android/gradlew.bat rename to clients/android/gradlew.bat diff --git a/android/lib/build.gradle.kts b/clients/android/lib/build.gradle.kts similarity index 100% rename from android/lib/build.gradle.kts rename to clients/android/lib/build.gradle.kts diff --git a/android/lib/src/main/AndroidManifest.xml b/clients/android/lib/src/main/AndroidManifest.xml similarity index 100% rename from android/lib/src/main/AndroidManifest.xml rename to clients/android/lib/src/main/AndroidManifest.xml diff --git a/android/lib/src/main/java/dev/firezone/connlib/Logger.kt b/clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt similarity index 100% rename from android/lib/src/main/java/dev/firezone/connlib/Logger.kt rename to clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt diff --git a/android/lib/src/main/java/dev/firezone/connlib/Session.kt b/clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt similarity index 100% rename from android/lib/src/main/java/dev/firezone/connlib/Session.kt rename to clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt diff --git a/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt b/clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt similarity index 100% rename from android/lib/src/main/java/dev/firezone/connlib/VpnService.kt rename to clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt diff --git a/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt b/clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt similarity index 100% rename from android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt rename to clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt diff --git a/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt b/clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt similarity index 100% rename from android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt rename to clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt diff --git a/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt b/clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt similarity index 100% rename from android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt rename to clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt diff --git a/android/proguard-rules.pro b/clients/android/proguard-rules.pro similarity index 100% rename from android/proguard-rules.pro rename to clients/android/proguard-rules.pro diff --git a/android/settings.gradle.kts b/clients/android/settings.gradle.kts similarity index 100% rename from android/settings.gradle.kts rename to clients/android/settings.gradle.kts diff --git a/android/src/lib.rs b/clients/android/src/lib.rs similarity index 71% rename from android/src/lib.rs rename to clients/android/src/lib.rs index 443ba43..7d861df 100644 --- a/android/src/lib.rs +++ b/clients/android/src/lib.rs @@ -4,7 +4,9 @@ extern crate android_logger; extern crate jni; use self::jni::JNIEnv; use android_logger::Config; -use firezone_connlib::Session; +use firezone_client_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, TunnelAddresses, +}; use jni::objects::{JClass, JObject, JString, JValue}; use log::LevelFilter; @@ -25,19 +27,36 @@ pub extern "system" fn Java_dev_firezone_connlib_Logger_init(_: JNIEnv, _: JClas ) } +pub enum CallbackHandler {} +impl Callbacks for CallbackHandler { + fn on_update_resources(_resource_list: ResourceList) { + todo!() + } + + fn on_set_tunnel_adresses(_tunnel_addresses: TunnelAddresses) { + todo!() + } + + fn on_error(_error: &Error, _error_type: ErrorType) { + todo!() + } +} + #[allow(non_snake_case)] #[no_mangle] -pub extern "system" fn Java_dev_firezone_connlib_Session_connect( +pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect( mut env: JNIEnv, _class: JClass, portal_url: JString, portal_token: JString, callback: JObject, -) -> *const Session { +) -> *const Session { let portal_url: String = env.get_string(&portal_url).unwrap().into(); let portal_token: String = env.get_string(&portal_token).unwrap().into(); - let session = Session::connect(portal_url, portal_token).expect("Failed to connect to portal"); + let session = Box::new( + Session::connect::(portal_url.as_str(), portal_token).expect("TODO!"), + ); // TODO: Get actual IPs returned from portal based on this device let tunnelAddressesJSON = "[{\"tunnel_ipv4\": \"100.100.1.1\", \"tunnel_ipv6\": \"fd00:0222:2011:1111:6def:1001:fe67:0012\"}]"; @@ -52,22 +71,7 @@ pub extern "system" fn Java_dev_firezone_connlib_Session_connect( Err(e) => error!("Failed to call setTunnelAddresses: {:?}", e), } - // TODO: Fix callback ref copy - // let resourcesJSON = "[{\"id\": \"342b8565-5de2-4289-877c-751d924518e9\", \"label\": \"GitLab\", \"address\": \"gitlab.com\", \"tunnel_ipv4\": \"100.71.55.101\", \"tunnel_ipv6\": \"fd00:0222:2011:1111:6def:1001:fe67:0012\"}]"; - // let resources = env.new_string(resourcesJSON).unwrap(); - // match env.call_method( - // callback, - // "onUpdateResources", - // "(Ljava/lang/String;)Z", - // &[JValue::from(&resources)], - // ) { - // Ok(res) => trace!("onUpdateResources returned {:?}", res), - // Err(e) => error!("Failed to call setResources: {:?}", e), - // } - - let session_ptr = Box::into_raw(Box::new(session)); - - session_ptr + Box::into_raw(session) } #[allow(non_snake_case)] @@ -75,19 +79,20 @@ pub extern "system" fn Java_dev_firezone_connlib_Session_connect( pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_disconnect( _env: JNIEnv, _: JClass, - session_ptr: *mut Session, + session_ptr: *mut Session, ) -> bool { if session_ptr.is_null() { return false; } - unsafe { Box::from_raw(session_ptr).disconnect() } + let session = unsafe { &mut *session_ptr }; + session.disconnect() } #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_bump_sockets( - session_ptr: *const Session, + session_ptr: *const Session, ) -> bool { if session_ptr.is_null() { return false; @@ -102,7 +107,7 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_bump_sockets( #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_disable_some_roaming_for_broken_mobile_semantics( - session_ptr: *const Session, + session_ptr: *const Session, ) -> bool { if session_ptr.is_null() { return false; diff --git a/apple/.gitignore b/clients/apple/.gitignore similarity index 100% rename from apple/.gitignore rename to clients/apple/.gitignore diff --git a/apple/Cargo.toml b/clients/apple/Cargo.toml similarity index 50% rename from apple/Cargo.toml rename to clients/apple/Cargo.toml index a3be9d3..acddc54 100644 --- a/apple/Cargo.toml +++ b/clients/apple/Cargo.toml @@ -6,12 +6,12 @@ edition = "2021" build = "build.rs" [build-dependencies] -swift-bridge-build = "0.1" +swift-bridge-build = { path = "../../../swift-bridge/crates/swift-bridge-build" } [dependencies] libc = "0.2" -swift-bridge = "0.1" -firezone-connlib = { path = "../" } +swift-bridge = { path = "../../../swift-bridge" } +firezone-client-connlib = { path = "../../libs/client" } [lib] name = "connlib" diff --git a/apple/README.md b/clients/apple/README.md similarity index 100% rename from apple/README.md rename to clients/apple/README.md diff --git a/apple/Sources/Connlib/Adapter.swift b/clients/apple/Sources/Connlib/Adapter.swift similarity index 100% rename from apple/Sources/Connlib/Adapter.swift rename to clients/apple/Sources/Connlib/Adapter.swift diff --git a/apple/Sources/Connlib/BridgingHeader.h b/clients/apple/Sources/Connlib/BridgingHeader.h similarity index 100% rename from apple/Sources/Connlib/BridgingHeader.h rename to clients/apple/Sources/Connlib/BridgingHeader.h diff --git a/apple/Sources/Connlib/CallbackHandler.swift b/clients/apple/Sources/Connlib/CallbackHandler.swift similarity index 100% rename from apple/Sources/Connlib/CallbackHandler.swift rename to clients/apple/Sources/Connlib/CallbackHandler.swift diff --git a/apple/Sources/Connlib/Generated/.gitignore b/clients/apple/Sources/Connlib/Generated/.gitignore similarity index 100% rename from apple/Sources/Connlib/Generated/.gitignore rename to clients/apple/Sources/Connlib/Generated/.gitignore diff --git a/apple/Sources/Connlib/connlib.h b/clients/apple/Sources/Connlib/connlib.h similarity index 100% rename from apple/Sources/Connlib/connlib.h rename to clients/apple/Sources/Connlib/connlib.h diff --git a/apple/Tests/connlibTests/.gitkeep b/clients/apple/Tests/connlibTests/.gitkeep similarity index 100% rename from apple/Tests/connlibTests/.gitkeep rename to clients/apple/Tests/connlibTests/.gitkeep diff --git a/apple/build-rust.sh b/clients/apple/build-rust.sh similarity index 100% rename from apple/build-rust.sh rename to clients/apple/build-rust.sh diff --git a/apple/build-xcframework.sh b/clients/apple/build-xcframework.sh similarity index 100% rename from apple/build-xcframework.sh rename to clients/apple/build-xcframework.sh diff --git a/apple/build.rs b/clients/apple/build.rs similarity index 100% rename from apple/build.rs rename to clients/apple/build.rs diff --git a/apple/connlib.xcodeproj/project.pbxproj b/clients/apple/connlib.xcodeproj/project.pbxproj similarity index 100% rename from apple/connlib.xcodeproj/project.pbxproj rename to clients/apple/connlib.xcodeproj/project.pbxproj diff --git a/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme b/clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme similarity index 100% rename from apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme rename to clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme diff --git a/clients/apple/src/lib.rs b/clients/apple/src/lib.rs new file mode 100644 index 0000000..24580bd --- /dev/null +++ b/clients/apple/src/lib.rs @@ -0,0 +1,115 @@ +// Swift bridge generated code triggers this below +#![allow(improper_ctypes)] + +use firezone_client_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, SwiftConnlibError, SwiftErrorType, + TunnelAddresses, +}; + +#[swift_bridge::bridge] +mod ffi { + // TODO: Allegedly not FFI safe, but works + #[swift_bridge(swift_repr = "struct")] + struct ResourceList { + resources: String, + } + + // TODO: Allegedly not FFI safe, but works + #[swift_bridge(swift_repr = "struct")] + struct TunnelAddresses { + address4: String, + address6: String, + } + + #[swift_bridge(already_declared)] + enum SwiftConnlibError {} + + #[swift_bridge(already_declared)] + enum SwiftErrorType {} + + extern "Rust" { + type WrappedSession; + + #[swift_bridge(associated_to = WrappedSession)] + fn connect(portal_url: String, token: String) -> Result; + + #[swift_bridge(swift_name = "bumpSockets")] + fn bump_sockets(&self) -> bool; + + #[swift_bridge(swift_name = "disableSomeRoamingForBrokenMobileSemantics")] + fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool; + + fn disconnect(&mut self) -> bool; + } + + extern "Swift" { + type Opaque; + #[swift_bridge(swift_name = "onUpdateResources")] + fn on_update_resources(resourceList: ResourceList); + + #[swift_bridge(swift_name = "onSetTunnelAddresses")] + fn on_set_tunnel_addresses(tunnelAddresses: TunnelAddresses); + + #[swift_bridge(swift_name = "onError")] + fn on_error(error: SwiftConnlibError, error_type: SwiftErrorType); + } +} + +impl From for ffi::ResourceList { + fn from(value: ResourceList) -> Self { + Self { + resources: value.resources.join(","), + } + } +} + +impl From for ffi::TunnelAddresses { + fn from(value: TunnelAddresses) -> Self { + Self { + address4: value.address4.to_string(), + address6: value.address6.to_string(), + } + } +} + +/// This is used by the apple client to interact with our code. +pub struct WrappedSession { + session: Session, +} + +struct CallbackHandler; + +impl Callbacks for CallbackHandler { + fn on_update_resources(resource_list: ResourceList) { + ffi::on_update_resources(resource_list.into()); + } + + fn on_set_tunnel_adresses(tunnel_addresses: TunnelAddresses) { + ffi::on_set_tunnel_addresses(tunnel_addresses.into()); + } + + fn on_error(error: &Error, error_type: ErrorType) { + ffi::on_error(error.into(), error_type.into()); + } +} + +impl WrappedSession { + fn connect(portal_url: String, token: String) -> Result { + let session = Session::connect::(portal_url.as_str(), token)?; + Ok(WrappedSession { session }) + } + + fn bump_sockets(&self) -> bool { + // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#L177 + todo!() + } + + fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { + // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 + todo!() + } + + fn disconnect(&mut self) -> bool { + self.session.disconnect() + } +} diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml new file mode 100644 index 0000000..07145d1 --- /dev/null +++ b/gateway/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gateway" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +firezone-gateway-connlib = { path = "../libs/gateway" } +clap = { version = "4.2", features = ["derive"] } +url = { version = "2.3.1", default-features = false } +tracing-subscriber = { version = "0.3" } +tracing = { version = "0.1" } \ No newline at end of file diff --git a/gateway/src/main.rs b/gateway/src/main.rs new file mode 100644 index 0000000..1c05235 --- /dev/null +++ b/gateway/src/main.rs @@ -0,0 +1,42 @@ +use clap::Parser; +use firezone_gateway_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, TunnelAddresses, +}; +use url::Url; + +enum CallbackHandler {} + +impl Callbacks for CallbackHandler { + fn on_update_resources(_resource_list: ResourceList) { + todo!() + } + + fn on_set_tunnel_adresses(_tunnel_addresses: TunnelAddresses) { + todo!() + } + + fn on_error(_error: &Error, _error_type: ErrorType) { + todo!() + } +} + +fn main() { + tracing_subscriber::fmt::init(); + // TODO: read args from env instead + let args = Args::parse(); + // TODO: This is disgusting + let mut session = + Session::::connect::(args.url, args.secret).unwrap(); + session.wait_for_ctrl_c().unwrap(); + session.disconnect(); +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + #[arg(long)] + pub secret: String, + #[arg(long)] + pub url: Url, +} diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml new file mode 100644 index 0000000..970a877 --- /dev/null +++ b/libs/client/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "firezone-client-connlib" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.27", default-features = false, features = ["sync"] } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +async-trait = { version = "0.1", default-features = false } +libs-common = { path = "../common" } +firezone-tunnel = { path = "../tunnel" } +serde = { version = "1.0", default-features = false, features = ["std", "derive"] } diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs new file mode 100644 index 0000000..62bb7a8 --- /dev/null +++ b/libs/client/src/control.rs @@ -0,0 +1,194 @@ +use std::{marker::PhantomData, sync::Arc, time::Duration}; + +use crate::messages::{Connect, EgressMessages, IngressMessages, InitClient, Relays}; +use libs_common::{ + boringtun::x25519::StaticSecret, + error_type::ErrorType::{Fatal, Recoverable}, + messages::{Id, ResourceDescription}, + Callbacks, ControlSession, Result, +}; + +use async_trait::async_trait; +use firezone_tunnel::{ControlSignal, Tunnel}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; + +// FIXME: Replace all `expect` with a handler function +// that should be passed from the client. +// Also, we should decide if we disconnect or keep running depending on the error kind. + +const INTERNAL_CHANNEL_SIZE: usize = 256; + +#[async_trait] +impl ControlSignal for ControlSignaler { + async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()> { + self.internal_sender + .send(EgressMessages::GetConnectionDetails(resource.id)) + .await?; + Ok(()) + } +} + +/// Implementation of [ControlSession] for clients. +pub struct ControlPlane { + tunnel: Arc>, + control_signaler: ControlSignaler, + _phantom: PhantomData, +} + +#[derive(Clone)] +struct ControlSignaler { + internal_sender: Arc>, +} + +impl ControlPlane +where + C: Send + Sync + 'static, +{ + #[tracing::instrument(level = "trace", skip(self))] + async fn start(mut self, mut receiver: Receiver) { + let mut interval = tokio::time::interval(Duration::from_secs(10)); + loop { + tokio::select! { + Some(msg) = receiver.recv() => self.handle_message(msg).await, + _ = interval.tick() => self.stats_event().await, + else => break + } + } + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn init( + &mut self, + InitClient { + interface, + resources, + }: InitClient, + ) { + if let Err(e) = self.tunnel.set_interface(&interface).await { + tracing::error!("Couldn't intialize interface: {e}"); + C::on_error(&e, Fatal); + return; + } + + for resource_description in resources { + self.add_resource(resource_description) + } + + tracing::info!("Firezoned Started!"); + } + + #[tracing::instrument(level = "trace", skip(self))] + async fn connect( + &mut self, + Connect { + rtc_sdp, + resource_id, + gateway_public_key, + }: Connect, + ) { + if let Err(e) = self + .tunnel + .recieved_offer_response(resource_id, rtc_sdp, gateway_public_key.0.into()) + .await + { + C::on_error(&e, Recoverable); + } + } + + #[tracing::instrument(level = "trace", skip(self))] + fn add_resource(&self, resource_description: ResourceDescription) { + self.tunnel.add_resource(resource_description); + } + + #[tracing::instrument(level = "trace", skip(self))] + fn remove_resource(&self, id: Id) { + todo!() + } + + #[tracing::instrument(level = "trace", skip(self))] + fn update_resource(&self, resource_description: ResourceDescription) { + todo!() + } + + #[tracing::instrument(level = "trace", skip(self))] + fn relays( + &self, + Relays { + resource_id, + relays, + }: Relays, + ) { + let tunnel = Arc::clone(&self.tunnel); + let control_signaler = self.control_signaler.clone(); + tokio::spawn(async move { + match tunnel.request_connection(resource_id, relays).await { + Ok(connection_request) => { + if let Err(err) = control_signaler + .internal_sender + .send(EgressMessages::RequestConnection(connection_request)) + .await + { + tunnel.cleanup_connection(resource_id); + C::on_error(&err.into(), Recoverable); + } + } + Err(err) => { + tunnel.cleanup_connection(resource_id); + C::on_error(&err, Recoverable); + } + } + }); + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn handle_message(&mut self, msg: IngressMessages) { + match msg { + IngressMessages::InitClient(init) => self.init(init).await, + IngressMessages::Relays(connection_details) => self.relays(connection_details), + IngressMessages::Connect(connect) => self.connect(connect).await, + IngressMessages::AddResource(resource) => self.add_resource(resource), + IngressMessages::RemoveResource(resource) => self.remove_resource(resource.id), + IngressMessages::UpdateResource(resource) => self.update_resource(resource), + } + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn stats_event(&mut self) { + // TODO + } +} + +#[async_trait] +impl ControlSession + for ControlPlane +{ + #[tracing::instrument(level = "trace", skip(private_key))] + async fn start( + private_key: StaticSecret, + ) -> Result<(Sender, Receiver)> { + // This is kinda hacky, the buffer size is 1 so that we make sure that we + // process one message at a time, blocking if a previous message haven't been processed + // to force queue ordering. + let (sender, receiver) = channel::(1); + + let (internal_sender, internal_receiver) = channel(INTERNAL_CHANNEL_SIZE); + let internal_sender = Arc::new(internal_sender); + let control_signaler = ControlSignaler { internal_sender }; + let tunnel = Arc::new(Tunnel::new(private_key, control_signaler.clone()).await?); + + let control_plane = ControlPlane:: { + tunnel, + control_signaler, + _phantom: PhantomData, + }; + + // TODO: We should have some kind of callback from clients to surface errors here + tokio::spawn(async move { control_plane.start(receiver).await }); + + Ok((sender, internal_receiver)) + } + + fn mode() -> &'static str { + "client" + } +} diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs new file mode 100644 index 0000000..4df8006 --- /dev/null +++ b/libs/client/src/lib.rs @@ -0,0 +1,20 @@ +//! Main connlib library for clients. +use control::ControlPlane; +use messages::EgressMessages; +use messages::IngressMessages; + +mod control; +mod messages; + +const VIRTUAL_IFACE_MTU: u16 = 1420; + +/// Session type for clients. +/// +/// For more information see libs_common docs on [Session][libs_common::Session]. +pub type Session = libs_common::Session, IngressMessages, EgressMessages>; + +pub use libs_common::{ + error::SwiftConnlibError, + error_type::{ErrorType, SwiftErrorType}, + Callbacks, Error, ResourceList, TunnelAddresses, +}; diff --git a/libs/client/src/messages.rs b/libs/client/src/messages.rs new file mode 100644 index 0000000..2702a34 --- /dev/null +++ b/libs/client/src/messages.rs @@ -0,0 +1,52 @@ +use firezone_tunnel::RTCSessionDescription; +use serde::{Deserialize, Serialize}; + +use libs_common::messages::{Id, Interface, Key, RequestConnection, ResourceDescription}; + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct InitClient { + pub interface: Interface, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub resources: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct RemoveResource { + pub id: Id, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Connect { + pub rtc_sdp: RTCSessionDescription, + pub resource_id: Id, + pub gateway_public_key: Key, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Relays { + pub resource_id: Id, + pub relays: Vec, +} + +// These messages are the messages that can be recieved +// by a client. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum IngressMessages { + InitClient(InitClient), + Relays(Relays), + Connect(Connect), + + // Resources: arrive in an orderly fashion + AddResource(ResourceDescription), + RemoveResource(RemoveResource), + UpdateResource(ResourceDescription), +} + +// These messages can be sent from a client to a control pane +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum EgressMessages { + GetConnectionDetails(Id), + RequestConnection(RequestConnection), +} diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml new file mode 100644 index 0000000..ba1eadf --- /dev/null +++ b/libs/common/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "libs-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +device = ["boringtun/device"] +jni-bindings = ["boringtun/jni-bindings"] + +[dependencies] +base64 = { version = "0.21", default-features = false, features = ["std"] } +serde = { version = "1.0", default-features = false, features = ["derive", "std"] } +futures = { version = "0.3", default-features = false, features = ["std", "async-await", "executor"] } +futures-util = { version = "0.3", default-features = false, features = ["std", "async-await", "async-await-macro"] } +tokio-tungstenite = { version = "0.18", default-features = false, features = ["connect", "handshake"] } +webrtc = { version = "0.7" } +uuid = { version = "1.3", default-features = false, features = ["std", "v4", "serde"] } +thiserror = { version = "1.0", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +serde_json = { version = "1.0", default-features = false, features = ["std"] } +tokio = { version = "1.28", default-features = false, features = ["rt", "rt-multi-thread"]} +url = { version = "2.3.1", default-features = false } +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } +rand_core = { version = "0.6.4", default-features = false, features = ["std"] } +async-trait = { version = "0.1", default-features = false } +backoff = { version = "0.4", default-features = false } + +macros = { path = "../../macros" } + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +swift-bridge = { path = "../../../swift-bridge" } diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs new file mode 100644 index 0000000..8430230 --- /dev/null +++ b/libs/common/src/control.rs @@ -0,0 +1,227 @@ +//! Control protocol related module. +//! +//! This modules contains the logic for handling in and out messages through the control plane. +//! Handling of the message itself can be found in the other lib crates. +//! +//! Entrypoint for this module is [PhoenixChannel]. +use std::{marker::PhantomData, time::Duration}; + +use futures::{ + channel::mpsc::{channel, Receiver, Sender}, + TryStreamExt, +}; +use futures_util::{Future, SinkExt, StreamExt}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tokio_tungstenite::{connect_async, tungstenite}; +use tungstenite::Message; +use url::Url; + +use crate::Result; + +const CHANNEL_SIZE: usize = 1_000; + +/// Main struct to interact with the control-protocol channel. +/// +/// After creating a new `PhoenixChannel` using [PhoenixChannel::new] you need to +/// use [start][PhoenixChannel::start] for the channel to do anything. +/// +/// If you want to send something through the channel you need to obtain a [PhoenixSender] through +/// [PhoenixChannel::sender], this will already clone the sender so no need to clone it after you obtain it. +/// +/// When [PhoenixChannel::start] is called a new websocket is created that will listen message from the control plane +/// based on the parameters passed on [new][PhoenixChannel::new], from then on any messages sent with a sender +/// obtained by [PhoenixChannel::sender] will be forwarded to the websocket up to the control plane. Ingress messages +/// will be passed on to the `handler` provided in [PhoenixChannel::new]. +/// +/// The future returned by [PhoenixChannel::start] will finish when the websocket closes (by an error), meaning that if you +/// `await` it, it will block until you use `close` in a [PhoenixSender], the portal close the connection or something goes wrong. +pub struct PhoenixChannel { + uri: Url, + handler: F, + sender: Sender, + receiver: Receiver, + _phantom: PhantomData, +} + +impl PhoenixChannel +where + I: DeserializeOwned, + F: Fn(I) -> Fut, + Fut: Future + Send + 'static, +{ + /// Starts the tunnel with the parameters given in [Self::new]. + /// + /// See [struct-level docs][PhoenixChannel] for more info. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn start(&mut self) -> std::result::Result<(), tungstenite::Error> { + tracing::trace!("Trying to connect to the portal..."); + + let (ws_stream, _) = connect_async(&self.uri).await?; + + tracing::trace!("Successfully connected to portal"); + + let (write, read) = ws_stream.split(); + + let mut sender = self.sender(); + let Self { + handler, receiver, .. + } = self; + + let process_messages = read.try_for_each(|message| async { + Self::message_process(handler, message).await; + Ok(()) + }); + + // TODO: is Forward cancel safe? + // I would assume it is and that's the advantage over + // while let Some(item) = reciever.next().await { write.send(item) } ... + // but double check this! + // If it's not cancel safe this means an item can be consumed and never sent. + // Furthermore can this also happen if write errors out? *that* I'd assume is possible... + // What option is left? write a new future to forward items. + // For now we should never assume that an item arrived the portal because we sent it! + let send_messages = receiver.map(Ok).forward(write); + + let phoenix_heartbeat = tokio::spawn(async move { + let mut timer = tokio::time::interval(Duration::from_secs(30)); + loop { + timer.tick().await; + let Ok(_) = sender.send("phoenix", "heartbeat", Empty {}).await else {break}; + } + }); + + futures_util::pin_mut!(process_messages, send_messages); + // processing messages should be quick otherwise it'd block sending messages. + // we could remove this limitation by spawning a separate taks for each of these. + let result = futures::future::select(process_messages, send_messages) + .await + .factor_first() + .0; + phoenix_heartbeat.abort(); + result?; + + Ok(()) + } + + #[tracing::instrument(level = "trace", skip(handler))] + async fn message_process(handler: &F, message: tungstenite::Message) { + tracing::trace!("{message:?}"); + + match message.into_text() { + Ok(m_str) => match serde_json::from_str::>(&m_str) { + Ok(m) => match m.payload { + Payload::Message(m) => handler(m).await, + Payload::PhoenixReply { status, .. } => { + // TODO: This could be an error + tracing::trace!("Recieved phoenix status message: {status}") + } + }, + Err(e) => { + tracing::error!("Error deserializing message {m_str}: {e:?}"); + } + }, + _ => tracing::error!("Recieved message that is not text"), + } + } + + /// Obtains a new sender that can be used to send message with this [PhoenixChannel] to the portal. + /// + /// Note that for the sender to relay any message will need the future returned [PhoenixChannel::start] to be polled (await it), + /// and [PhoenixChannel::start] takes `&mut self`, meaning you need to get the sender before running [PhoenixChannel::start]. + pub fn sender(&self) -> PhoenixSender { + PhoenixSender { + sender: self.sender.clone(), + } + } + + /// Creates a new [PhoenixChannel] not started yet. + /// + /// # Parameters: + /// - `uri`: Portal's websocket uri + /// - `handler`: The handle that will be called for each recieved message. + /// + /// For more info see [struct-level docs][PhoenixChannel]. + pub fn new(uri: Url, handler: F) -> Self { + let (sender, receiver) = channel(CHANNEL_SIZE); + + Self { + sender, + receiver, + uri, + handler, + _phantom: PhantomData, + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +#[serde(untagged)] +enum Payload { + // TODO: We should be able to extract a Result from this. + PhoenixReply { response: Empty, status: String }, + Message(T), +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +struct PhoenixMessage { + topic: String, + event: String, + payload: Payload, + #[serde(rename = "ref")] + reference: Option, +} + +impl PhoenixMessage { + fn new(topic: impl Into, event: impl Into, payload: T) -> Self { + Self { + topic: topic.into(), + event: event.into(), + payload: Payload::Message(payload), + reference: Some(0), + } + } +} + +// Awful hack to get serde_json to generate an empty "{}" instead of using "null" +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] +struct Empty {} + +/// You can use this sender to send messages through a `PhoenixChannel`. +/// +/// Messages won't be sent unless [PhoenixChannel::start] is running, internally +/// this sends messages through a future channel that are forwrarded then in [PhoenixChannel] event loop +pub struct PhoenixSender { + sender: Sender, +} + +impl PhoenixSender { + /// Sends a message upstream to a connected [PhoenixChannel]. + /// + /// # Parameters + /// - topic: Phoenix topic + /// - event: Phoenix event + /// - payload: Message's payload + pub async fn send( + &mut self, + topic: impl Into, + event: impl Into, + payload: impl Serialize, + ) -> Result<()> { + let str = serde_json::to_string(&PhoenixMessage::new(topic, event, payload))?; + self.sender.send(Message::text(str)).await?; + Ok(()) + } + + /// Join a phoenix topic, meaning that after this method is invoked [PhoenixChannel] will + /// recieve messages from that topic, given that upstream accepts you into the given topic. + pub async fn join_topic(&mut self, topic: impl Into) -> Result<()> { + self.send(topic, "phx_join", Empty {}).await + } + + /// Closes the [PhoenixChannel] + pub async fn close(&mut self) -> Result<()> { + self.sender.send(Message::Close(None)).await?; + self.sender.close().await?; + Ok(()) + } +} diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs new file mode 100644 index 0000000..3ff0b9c --- /dev/null +++ b/libs/common/src/error.rs @@ -0,0 +1,92 @@ +//! Error module. +use base64::{DecodeError, DecodeSliceError}; +use boringtun::noise::errors::WireGuardError; +use macros::SwiftEnum; +use thiserror::Error; + +/// Unified Result type to use across connlib. +pub type Result = std::result::Result; + +/// Unified error type to use across connlib. +#[derive(Error, Debug, SwiftEnum)] +pub enum ConnlibError { + /// Standard IO error. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Error while decoding a base64 value. + #[error("There was an error while decoding a base64 value: `{0}`")] + Base64DecodeError(#[from] DecodeError), + /// Error while decoding a base64 value from a slice. + #[error("There was an error while decoding a base64 value: `{0}`")] + Base64DecodeSliceError(#[from] DecodeSliceError), + /// Request error for websocket connection. + #[error("Error forming request: {0}")] + RequestError(#[from] tokio_tungstenite::tungstenite::http::Error), + /// Error during websocket connection. + #[error("Portal connection error: {0}")] + PortalConnectionError(#[from] tokio_tungstenite::tungstenite::error::Error), + /// Provided string was not formatted as a URL. + #[error("Baddly formatted URI")] + UriError, + /// Serde's serialize error. + #[error(transparent)] + SerializeError(#[from] serde_json::Error), + /// Webrtc errror + #[error("ICE-related error: {0}")] + IceError(#[from] webrtc::Error), + /// Webrtc error regarding data channel. + #[error("ICE-data error: {0}")] + IceDataError(#[from] webrtc::data::Error), + /// Error while sending through an async channelchannel. + #[error("Error sending message through an async channel")] + SendChannelError, + /// Error when trying to stablish connection between peers. + #[error("Error while stablishing connection between peers")] + ConnectionStablishError, + /// Error regarding boringtun's devices + #[error("Error while using boringtun's device")] + BoringtunError(#[from] boringtun::device::Error), + /// Error related to wireguard protocol. + #[error("Wireguard error")] + WireguardError(WireGuardError), + /// Expected an initialized runtime but there was none. + #[error("Expected runtime to be initialized")] + NoRuntime, + /// Tried to access a resource which didn't exists. + #[error("Tried to access an undefined resource")] + UnknownResource, + /// Error regarding our own control protocol. + #[error("Control plane protocol error. Unexpected messages or message order.")] + ControlProtocolError, + /// Glob for errors without a type. + #[error("Other error")] + Other(&'static str), +} + +/// Type auto-generated by [SwiftEnum] intended to be used with rust-swift-bridge. +/// All the variants come from [ConnlibError], reference that for documentaiton. +pub use swift_ffi::SwiftConnlibError; + +impl From for ConnlibError { + fn from(e: WireGuardError) -> Self { + ConnlibError::WireguardError(e) + } +} + +impl From<&'static str> for ConnlibError { + fn from(e: &'static str) -> Self { + ConnlibError::Other(e) + } +} + +impl From> for ConnlibError { + fn from(_: tokio::sync::mpsc::error::SendError) -> Self { + ConnlibError::SendChannelError + } +} + +impl From for ConnlibError { + fn from(_: futures::channel::mpsc::SendError) -> Self { + ConnlibError::SendChannelError + } +} diff --git a/libs/common/src/error_type.rs b/libs/common/src/error_type.rs new file mode 100644 index 0000000..7f411c8 --- /dev/null +++ b/libs/common/src/error_type.rs @@ -0,0 +1,20 @@ +//! Module that contains the Error-Type that hints how to handle an error to upper layers. +use macros::SwiftEnum; +/// This indicates whether the produced error is something recoverable or fatal. +/// Fata/Recoverable only indicates how to handle the error for the client. +/// +/// Any of the errors in [ConnlibError][crate::error::ConnlibError] could be of any [ErrorType] depending the circumstance. +#[derive(Debug, Clone, Copy, SwiftEnum)] +pub enum ErrorType { + /// Recoverable means that the session can continue + /// e.g. Failed to send an SDP + Recoverable, + /// Fatal error means that the session should stop and start again, + /// generally after user input, such as clicking connect once more. + /// e.g. Max number of retries was reached when trying to connect to the portal. + Fatal, +} + +/// Auto generated enum by [SwiftEnum], all variants come from [ErrorType] +/// reference that for docs. +pub use swift_ffi::SwiftErrorType; diff --git a/libs/common/src/lib.rs b/libs/common/src/lib.rs new file mode 100644 index 0000000..2d4c0fe --- /dev/null +++ b/libs/common/src/lib.rs @@ -0,0 +1,18 @@ +//! This crates contains shared types and behavior between all the other libraries. +//! +//! This includes types provided by external crates, i.e. [boringtun] to make sure that +//! we are using the same version across our own crates. + +pub mod error; +pub mod error_type; + +mod session; + +pub mod control; +pub mod messages; + +pub use boringtun; +pub use error::ConnlibError as Error; +pub use error::Result; + +pub use session::{Callbacks, ControlSession, ResourceList, Session, TunnelAddresses}; diff --git a/libs/common/src/messages.rs b/libs/common/src/messages.rs new file mode 100644 index 0000000..5e60c06 --- /dev/null +++ b/libs/common/src/messages.rs @@ -0,0 +1,75 @@ +//! Message types that are used by both the gateway and client. +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; + +mod key; + +pub use key::Key; + +/// General type for handling portal's id (UUID v4) +pub type Id = Uuid; + +/// Represents a wireguard peer. +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct Peer { + /// Keepalive: How often to send a keep alive message. + pub persistent_keepalive: Option, + /// Peer's public key. + pub public_key: Key, + /// Peer's Ipv4 (only 1 ipv4 per peer for now and mandatory). + pub ipv4: Ipv4Addr, + /// Peer's Ipv6 (only 1 ipv6 per peer for now and mandatory). + pub ipv6: Ipv6Addr, + /// Preshared key for the given peer. + pub preshared_key: Key, +} + +/// Represent a connection request from a client to a given resource. +/// +/// While this is a client-only message it's hosted in common since the tunnel +/// make use of this message type. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RequestConnection { + /// Resource id the request is for. + pub resource_id: Id, + /// The preshared key the client generated for the connection that it is trying to establish. + pub client_preshared_key: Key, + /// Client's local RTC Session Description that the client will use for this connection. + pub client_rtc_sdp: RTCSessionDescription, +} + +/// Description of a resource from a client's perspective. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ResourceDescription { + /// Resource's id. + pub id: Id, + /// Internal resource's domain name if any. + pub dns_name: Option, + /// Resource's ipv4 mapping. + /// + /// Note that this is not the actual ipv4 for the resource not even wireguard's ipv4 for the resource. + /// This is just the mapping we use internally between a resource and its ip for intercepting packets. + pub ipv4: Ipv4Addr, + /// Resource's ipv6 mapping. + /// + /// Note that this is not the actual ipv6 for the resource not even wireguard's ipv6 for the resource. + /// This is just the mapping we use internally between a resource and its ip for intercepting packets. + pub ipv6: Ipv6Addr, +} + +/// Represents a wireguard interface configuration. +/// +/// Note that the ips are /32 for ipv4 and /128 for ipv6. +/// This is done to minimize collisions and we update the routing table manually. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Interface { + /// Interface's Ipv4. + pub ipv4: Ipv4Addr, + /// Interface's Ipv6. + pub ipv6: Ipv6Addr, + /// DNS that will be used to query for DNS that aren't within our resource list. + pub upstream_dns: Vec, +} diff --git a/libs/common/src/messages/key.rs b/libs/common/src/messages/key.rs new file mode 100644 index 0000000..efdae4f --- /dev/null +++ b/libs/common/src/messages/key.rs @@ -0,0 +1,54 @@ +use base64::{display::Base64Display, engine::general_purpose::STANDARD, Engine}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use std::{fmt, str::FromStr}; + +use crate::Error; + +const KEY_SIZE: usize = 32; + +/// A `Key` struct to hold interface or peer keys as bytes. This type is +/// deserialized from a base64 encoded string. It can also be serialized back +/// into an encoded string. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Key(pub [u8; KEY_SIZE]); + +impl FromStr for Key { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut key_bytes = [0u8; KEY_SIZE]; + let bytes_decoded = STANDARD.decode_slice(s, &mut key_bytes)?; + + if bytes_decoded != KEY_SIZE { + Err(base64::DecodeError::InvalidLength)?; + } + + Ok(Self(key_bytes)) + } +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", Base64Display::new(&self.0, &STANDARD)) + } +} + +impl<'de> Deserialize<'de> for Key { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + +impl Serialize for Key { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(&self) + } +} diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs new file mode 100644 index 0000000..a19a930 --- /dev/null +++ b/libs/common/src/session.rs @@ -0,0 +1,219 @@ +use async_trait::async_trait; +use backoff::{backoff::Backoff, ExponentialBackoffBuilder}; +use boringtun::x25519::StaticSecret; +use rand_core::OsRng; +use std::{ + marker::PhantomData, + net::{Ipv4Addr, Ipv6Addr}, +}; +use tokio::{ + runtime::Runtime, + sync::mpsc::{Receiver, Sender}, +}; +use url::Url; + +use crate::{control::PhoenixChannel, error_type::ErrorType, Error, Result}; + +// TODO: Not the most tidy trait for a control-plane. +/// Trait that represents a control-plane. +#[async_trait] +pub trait ControlSession { + /// Start control-plane with the given private-key in the background. + async fn start(private_key: StaticSecret) -> Result<(Sender, Receiver)>; + + /// Either "gateway" or "client" used to ge the control-plane URL. + fn mode() -> &'static str; +} + +// TODO: Currently I'm using Session for both gateway and clients +// however, gateway could use the runtime directly and could make things easier +// so revisit this. +/// A session is the entry-point for connlib, mantains the runtime and the tunnel. +/// +/// A session is created using [Session::connect], then to stop a session we use [Session::disconnect]. +pub struct Session { + runtime: Option, + _phantom: PhantomData<(T, U, V)>, +} + +/// Resource list that will be displayed to the users. +pub struct ResourceList { + pub resources: Vec, +} + +/// Tunnel addresses to be surfaced to the client apps. +pub struct TunnelAddresses { + /// IPv4 Address. + pub address4: Ipv4Addr, + /// IPv6 Address. + pub address6: Ipv6Addr, +} + +// Evaluate doing this not static +/// Traits that will be used by connlib to callback the client upper layers. +pub trait Callbacks { + /// Called when there's a change in the resource list. + fn on_update_resources(resource_list: ResourceList); + /// Called when the tunnel address is set. + fn on_set_tunnel_adresses(tunnel_addresses: TunnelAddresses); + /// Called when there's an error. + /// + /// # Parameters + /// - `error`: The actual error that happened. + /// - `error_type`: Wether the error should terminate the session or not. + fn on_error(error: &Error, error_type: ErrorType); +} + +macro_rules! fatal_error { + ($result:expr, $c:ty) => { + match $result { + Ok(res) => res, + Err(e) => { + <$c>::on_error(&e, ErrorType::Fatal); + return; + } + } + }; +} + +impl Session +where + T: ControlSession, + U: for<'de> serde::Deserialize<'de> + std::fmt::Debug + Send + 'static, + V: serde::Serialize + Send + 'static, +{ + /// Block on waiting for ctrl+c to terminate the runtime. + /// (Used for the gateways). + pub fn wait_for_ctrl_c(&mut self) -> Result<()> { + self.runtime + .as_ref() + .ok_or(Error::NoRuntime)? + .block_on(async { + tokio::signal::ctrl_c().await?; + Ok(()) + }) + } + + /// Starts a session in the background. + /// + /// This will: + /// 1. Create and start a tokio runtime + /// 2. Connect to the control plane to the portal + /// 3. Start the tunnel in the background and forward control plane messages to it. + /// + /// The generic parameter `C` should implement all the handlers and that's how errors will be surfaced. + /// + /// On a fatal error you should call `[Session::disconnect]` and start a new one. + pub fn connect(portal_url: impl TryInto, token: String) -> Result { + // TODO: We could use tokio::runtime::current() to get the current runtime + // which could work with swif-rust that already runs a runtime. But IDK if that will work + // in all pltaforms, a couple of new threads shouldn't bother none. + // Big question here however is how do we get the result? We could block here await the result and spawn a new task. + // but then platforms should know that this function is blocking. + + let portal_url = portal_url.try_into().map_err(|_| Error::UriError)?; + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + + runtime.spawn(async move { + let private_key = StaticSecret::random_from_rng(OsRng); + + let (sender, mut receiver) = fatal_error!(T::start(private_key).await, C); + + let connect_url = fatal_error!(get_websocket_path(portal_url, token, T::mode()), C); + + let mut connection = PhoenixChannel::new(connect_url, move |msg| { + let sender = sender.clone(); + async move { + tracing::trace!("Recieved message: {msg:?}"); + if let Err(e) = sender.send(msg).await { + tracing::warn!("Recieved a message after handler already closed: {e}. Probably message recieved during session clean up."); + } + } + }); + + let mut internal_sender = connection.sender(); + + tokio::spawn(async move { + let mut exponential_backoff = ExponentialBackoffBuilder::default().build(); + loop { + let result = connection.start().await; + if let Some(t) = exponential_backoff.next_backoff() { + tracing::warn!("Error during connection to the portal, retrying in {} seconds", t.as_secs()); + tokio::time::sleep(t).await; + match result { + Ok(()) => C::on_error(&tokio_tungstenite::tungstenite::Error::ConnectionClosed.into(), ErrorType::Recoverable), + Err(e) => C::on_error(&e.into(), ErrorType::Recoverable) + } + } else { + tracing::error!("Conneciton to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); + match result { + Ok(()) => C::on_error(&crate::Error::PortalConnectionError(tokio_tungstenite::tungstenite::Error::ConnectionClosed), ErrorType::Fatal), + Err(e) => C::on_error(&crate::Error::PortalConnectionError(e), ErrorType::Fatal) + } + break; + } + } + + }); + + // TODO: Implement Sink for PhoenixEvent (created from a PhoenixSender event + topic) + // that way we can simply do receiver.forward(sender) + tokio::spawn(async move { + while let Some(message) = receiver.recv().await { + if let Err(err) = internal_sender.send("TODO", "TODO", message).await { + tracing::error!("Channel already closed when trying to send message: {err}. Probably trying to send a message during session clean up."); + } + } + }); + }); + + Ok(Self { + runtime: Some(runtime), + _phantom: PhantomData, + }) + } + + /// Cleanup a [Session]. + /// + /// For now this just drops the runtime, which should drop all pending tasks. + /// Further cleanup should be done here. (Otherwise we can just drop [Session]). + pub fn disconnect(&mut self) -> bool { + // 1. Close the websocket connection + // 2. Free the device handle (UNIX) + // 3. Close the file descriptor (UNIX) + // 4. Remove the mapping + + // The way we cleanup the tasks is we drop the runtime + // this means we don't need to keep track of different tasks + // but if any of the tasks never yields this will block forever! + // So always yield and if you spawn a blocking tasks rewrite this. + // Furthermore, we will depend on Drop impls to do the list above so, + // implement them :) + self.runtime = None; + true + } + + /// TODO + pub fn bump_sockets(&self) -> bool { + true + } + + /// TODO + pub fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { + true + } +} + +fn get_websocket_path(mut url: Url, secret: String, mode: &str) -> Result { + { + let mut paths = url.path_segments_mut().map_err(|_| Error::UriError)?; + paths.pop_if_empty(); + paths.push(mode); + paths.push("websocket"); + } + url.set_query(Some(&format!("secret={secret}"))); + Ok(url) +} diff --git a/libs/gateway/Cargo.toml b/libs/gateway/Cargo.toml new file mode 100644 index 0000000..2a5e4b9 --- /dev/null +++ b/libs/gateway/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "firezone-gateway-connlib" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libs-common = { path = "../common" } +async-trait = { version = "0.1", default-features = false } +firezone-tunnel = { path = "../tunnel" } +tokio = { version = "1.27", default-features = false, features = ["sync"] } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +serde = { version = "1.0", default-features = false, features = ["std", "derive"] } diff --git a/libs/gateway/src/control.rs b/libs/gateway/src/control.rs new file mode 100644 index 0000000..d2e6428 --- /dev/null +++ b/libs/gateway/src/control.rs @@ -0,0 +1,146 @@ +use std::{sync::Arc, time::Duration}; + +use firezone_tunnel::{ControlSignal, Tunnel}; +use libs_common::{ + boringtun::x25519::StaticSecret, messages::ResourceDescription, Callbacks, ControlSession, + Result, +}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; + +use super::messages::{ + ConnectionReady, ConnectionRequest, EgressMessages, IngressMessages, InitGateway, Resource, +}; + +use async_trait::async_trait; + +const INTERNAL_CHANNEL_SIZE: usize = 256; + +pub struct ControlPlane { + tunnel: Arc>, + control_signaler: ControlSignaler, +} + +#[derive(Clone)] +struct ControlSignaler { + internal_sender: Arc>, +} + +#[async_trait] +impl ControlSignal for ControlSignaler { + async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()> { + tracing::warn!("A message to network resource: {resource:?} was discarded, gateways aren't meant to be used as clients."); + Ok(()) + } +} + +impl ControlPlane +where + C: Send + Sync + 'static, +{ + #[tracing::instrument(level = "trace", skip(self))] + async fn start(mut self, mut receiver: Receiver) { + let mut interval = tokio::time::interval(Duration::from_secs(10)); + loop { + tokio::select! { + Some(msg) = receiver.recv() => self.handle_message(msg).await, + _ = interval.tick() => self.stats_event().await, + else => break + } + } + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn init(&mut self, init: InitGateway) { + self.tunnel + .set_interface(&init.interface) + .await + .expect("Couldn't start tunnel"); + + // TODO: Enable masquerading here. + tracing::info!("Firezoned Started!"); + } + + #[tracing::instrument(level = "trace", skip(self))] + fn connection_request(&self, connection_request: ConnectionRequest) { + let tunnel = Arc::clone(&self.tunnel); + let control_signaler = self.control_signaler.clone(); + tokio::spawn(async move { + let gateway_rtc_sdp = tunnel + .set_peer_connection_request( + connection_request.rtc_sdp, + connection_request.client.peer.into(), + connection_request.relays, + connection_request.client.id, + ) + .await + .expect("TODO"); + control_signaler + .internal_sender + .send(EgressMessages::ConnectionReady(ConnectionReady { + client_id: connection_request.client.id, + gateway_rtc_sdp, + })) + .await + .expect("TODO!"); + }); + } + + #[tracing::instrument(level = "trace", skip(self))] + fn add_resource(&self, resource: Resource) { + todo!() + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn handle_message(&mut self, msg: IngressMessages) { + match msg { + IngressMessages::InitGateway(init) => self.init(init).await, + IngressMessages::ConnectionRequest(connection_request) => { + self.connection_request(connection_request) + } + IngressMessages::AddResource(resource) => self.add_resource(resource), + IngressMessages::RemoveResource(_) => todo!(), + IngressMessages::UpdateResource(_) => todo!(), + } + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn stats_event(&mut self) { + tracing::debug!("TODO: STATS EVENT"); + } +} + +#[async_trait] +impl ControlSession for ControlPlane +where + C: Send + Sync + 'static, +{ + #[tracing::instrument(level = "trace", skip(private_key))] + async fn start( + private_key: StaticSecret, + ) -> Result<(Sender, Receiver)> { + // This is kinda hacky, the buffer size is 1 so that we make sure that we + // process one message at a time, blocking if a previous message haven't been processed + // to force queue ordering. + // (couldn't find any other guarantee of the ordering of message) + let (sender, receiver) = channel::(1); + + let (internal_sender, internal_receiver) = channel(INTERNAL_CHANNEL_SIZE); + let internal_sender = Arc::new(internal_sender); + let control_signaler = ControlSignaler { internal_sender }; + let tunnel = Arc::new(Tunnel::<_, C>::new(private_key, control_signaler.clone()).await?); + + let control_plane = ControlPlane { + tunnel, + control_signaler, + }; + + // TODO: We should have some kind of callback from clients to surface errors here + tokio::spawn(async move { control_plane.start(receiver).await }); + + Ok((sender, internal_receiver)) + } + + fn mode() -> &'static str { + "client" + } +} diff --git a/libs/gateway/src/lib.rs b/libs/gateway/src/lib.rs new file mode 100644 index 0000000..3ad7fb4 --- /dev/null +++ b/libs/gateway/src/lib.rs @@ -0,0 +1,16 @@ +//! Main connlib library for gateway. +use control::ControlPlane; +use messages::EgressMessages; +use messages::IngressMessages; + +mod control; +mod messages; + +const VIRTUAL_IFACE_MTU: u16 = 1420; + +/// Session type for gateway. +/// +/// For more information see libs_common docs on [Session][libs_common::Session]. +pub type Session = libs_common::Session, IngressMessages, EgressMessages>; + +pub use libs_common::{error_type::ErrorType, Callbacks, Error, ResourceList, TunnelAddresses}; diff --git a/libs/gateway/src/messages.rs b/libs/gateway/src/messages.rs new file mode 100644 index 0000000..f67425a --- /dev/null +++ b/libs/gateway/src/messages.rs @@ -0,0 +1,87 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use firezone_tunnel::RTCSessionDescription; +use libs_common::messages::{Id, Interface, Peer}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct InitGateway { + pub interface: Interface, + pub ipv4_masquerade: bool, + pub ipv6_masquerade: bool, + pub resources: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Client { + pub id: Id, + pub peer: Peer, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ConnectionRequest { + pub user_id: Id, + pub client: Client, + pub rtc_sdp: RTCSessionDescription, + pub relays: Vec, + pub resource: Resource, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub enum Destination { + DnsName(String), + Ip(Vec), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Resource { + pub id: Id, + pub internal_ipv4: Option, + pub internal_ipv6: Option, + pub resource_address: Destination, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Metrics { + peers_metrics: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Metric { + pub client_id: Id, + pub resource_id: Id, + pub rx_bytes: u32, + pub tx_bytes: u32, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RemoveResource { + pub id: Id, +} + +// These messages are the messages that can be recieved +// either by a client or a gateway by the client. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum IngressMessages { + InitGateway(InitGateway), + ConnectionRequest(ConnectionRequest), + AddResource(Resource), + RemoveResource(RemoveResource), + UpdateResource(Resource), +} + +// These messages can be sent from a gateway +// to a control pane. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum EgressMessages { + ConnectionReady(ConnectionReady), + Metrics(Metrics), +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ConnectionReady { + pub client_id: Id, + pub gateway_rtc_sdp: RTCSessionDescription, +} diff --git a/libs/tunnel/Cargo.toml b/libs/tunnel/Cargo.toml new file mode 100644 index 0000000..ce513fe --- /dev/null +++ b/libs/tunnel/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "firezone-tunnel" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-trait = { version = "0.1", default-features = false } +tokio = { version = "1.27", default-features = false, features = ["rt", "rt-multi-thread", "sync"] } +thiserror = { version = "1.0", default-features = false } +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +serde = { version = "1.0", default-features = false, features = ["derive", "std"] } +futures = { version = "0.3", default-features = false, features = ["std", "async-await", "executor"] } +futures-util = { version = "0.3", default-features = false, features = ["std", "async-await", "async-await-macro"] } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +parking_lot = { version = "0.12", default-features = false } +bytes = { version = "1.4", default-features = false, features = ["std"] } +itertools = { version = "0.10", default-features = false, features = ["use_std"] } + +# TODO: research replacing for https://github.com/algesten/str0m +webrtc = { version = "0.7" } + +# Apple tunnel dependencies +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +libs-common = { path = "../common", features = ["device"] } + +# Linux tunnel dependencies +[target.'cfg(target_os = "linux")'.dependencies] +libs-common = { path = "../common", features = ["device"] } + +# Android tunnel dependencies +[target.'cfg(target_os = "android")'.dependencies] +libs-common = { path = "../common", features = ["jni-bindings"] } +android_logger = "0.13" +log = "0.4.14" + +# Windows tunnel dependencies +[target.'cfg(target_os = "windows")'.dependencies] +libs-common = { path = "../common" } +wintun = "0.2.1" diff --git a/libs/tunnel/src/control_protocol.rs b/libs/tunnel/src/control_protocol.rs new file mode 100644 index 0000000..53f7b3e --- /dev/null +++ b/libs/tunnel/src/control_protocol.rs @@ -0,0 +1,302 @@ +use std::sync::Arc; + +use libs_common::{ + boringtun::{ + noise::Tunn, + x25519::{PublicKey, StaticSecret}, + }, + error_type::ErrorType::Recoverable, + messages::{Id, Key, RequestConnection}, + Callbacks, Error, Result, +}; +use rand_core::OsRng; +use webrtc::{ + data_channel::RTCDataChannel, + ice_transport::ice_server::RTCIceServer, + peer_connection::{ + configuration::RTCConfiguration, peer_connection_state::RTCPeerConnectionState, + sdp::session_description::RTCSessionDescription, RTCPeerConnection, + }, +}; + +use crate::{peer::Peer, ControlSignal, PeerConfig, Tunnel}; + +impl Tunnel +where + C: Send + Sync + 'static, + CB: Send + Sync + 'static, +{ + async fn handle_channel_open( + self: &Arc, + data_channel: Arc, + index: u32, + peer_config: PeerConfig, + ) -> Result<()> { + let channel = data_channel.detach().await.expect("TODO"); + let tunn = Tunn::new( + self.private_key.clone(), + peer_config.public_key, + Some(peer_config.preshared_key.to_bytes()), + peer_config.persistent_keepalive, + index, + None, + )?; + + let peer = Arc::new(Peer::from_config( + tunn, + index, + &peer_config, + Arc::clone(&channel), + )); + + { + let mut peers_by_ip = self.peers_by_ip.write(); + peers_by_ip.insert(peer_config.ipv4.into(), Arc::clone(&peer)); + peers_by_ip.insert(peer_config.ipv6.into(), Arc::clone(&peer)); + } + + self.start_peer_handler(Arc::clone(&peer)); + Ok(()) + } + + #[tracing::instrument(level = "trace", skip(self))] + async fn initialize_peer_request( + self: &Arc, + relays: Vec, + ) -> Result> { + let config = RTCConfiguration { + ice_servers: relays + .into_iter() + .map(|srv| RTCIceServer { + urls: vec![srv], + ..Default::default() + }) + .collect(), + ..Default::default() + }; + let peer_connection = Arc::new(self.webrtc_api.new_peer_connection(config).await?); + + peer_connection.on_peer_connection_state_change(Box::new(|_s| { + Box::pin(async { + // Respond with failure to control plane and remove peer + }) + })); + + Ok(peer_connection) + } + + #[tracing::instrument(level = "trace", skip(self))] + fn handle_connection_state_update(self: &Arc, state: RTCPeerConnectionState) { + tracing::trace!("Peer Connection State has changed: {state}"); + if state == RTCPeerConnectionState::Failed { + // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. + // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. + // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. + tracing::warn!("Peer Connection has gone to failed exiting"); + } + } + + #[tracing::instrument(level = "trace", skip(self))] + fn set_connection_state_update(self: &Arc, peer_connection: &Arc) { + let tunnel = Arc::clone(self); + peer_connection.on_peer_connection_state_change(Box::new( + move |state: RTCPeerConnectionState| { + let tunnel = Arc::clone(&tunnel); + Box::pin(async move { tunnel.handle_connection_state_update(state) }) + }, + )); + } + + /// Initiate an ice connection request. + /// + /// Given a resource id and a list of relay creates a [RequestConnection] + /// and prepares the tunnel to handle the connection once initiated. + /// + /// # Note + /// This function blocks until all ICE candidates are gathered so it might block for a long time. + /// + /// # Parameters + /// - `resource_id`: Id of the resource we are going to request the connection to. + /// - `relays`: The list of relays used for that connection. + /// + /// # Returns + /// A [RequestConnection] that should be sent to the gateway through the control-plane. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn request_connection( + self: &Arc, + resource_id: Id, + relays: Vec, + ) -> Result { + let peer_connection = self.initialize_peer_request(relays).await?; + self.set_connection_state_update(&peer_connection); + + let data_channel = peer_connection.create_data_channel("data", None).await?; + let d = Arc::clone(&data_channel); + + let tunnel = Arc::clone(self); + + let preshared_key = StaticSecret::random_from_rng(OsRng); + let p_key = preshared_key.clone(); + let resource_description = tunnel + .resources + .read() + .get_main(&resource_id) + .expect("TODO") + .clone(); + data_channel.on_open(Box::new(move || { + tracing::trace!("new data channel opened!"); + Box::pin(async move { + let index = tunnel.next_index(); + let Some(gateway_public_key) = tunnel.gateway_public_keys.lock().remove(&resource_id) else { + tunnel.cleanup_connection(resource_id); + tracing::warn!("Opened ICE channel with gateway without ever recieving public key"); + CB::on_error(&Error::ControlProtocolError, Recoverable); + return; + }; + let peer_config = { + PeerConfig { + persistent_keepalive: None, + public_key: gateway_public_key, + ipv4: resource_description.ipv4, + ipv6: resource_description.ipv6, + preshared_key: p_key, + } + }; + + if let Err(e) = tunnel.handle_channel_open(d, index, peer_config).await { + tracing::error!("Couldn't stablish wireguard link after channel was opened: {e}"); + CB::on_error(&e, Recoverable); + tunnel.cleanup_connection(resource_id); + } + tunnel.awaiting_connection.lock().remove(&resource_id); + }) + })); + + let offer = peer_connection.create_offer(None).await?; + let mut gather_complete = peer_connection.gathering_complete_promise().await; + peer_connection.set_local_description(offer).await?; + + // FIXME: timeout here! (but probably don't even bother because we need to implement ICE trickle) + let _ = gather_complete.recv().await; + let local_description = peer_connection + .local_description() + .await + .expect("set_local_description was just called above"); + + self.peer_connections + .lock() + .insert(resource_id, peer_connection); + + Ok(RequestConnection { + resource_id, + client_preshared_key: Key(preshared_key.to_bytes()), + client_rtc_sdp: local_description, + }) + } + + /// Called when a response to [Tunnel::request_connection] is ready. + /// + /// Once this is called if everything goes fine a new tunnel should be started between the 2 peers. + /// + /// # Parameters + /// - `resource_id`: Id of the resource that responded. + /// - `rtc_sdp`: Remote SDP. + /// - `gateway_public_key`: Public key of the gateway that is handling that resource for this connection. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn recieved_offer_response( + self: &Arc, + resource_id: Id, + rtc_sdp: RTCSessionDescription, + gateway_public_key: PublicKey, + ) -> Result<()> { + let peer_connection = self + .peer_connections + .lock() + .get(&resource_id) + .ok_or(Error::UnknownResource)? + .clone(); + self.gateway_public_keys + .lock() + .insert(resource_id, gateway_public_key); + peer_connection.set_remote_description(rtc_sdp).await?; + Ok(()) + } + + /// Accept a connection request from a client. + /// + /// Sets a connection to a remote SDP, creates the local SDP + /// and returns it. + /// + /// # Note + /// + /// This function blocks until it gathers all the ICE candidates + /// so it might block for a long time. + /// + /// # Parameters + /// - `sdp_session`: Remote session description. + /// - `peer`: Configuration for the remote peer. + /// - `relays`: List of relays to use with this connection. + /// - `client_id`: UUID of the remote client. + /// + /// # Returns + /// An [RTCSessionDescription] of the local sdp, with candidates gathered. + pub async fn set_peer_connection_request( + self: &Arc, + sdp_session: RTCSessionDescription, + peer: PeerConfig, + relays: Vec, + client_id: Id, + ) -> Result { + let peer_connection = self.initialize_peer_request(relays).await?; + let index = self.next_index(); + let tunnel = Arc::clone(self); + self.peer_connections + .lock() + .insert(client_id, Arc::clone(&peer_connection)); + + self.set_connection_state_update(&peer_connection); + + peer_connection.on_data_channel(Box::new(move |d| { + tracing::trace!("data channel created!"); + let data_channel = Arc::clone(&d); + let peer = peer.clone(); + let tunnel = Arc::clone(&tunnel); + Box::pin(async move { + d.on_open(Box::new(move || { + tracing::trace!("new data channel opened!"); + Box::pin(async move { + if let Err(e) = tunnel.handle_channel_open(data_channel, index, peer).await + { + CB::on_error(&e, Recoverable); + tracing::error!( + "Couldn't stablish wireguard link after opening channel: {e}" + ); + // Note: handle_channel_open can only error out before insert to peers_by_ip + // otherwise we would need to clean that up too! + tunnel.peer_connections.lock().remove(&client_id); + } + }) + })) + }) + })); + + peer_connection.set_remote_description(sdp_session).await?; + + let mut gather_complete = peer_connection.gathering_complete_promise().await; + let answer = peer_connection.create_answer(None).await?; + peer_connection.set_local_description(answer).await?; + let _ = gather_complete.recv().await; + let local_desc = peer_connection + .local_description() + .await + .ok_or(Error::ConnectionStablishError)?; + + Ok(local_desc) + } + + /// Clean up a connection to a resource. + pub fn cleanup_connection(&self, resource_id: Id) { + self.awaiting_connection.lock().remove(&resource_id); + self.peer_connections.lock().remove(&resource_id); + } +} diff --git a/libs/tunnel/src/index.rs b/libs/tunnel/src/index.rs new file mode 100644 index 0000000..f03a89c --- /dev/null +++ b/libs/tunnel/src/index.rs @@ -0,0 +1,61 @@ +use rand_core::{OsRng, RngCore}; + +// Taken from boringtun (todo) +/// A basic linear-feedback shift register implemented as xorshift, used to +/// distribute peer indexes across the 24-bit address space reserved for peer +/// identification. +/// The purpose is to obscure the total number of peers using the system and to +/// ensure it requires a non-trivial amount of processing power and/or samples +/// to guess other peers' indices. Anything more ambitious than this is wasted +/// with only 24 bits of space. +pub(crate) struct IndexLfsr { + initial: u32, + lfsr: u32, + mask: u32, +} + +impl IndexLfsr { + /// Generate a random 24-bit nonzero integer + fn random_index() -> u32 { + const LFSR_MAX: u32 = 0xffffff; // 24-bit seed + loop { + let i = OsRng.next_u32() & LFSR_MAX; + if i > 0 { + // LFSR seed must be non-zero + return i; + } + } + } + + /// Generate the next value in the pseudorandom sequence + pub(crate) fn next(&mut self) -> u32 { + // 24-bit polynomial for randomness. This is arbitrarily chosen to + // inject bitflips into the value. + const LFSR_POLY: u32 = 0xd80000; // 24-bit polynomial + let value = self.lfsr - 1; // lfsr will never have value of 0 + self.lfsr = (self.lfsr >> 1) ^ ((0u32.wrapping_sub(self.lfsr & 1u32)) & LFSR_POLY); + assert!(self.lfsr != self.initial, "Too many peers created"); + value ^ self.mask + } +} + +impl Default for IndexLfsr { + fn default() -> Self { + let seed = Self::random_index(); + IndexLfsr { + initial: seed, + lfsr: seed, + mask: Self::random_index(), + } + } +} + +// Checks that a packet has the index we expect +pub(crate) fn check_packet_index(recv_idx: u32, expected_idx: u32) -> bool { + if (recv_idx >> 8) == expected_idx { + true + } else { + tracing::warn!("receiver index doesn't match peer index, something fishy is going on"); + false + } +} diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs new file mode 100644 index 0000000..41e7deb --- /dev/null +++ b/libs/tunnel/src/lib.rs @@ -0,0 +1,487 @@ +//! Connlib tunnel implementation. +//! +//! This is both the wireguard and ICE implementation that should work in tandem. +//! [Tunnel] is the main entry-point for this crate. +use libs_common::{ + boringtun::{ + noise::{ + errors::WireGuardError, handshake::parse_handshake_anon, rate_limiter::RateLimiter, + Packet, Tunn, TunnResult, + }, + x25519::{PublicKey, StaticSecret}, + }, + error_type::ErrorType::Recoverable, + Callbacks, +}; + +use async_trait::async_trait; +use bytes::Bytes; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock}; +use peer::Peer; +use tokio::time::MissedTickBehavior; +use webrtc::{ + api::{ + interceptor_registry::register_default_interceptors, media_engine::MediaEngine, + setting_engine::SettingEngine, APIBuilder, API, + }, + interceptor::registry::Registry, + peer_connection::RTCPeerConnection, +}; + +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, + marker::PhantomData, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::Arc, + time::Duration, +}; + +use libs_common::{ + messages::{Id, Interface as InterfaceConfig, ResourceDescription}, + Result, +}; + +use self::tun_device::DeviceChannel; +use self::tun_device::IfaceDevice; + +pub use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; + +use index::{check_packet_index, IndexLfsr}; +use multimap::MultiMap; + +mod control_protocol; +mod index; +mod multimap; +mod peer; + +#[cfg(target_os = "linux")] +#[path = "tun_linux.rs"] +mod tun_device; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[path = "tun_darwin.rs"] +mod tun_device; + +const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1); +const REFRESH_PEERS_TIEMRS_INTERVAL: Duration = Duration::from_secs(1); + +// Note: Taken from boringtun +const HANDSHAKE_RATE_LIMIT: u64 = 100; +const MAX_UDP_SIZE: usize = (1 << 16) - 1; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum ResourceKind { + _Name(String), + Addr(T), +} + +/// Represent's the tunnel actual peer's config +/// Obtained from libs_common's Peer +#[derive(Clone)] +pub struct PeerConfig { + pub(crate) persistent_keepalive: Option, + pub(crate) public_key: PublicKey, + pub(crate) ipv4: Ipv4Addr, + pub(crate) ipv6: Ipv6Addr, + pub(crate) preshared_key: StaticSecret, +} + +impl From for PeerConfig { + fn from(value: libs_common::messages::Peer) -> Self { + Self { + persistent_keepalive: value.persistent_keepalive, + public_key: value.public_key.0.into(), + ipv4: value.ipv4, + ipv6: value.ipv6, + preshared_key: value.preshared_key.0.into(), + } + } +} + +/// Trait used for out-going signals to control plane that are **required** to be made from inside the tunnel. +/// +/// Generally, we try to return from the functions here rather than using this callback. +#[async_trait] +pub trait ControlSignal { + /// Signals to the control plane an intent to initiate a connecti to the given resource. + /// + /// Used when a packet is found to a resource we have no connection stablished but is within the list of resources available for the client. + async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()>; +} + +/// Tunnel is a wireguard state machine that uses webrtc's ICE channels instead of UDP sockets +/// to communicate between peers. +pub struct Tunnel { + next_index: Mutex, + iface_device: Mutex, + device_channel: Arc, + rate_limiter: Arc, + private_key: StaticSecret, + public_key: PublicKey, + peers_by_ip: RwLock>>, + peer_connections: Mutex>>, + awaiting_connection: Mutex>, + webrtc_api: API, + resources: + RwLock, ResourceKind, ResourceDescription>>, + control_signaler: C, + gateway_public_keys: Mutex>, + _phantom: PhantomData, +} + +impl Tunnel +where + C: Send + Sync + 'static, + CB: Send + Sync + 'static, +{ + /// Creates a new tunnel. + /// + /// # Parameters + /// - `private_key`: wireguard's private key. + /// - `control_signaler`: this is used to send SDP from the tunnel to the control plane. + #[tracing::instrument(level = "trace", skip(private_key, control_signaler))] + pub async fn new(private_key: StaticSecret, control_signaler: C) -> Result { + let public_key = (&private_key).into(); + let rate_limiter = Arc::new(RateLimiter::new(&public_key, HANDSHAKE_RATE_LIMIT)); + let peers_by_ip = Default::default(); + let next_index = Default::default(); + let (iface_device, device_channel) = tun_device::create_iface().await?; + let iface_device = Mutex::new(iface_device); + let device_channel = Arc::new(device_channel); + let peer_connections = Default::default(); + let resources = Default::default(); + let awaiting_connection = Default::default(); + let gateway_public_keys = Default::default(); + + // ICE + let mut media_engine = MediaEngine::default(); + + // Register default codecs (TODO: We need this?) + media_engine.register_default_codecs()?; + let mut registry = Registry::new(); + registry = register_default_interceptors(registry, &mut media_engine)?; + let mut setting_engine = SettingEngine::default(); + setting_engine.detach_data_channels(); + // TODO: Enable UDPMultiplex (had some problems before) + + let webrtc_api = APIBuilder::new() + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .with_setting_engine(setting_engine) + .build(); + + Ok(Self { + gateway_public_keys, + rate_limiter, + private_key, + peer_connections, + public_key, + peers_by_ip, + next_index, + webrtc_api, + iface_device, + device_channel, + resources, + awaiting_connection, + control_signaler, + _phantom: PhantomData, + }) + } + + /// Adds a the given resource to the tunnel. + /// + /// Once added, when a packet for the resource is intercepted a new data channel will be created + /// and packets will be wrapped with wireguard and sent through it. + #[tracing::instrument(level = "trace", skip(self))] + pub fn add_resource(&self, resource_description: ResourceDescription) { + let mut resources = self.resources.write(); + resources.insert( + resource_description.id, + Some(ResourceKind::Addr(resource_description.ipv4)), + Some(ResourceKind::Addr(resource_description.ipv6)), + resource_description, + ); + } + + /// Sets the interface configuration and starts background tasks. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn set_interface(self: &Arc, config: &InterfaceConfig) -> Result<()> { + { + let mut iface_device = self.iface_device.lock(); + iface_device + .set_iface_config(config) + .expect("Couldn't initiate interface"); + iface_device.up().expect("Couldn't initiate interface"); + } + + self.start_timers(); + self.start_iface_handler(); + + tracing::trace!("Started background loops"); + + Ok(()) + } + + async fn peer_refresh(peer: &Peer, dst_buf: &mut [u8; MAX_UDP_SIZE]) { + let update_timers_result = peer.update_timers(&mut dst_buf[..]); + + match update_timers_result { + TunnResult::Done => {} + TunnResult::Err(WireGuardError::ConnectionExpired) => { + tracing::error!("Connection expired"); + } + TunnResult::Err(e) => tracing::error!(message = "Timer error", error = ?e), + TunnResult::WriteToNetwork(packet) => peer.send_infallible::(packet).await, + _ => panic!("Unexpected result from update_timers"), + }; + } + + fn start_rate_limiter_refresh_timer(self: &Arc) { + let rate_limiter = self.rate_limiter.clone(); + tokio::spawn(async move { + let mut interval = tokio::time::interval(RESET_PACKET_COUNT_INTERVAL); + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + loop { + rate_limiter.reset_count(); + interval.tick().await; + } + }); + } + + fn start_peers_refresh_timer(self: &Arc) { + let tunnel = self.clone(); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(REFRESH_PEERS_TIEMRS_INTERVAL); + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + let mut dst_buf = [0u8; MAX_UDP_SIZE]; + + loop { + let peers: Vec<_> = tunnel + .peers_by_ip + .read() + .values() + .unique_by(|p| p.index) + .cloned() + .collect(); + + for peer in peers { + Self::peer_refresh(&peer, &mut dst_buf).await; + } + + interval.tick().await; + } + }); + } + + fn start_timers(self: &Arc) { + self.start_rate_limiter_refresh_timer(); + self.start_peers_refresh_timer(); + } + + fn is_wireguard_packet_ok(&self, parsed_packet: &Packet, peer: &Peer) -> bool { + match &parsed_packet { + Packet::HandshakeInit(p) => { + parse_handshake_anon(&self.private_key, &self.public_key, p).is_ok() + } + Packet::HandshakeResponse(p) => check_packet_index(p.receiver_idx, peer.index), + Packet::PacketCookieReply(p) => check_packet_index(p.receiver_idx, peer.index), + Packet::PacketData(p) => check_packet_index(p.receiver_idx, peer.index), + } + } + + fn start_peer_handler(self: &Arc, peer: Arc) { + let tunnel = Arc::clone(self); + tokio::spawn(async move { + let mut src_buf = [0u8; MAX_UDP_SIZE]; + let mut dst_buf = [0u8; MAX_UDP_SIZE]; + // Loop while we have packets on the anonymous connection + while let Ok(size) = peer.channel.read(&mut src_buf[..]).await { + tracing::trace!("read {size} bytes from peer"); + // The rate limiter initially checks mac1 and mac2, and optionally asks to send a cookie + let parsed_packet = match tunnel.rate_limiter.verify_packet( + // TODO: Some(addr.ip()) webrtc doesn't expose easily the underlying data channel remote ip + // so for now we don't use it. but we need it for rate limiter although we probably not need it since the data channel + // will only be established to authenticated peers, so the portal could already prevent being ddos'd + // but maybe in that cased we can drop this rate_limiter all together and just use decapsulate + None, + &src_buf[..size], + &mut dst_buf, + ) { + Ok(packet) => packet, + Err(TunnResult::WriteToNetwork(cookie)) => { + peer.send_infallible::(cookie).await; + continue; + } + Err(_) => continue, + }; + + if !tunnel.is_wireguard_packet_ok(&parsed_packet, &peer) { + continue; + } + + let decapsulate_result = { + peer.tunnel.lock().decapsulate( + // TODO: See comment above + None, + &src_buf[..size], + &mut dst_buf[..], + ) + }; + + // We found a peer, use it to decapsulate the message+ + let mut flush = false; + match decapsulate_result { + TunnResult::Done => {} + TunnResult::Err(_) => continue, + TunnResult::WriteToNetwork(packet) => { + flush = true; + peer.send_infallible::(packet).await; + } + TunnResult::WriteToTunnelV4(packet, addr) => { + if peer.is_allowed_ipv4(&addr) { + tunnel.write_device_infallible(packet).await; + } + } + TunnResult::WriteToTunnelV6(packet, addr) => { + if peer.is_allowed_ipv6(&addr) { + tunnel.write_device_infallible(packet).await; + } + } + }; + + if flush { + // Flush pending queue + while let TunnResult::WriteToNetwork(packet) = { + let res = peer.tunnel.lock().decapsulate(None, &[], &mut dst_buf[..]); + res + } { + peer.send_infallible::(packet).await; + } + } + } + }); + } + + async fn write_device_infallible(&self, packet: &[u8]) { + if let Err(e) = self.device_channel.write(packet).await { + CB::on_error(&e.into(), Recoverable); + } + } + + fn get_resource(&self, buff: &[u8]) -> Option { + // TODO: Check if DNS packet, in that case parse and get dns + let addr = Tunn::dst_address(buff)?; + let resources = self.resources.read(); + match addr { + IpAddr::V4(ipv4) => resources + .get_by_helper_1(&ResourceKind::Addr(ipv4)) + .cloned(), + IpAddr::V6(ipv6) => resources + .get_by_helper_2(&ResourceKind::Addr(ipv6)) + .cloned(), + } + } + + fn start_iface_handler(self: &Arc) { + let dev = self.clone(); + tokio::spawn(async move { + loop { + let mut src = [0u8; MAX_UDP_SIZE]; + let mut dst = [0u8; MAX_UDP_SIZE]; + let res = { + // TODO: We should check here if what we read is a whole packet + // there's no docs on tun device on when a whole packet is read, is it \n or another thing? + // found some comments saying that a single read syscall represents a single packet but no docs on that + // See https://stackoverflow.com/questions/18461365/how-to-read-packet-by-packet-from-linux-tun-tap + match dev.device_channel.mtu().await { + Ok(mtu) => match dev.device_channel.read(&mut src[..mtu]).await { + Ok(res) => res, + Err(err) => { + tracing::error!("Couldn't read packet from interface: {err}"); + CB::on_error(&err.into(), Recoverable); + continue; + } + }, + Err(err) => { + tracing::error!("Couldn't obtain iface mtu: {err}"); + CB::on_error(&err, Recoverable); + continue; + } + } + }; + + let dst_addr = match Tunn::dst_address(&src[..res]) { + Some(addr) => addr, + None => continue, + }; + + let (encapsulate_result, channel) = { + let peers_by_ip = dev.peers_by_ip.read(); + match peers_by_ip.get(&dst_addr) { + Some(peer) => ( + peer.tunnel.lock().encapsulate(&src[..res], &mut dst[..]), + peer.channel.clone(), + ), + None => { + // We can buffer requests here but will drop them for now and let the upper layer reialability protocol handle this + if let Some(resource) = dev.get_resource(&src[..res]) { + // We have awaiting connection to prevent a race condition where + // create_peer_connection hasn't added the thing to peer_connections + // and we are finding another packet to the same address (otherwise we would just use peer_connections here) + let mut awaiting_connection = dev.awaiting_connection.lock(); + let id = resource.id; + if !awaiting_connection.contains(&id) { + tracing::trace!("Found new intent to send packets to resource with resource-ip: {dst_addr}, initalizing conection..."); + + awaiting_connection.insert(id); + let dev = Arc::clone(&dev); + + tokio::spawn(async move { + if let Err(e) = dev + .control_signaler + .signal_connection_to(&resource) + .await + { + // Not a deadlock because this is a different task + dev.awaiting_connection.lock().remove(&id); + tracing::error!("couldn't start protocol for new connection to resource: {e}"); + CB::on_error(&e, Recoverable); + } + }); + } + } + continue; + } + } + }; + + match encapsulate_result { + TunnResult::Done => { + tracing::trace!( + "tunnel for resource corresponding to {dst_addr} was finalized" + ); + } + TunnResult::Err(e) => { + tracing::error!(message = "Encapsulate error for resoruce corresponding to {dst_addr}", error = ?e); + CB::on_error(&e.into(), Recoverable); + } + TunnResult::WriteToNetwork(packet) => { + tracing::trace!("writing iface packet to peer: {}", dst_addr); + if let Err(e) = channel.write(&Bytes::copy_from_slice(packet)).await { + tracing::error!("Couldn't write packet to channel: {e}"); + CB::on_error(&e.into(), Recoverable); + } + } + _ => panic!("Unexpected result from encapsulate"), + }; + } + }); + } + + fn next_index(&self) -> u32 { + self.next_index.lock().next() + } +} diff --git a/libs/tunnel/src/multimap.rs b/libs/tunnel/src/multimap.rs new file mode 100644 index 0000000..9af7221 --- /dev/null +++ b/libs/tunnel/src/multimap.rs @@ -0,0 +1,80 @@ +use std::{collections::HashMap, hash::Hash}; + +// Custom very simple multimap +// it has get_main for getting things through the main key +// it has get_by_helper for getting things through helper keys +// it has remove which can only erase things through helper keys +// initially used to get things though id or a biyection(unique key) and delete through id +// e.g resource's id and resource kind mapped to the resource +#[derive(Debug, Clone)] +pub(crate) struct MultiMap { + main_map: HashMap, + helper_map_1: HashMap)>, + helper_map_2: HashMap)>, +} + +impl Default for MultiMap { + fn default() -> Self { + Self { + main_map: Default::default(), + helper_map_1: Default::default(), + helper_map_2: Default::default(), + } + } +} + +impl MultiMap +where + K1: Eq + PartialEq + Hash + Clone, + K2: Eq + PartialEq + Hash + Clone, + K3: Eq + PartialEq + Hash + Clone, +{ + pub(crate) fn insert( + &mut self, + main_key: K1, + helper_key_1: Option, + helper_key_2: Option, + value: V, + ) -> Option { + if let Some(k) = &helper_key_1 { + self.helper_map_1 + .insert(k.clone(), (main_key.clone(), helper_key_2.clone())); + } + + if let Some(k) = helper_key_2 { + self.helper_map_2 + .insert(k, (main_key.clone(), helper_key_1)); + } + self.main_map.insert(main_key, value) + } + + pub(crate) fn get_main(&self, k: &K1) -> Option<&V> { + self.main_map.get(k) + } + + pub(crate) fn get_by_helper_1(&self, k: &K2) -> Option<&V> { + let (k, _) = self.helper_map_1.get(k)?; + self.main_map.get(k) + } + + pub(crate) fn get_by_helper_2(&self, k: &K3) -> Option<&V> { + let (k, _) = self.helper_map_2.get(k)?; + self.main_map.get(k) + } + + pub(crate) fn _remove_by_helper_1(&mut self, k: &K2) -> Option { + let (k, k1) = self.helper_map_1.get(k)?; + if let Some(k) = k1 { + self.helper_map_2.remove(k); + } + self.main_map.remove(k) + } + + pub(crate) fn _remove_by_helper_2(&mut self, k: &K3) -> Option { + let (k, k2) = self.helper_map_2.get(k)?; + if let Some(k) = k2 { + self.helper_map_1.remove(k); + } + self.main_map.remove(k) + } +} diff --git a/libs/tunnel/src/peer.rs b/libs/tunnel/src/peer.rs new file mode 100644 index 0000000..122116f --- /dev/null +++ b/libs/tunnel/src/peer.rs @@ -0,0 +1,81 @@ +use std::{ + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::Arc, +}; + +use bytes::Bytes; +use libs_common::{ + boringtun::noise::{Tunn, TunnResult}, + error_type::ErrorType, + Callbacks, +}; +use parking_lot::Mutex; +use webrtc::data::data_channel::DataChannel; + +use super::PeerConfig; + +pub(crate) struct Peer { + pub(crate) tunnel: Mutex, + pub(crate) index: u32, + preshared_key: [u8; 32], + pub(crate) allowed_ipv4: Ipv4Addr, + pub(crate) allowed_ipv6: Ipv6Addr, + pub(crate) channel: Arc, +} + +impl Peer { + pub(crate) async fn send_infallible(&self, data: &[u8]) { + if let Err(e) = self.channel.write(&Bytes::copy_from_slice(data)).await { + tracing::error!("Couldn't send packet to connected peer: {e}"); + CB::on_error(&e.into(), ErrorType::Recoverable); + } + } + + pub(crate) fn from_config( + tunnel: Tunn, + index: u32, + config: &PeerConfig, + channel: Arc, + ) -> Self { + let preshared_key = config.preshared_key.to_bytes(); + + Self::new( + Mutex::new(tunnel), + index, + config.ipv4, + config.ipv6, + preshared_key, + channel, + ) + } + + pub(crate) fn new( + tunnel: Mutex, + index: u32, + ipv4: Ipv4Addr, + ipv6: Ipv6Addr, + preshared_key: [u8; 32], + channel: Arc, + ) -> Peer { + Peer { + tunnel, + index, + allowed_ipv4: ipv4, + allowed_ipv6: ipv6, + preshared_key, + channel, + } + } + + pub(crate) fn update_timers<'a>(&self, dst: &'a mut [u8]) -> TunnResult<'a> { + self.tunnel.lock().update_timers(dst) + } + + pub(crate) fn is_allowed_ipv4(&self, addr: &Ipv4Addr) -> bool { + &self.allowed_ipv4 == addr + } + + pub(crate) fn is_allowed_ipv6(&self, addr: &Ipv6Addr) -> bool { + &self.allowed_ipv6 == addr + } +} diff --git a/src/platform.rs b/libs/tunnel/src/platform.rs similarity index 100% rename from src/platform.rs rename to libs/tunnel/src/platform.rs diff --git a/src/platform/android.rs b/libs/tunnel/src/platform/android.rs similarity index 100% rename from src/platform/android.rs rename to libs/tunnel/src/platform/android.rs diff --git a/src/platform/apple.rs b/libs/tunnel/src/platform/apple.rs similarity index 100% rename from src/platform/apple.rs rename to libs/tunnel/src/platform/apple.rs diff --git a/src/platform/linux.rs b/libs/tunnel/src/platform/linux.rs similarity index 100% rename from src/platform/linux.rs rename to libs/tunnel/src/platform/linux.rs diff --git a/src/platform/windows.rs b/libs/tunnel/src/platform/windows.rs similarity index 100% rename from src/platform/windows.rs rename to libs/tunnel/src/platform/windows.rs diff --git a/libs/tunnel/src/tun_darwin.rs b/libs/tunnel/src/tun_darwin.rs new file mode 100644 index 0000000..e84a694 --- /dev/null +++ b/libs/tunnel/src/tun_darwin.rs @@ -0,0 +1,75 @@ +use libs_common::{boringtun::device::tun::TunSocket, Result}; +use std::os::fd::{AsRawFd, RawFd}; +use tokio::io::unix::AsyncFd; + +use super::InterfaceConfig; + +// TODO: we have to replace TunSocket because we need to use netpacketprovider to get approved in the app store +#[derive(Debug)] +pub(crate) struct DeviceChannel(tokio::io::unix::AsyncFd); + +#[derive(Debug)] +pub(crate) struct IfaceDevice { + fd: RawFd, +} + +impl DeviceChannel { + pub(crate) async fn mtu(&self) -> Result { + Ok(self.0.get_ref().mtu()?) + } + + pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { + loop { + let mut guard = self.0.readable().await?; + + match guard.try_io(|inner| { + inner.get_ref().read(out).map_err(|err| match err { + libs_common::boringtun::device::Error::IfaceRead(e) => e, + _ => panic!("Unexpected error while trying to read network interface"), + }) + }) { + Ok(result) => return result.map(|e| e.len()), + Err(_would_block) => continue, + } + } + } + + pub(crate) async fn write(&self, buf: &[u8]) -> std::io::Result { + loop { + let mut guard = self.0.writable().await?; + + // write4 and write6 does the same + match guard.try_io(|inner| match inner.get_ref().write4(buf) { + 0 => Err(std::io::Error::last_os_error()), + i => Ok(i), + }) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } +} + +impl IfaceDevice { + // It's easier to not make these functions async, setting these should not block the thread for too long + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + // TODO + + Ok(()) + } + + pub fn up(&mut self) -> Result<()> { + // TODO + Ok(()) + } +} + +pub(crate) async fn create_iface() -> Result<(IfaceDevice, DeviceChannel)> { + let dev = TunSocket::new("utun").unwrap().set_non_blocking().unwrap(); + let fd = dev.as_raw_fd(); + tracing::trace!("Started new interface with name: {:?}", dev.name()); + let dev = AsyncFd::new(dev)?; + + Ok((IfaceDevice { fd }, DeviceChannel(dev))) +} diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs new file mode 100644 index 0000000..e69de29 diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..e93d010 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0" } +proc-macro2 = { version = "1.0" } +quote = { version = "1.0" } + diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..d765549 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,108 @@ +#![recursion_limit = "128"] + +extern crate proc_macro; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{Data, DeriveInput, Fields}; + +/// Macro that generates a new enum with only the discriminants of another enum within a module that implements swift_bridge. +/// +/// This is a workaround to create an error type compatible with swift that can be converted from the original error type. +/// it implements `From` so the idea is that you can call a swift ffi function `handle_error(err.into());` +/// +/// This makes a lot of assumption about the types it's being implemented on since we're controling the type it is not meant +/// to be a public macro. (However be careful if you reuse it somewhere else! this is based in strum's EnumDiscrminant so you can +/// check there for an actual propper implementation). +/// +/// IMPORTANT!: You need to include swift_bridge::bridge for macos and ios target so this doesn't error out. +#[proc_macro_derive(SwiftEnum)] +pub fn swift_enum(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse_macro_input!(input as DeriveInput); + + let toks = swift_enum_inner(&ast).unwrap_or_else(|err| err.to_compile_error()); + toks.into() +} + +fn swift_enum_inner(ast: &DeriveInput) -> syn::Result { + let name = &ast.ident; + let vis = &ast.vis; + + let variants = match &ast.data { + Data::Enum(v) => &v.variants, + _ => { + return Err(syn::Error::new( + Span::call_site(), + "This macro only support enums.", + )) + } + }; + + let discriminants: Vec<_> = variants + .into_iter() + .map(|v| { + let ident = &v.ident; + quote! {#ident} + }) + .collect(); + + let enum_name = syn::Ident::new(&format!("Swift{}", name), Span::call_site()); + let mod_name = syn::Ident::new("swift_ffi", Span::call_site()); + + let arms = variants + .iter() + .map(|variant| { + let ident = &variant.ident; + let params = match &variant.fields { + Fields::Unit => quote! {}, + Fields::Unnamed(_fields) => { + quote! { (..) } + } + Fields::Named(_fields) => { + quote! { { .. } } + } + }; + + quote! { #name::#ident #params => #mod_name::#enum_name::#ident } + }) + .collect::>(); + + let from_fn_body = quote! { match val { #(#arms),* } }; + + let impl_from_ref = { + quote! { + impl<'a> ::core::convert::From<&'a #name> for #mod_name::#enum_name { + fn from(val: &'a #name) -> Self { + #from_fn_body + } + } + } + }; + + let impl_from = { + quote! { + impl ::core::convert::From<#name> for #mod_name::#enum_name { + fn from(val: #name) -> Self { + #from_fn_body + } + } + } + }; + + // If we wanted to expose this function we should have another crate that actually also includes + // swift_bridge. but since we are only using this inside our crates we can just make sure we include it. + Ok(quote! { + #[cfg_attr(any(target_os = "macos", target_os = "ios"), swift_bridge::bridge)] + #vis mod #mod_name { + pub enum #enum_name { + #(#discriminants),* + } + + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + #impl_from_ref + + #[cfg(any(target_os = "macos", target_os = "ios"))] + #impl_from + }) +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 1ddbcb2..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -use platform::tunnel::Tunnel; - -mod platform; - -#[allow(dead_code)] -pub struct Session { - tunnel: Tunnel, -} - -impl Session { - pub fn connect(_portal_url: String, _token: String) -> Result { - match Tunnel::new() { - Ok(tunnel) => Ok(Session { tunnel }), - Err(e) => Err(e), - } - } - - pub fn disconnect(&self) -> bool { - // 1. Close the websocket connection - // 2. Free the device handle (UNIX) - // 3. Close the file descriptor (UNIX) - // 4. Remove the mapping - true - } - - pub fn bump_sockets(&self) -> bool { - true - } - - pub fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { - true - } -} From 23fa26d286e72055a85a499e8f5bbd2c631d247d Mon Sep 17 00:00:00 2001 From: conectado Date: Wed, 24 May 2023 20:41:56 -0300 Subject: [PATCH 02/54] fix clippy --- clients/android/src/lib.rs | 12 ++++++++++-- clients/apple/build.rs | 2 +- libs/client/Cargo.toml | 2 -- libs/client/src/control.rs | 5 ----- libs/client/src/lib.rs | 2 -- libs/client/src/messages.rs | 4 ++++ libs/gateway/src/lib.rs | 2 -- libs/gateway/src/messages.rs | 4 ++++ libs/tunnel/src/peer.rs | 16 ++-------------- libs/tunnel/src/tun_darwin.rs | 6 +++--- 10 files changed, 24 insertions(+), 31 deletions(-) diff --git a/clients/android/src/lib.rs b/clients/android/src/lib.rs index 7d861df..1a80f8e 100644 --- a/clients/android/src/lib.rs +++ b/clients/android/src/lib.rs @@ -42,6 +42,8 @@ impl Callbacks for CallbackHandler { } } +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect( @@ -74,6 +76,8 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect( Box::into_raw(session) } +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_disconnect( @@ -89,6 +93,8 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_disconnect( session.disconnect() } +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_bump_sockets( @@ -101,9 +107,11 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_bump_sockets( unsafe { (*session_ptr).bump_sockets() }; // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 - return true; + true } +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_disable_some_roaming_for_broken_mobile_semantics( @@ -116,5 +124,5 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_disable_some_roaming_for unsafe { (*session_ptr).disable_some_roaming_for_broken_mobile_semantics() }; // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 - return true; + true } diff --git a/clients/apple/build.rs b/clients/apple/build.rs index b25e626..ddc9a9c 100644 --- a/clients/apple/build.rs +++ b/clients/apple/build.rs @@ -1,4 +1,4 @@ -const XCODE_CONFIGURATION_ENV: &'static str = "CONFIGURATION"; +const XCODE_CONFIGURATION_ENV: &str = "CONFIGURATION"; fn main() { let out_dir = "Sources/Connlib/Generated"; diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index 970a877..69b489d 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -3,8 +3,6 @@ name = "firezone-client-connlib" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] tokio = { version = "1.27", default-features = false, features = ["sync"] } tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs index 62bb7a8..a7d31e9 100644 --- a/libs/client/src/control.rs +++ b/libs/client/src/control.rs @@ -12,10 +12,6 @@ use async_trait::async_trait; use firezone_tunnel::{ControlSignal, Tunnel}; use tokio::sync::mpsc::{channel, Receiver, Sender}; -// FIXME: Replace all `expect` with a handler function -// that should be passed from the client. -// Also, we should decide if we disconnect or keep running depending on the error kind. - const INTERNAL_CHANNEL_SIZE: usize = 256; #[async_trait] @@ -182,7 +178,6 @@ impl ControlSession, pub(crate) index: u32, - preshared_key: [u8; 32], pub(crate) allowed_ipv4: Ipv4Addr, pub(crate) allowed_ipv6: Ipv6Addr, pub(crate) channel: Arc, @@ -37,16 +36,7 @@ impl Peer { config: &PeerConfig, channel: Arc, ) -> Self { - let preshared_key = config.preshared_key.to_bytes(); - - Self::new( - Mutex::new(tunnel), - index, - config.ipv4, - config.ipv6, - preshared_key, - channel, - ) + Self::new(Mutex::new(tunnel), index, config.ipv4, config.ipv6, channel) } pub(crate) fn new( @@ -54,7 +44,6 @@ impl Peer { index: u32, ipv4: Ipv4Addr, ipv6: Ipv6Addr, - preshared_key: [u8; 32], channel: Arc, ) -> Peer { Peer { @@ -62,7 +51,6 @@ impl Peer { index, allowed_ipv4: ipv4, allowed_ipv6: ipv6, - preshared_key, channel, } } diff --git a/libs/tunnel/src/tun_darwin.rs b/libs/tunnel/src/tun_darwin.rs index e84a694..c964cee 100644 --- a/libs/tunnel/src/tun_darwin.rs +++ b/libs/tunnel/src/tun_darwin.rs @@ -10,7 +10,7 @@ pub(crate) struct DeviceChannel(tokio::io::unix::AsyncFd); #[derive(Debug)] pub(crate) struct IfaceDevice { - fd: RawFd, + _fd: RawFd, } impl DeviceChannel { @@ -67,9 +67,9 @@ impl IfaceDevice { pub(crate) async fn create_iface() -> Result<(IfaceDevice, DeviceChannel)> { let dev = TunSocket::new("utun").unwrap().set_non_blocking().unwrap(); - let fd = dev.as_raw_fd(); + let _fd = dev.as_raw_fd(); tracing::trace!("Started new interface with name: {:?}", dev.name()); let dev = AsyncFd::new(dev)?; - Ok((IfaceDevice { fd }, DeviceChannel(dev))) + Ok((IfaceDevice { _fd }, DeviceChannel(dev))) } From 9517329bc7463e72468b2559d31c4687113fab86 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 00:13:40 -0300 Subject: [PATCH 03/54] fix gateway control-plane error handling --- libs/gateway/Cargo.toml | 2 -- libs/gateway/src/control.rs | 45 +++++++++++++++++++---------- libs/tunnel/src/control_protocol.rs | 5 ++++ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/libs/gateway/Cargo.toml b/libs/gateway/Cargo.toml index 2a5e4b9..944c29b 100644 --- a/libs/gateway/Cargo.toml +++ b/libs/gateway/Cargo.toml @@ -3,8 +3,6 @@ name = "firezone-gateway-connlib" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] libs-common = { path = "../common" } async-trait = { version = "0.1", default-features = false } diff --git a/libs/gateway/src/control.rs b/libs/gateway/src/control.rs index d2e6428..11215ac 100644 --- a/libs/gateway/src/control.rs +++ b/libs/gateway/src/control.rs @@ -2,8 +2,10 @@ use std::{sync::Arc, time::Duration}; use firezone_tunnel::{ControlSignal, Tunnel}; use libs_common::{ - boringtun::x25519::StaticSecret, messages::ResourceDescription, Callbacks, ControlSession, - Result, + boringtun::x25519::StaticSecret, + error_type::ErrorType::{Fatal, Recoverable}, + messages::ResourceDescription, + Callbacks, ControlSession, Result, }; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -51,10 +53,11 @@ where #[tracing::instrument(level = "trace", skip_all)] async fn init(&mut self, init: InitGateway) { - self.tunnel - .set_interface(&init.interface) - .await - .expect("Couldn't start tunnel"); + if let Err(e) = self.tunnel.set_interface(&init.interface).await { + tracing::error!("Couldn't intialize interface: {e}"); + C::on_error(&e, Fatal); + return; + } // TODO: Enable masquerading here. tracing::info!("Firezoned Started!"); @@ -65,7 +68,7 @@ where let tunnel = Arc::clone(&self.tunnel); let control_signaler = self.control_signaler.clone(); tokio::spawn(async move { - let gateway_rtc_sdp = tunnel + match tunnel .set_peer_connection_request( connection_request.rtc_sdp, connection_request.client.peer.into(), @@ -73,15 +76,25 @@ where connection_request.client.id, ) .await - .expect("TODO"); - control_signaler - .internal_sender - .send(EgressMessages::ConnectionReady(ConnectionReady { - client_id: connection_request.client.id, - gateway_rtc_sdp, - })) - .await - .expect("TODO!"); + { + Ok(gateway_rtc_sdp) => { + if let Err(err) = control_signaler + .internal_sender + .send(EgressMessages::ConnectionReady(ConnectionReady { + client_id: connection_request.client.id, + gateway_rtc_sdp, + })) + .await + { + tunnel.cleanup_peer_connection(connection_request.client.id); + C::on_error(&err.into(), Recoverable); + } + } + Err(err) => { + tunnel.cleanup_peer_connection(connection_request.client.id); + C::on_error(&err, Recoverable); + } + } }); } diff --git a/libs/tunnel/src/control_protocol.rs b/libs/tunnel/src/control_protocol.rs index 53f7b3e..a645035 100644 --- a/libs/tunnel/src/control_protocol.rs +++ b/libs/tunnel/src/control_protocol.rs @@ -222,6 +222,11 @@ where Ok(()) } + /// Removes client's id from connections we are expecting. + pub fn cleanup_peer_connection(self: &Arc, client_id: Id) { + self.peer_connections.lock().remove(&client_id); + } + /// Accept a connection request from a client. /// /// Sets a connection to a remote SDP, creates the local SDP From 2ec96844f279533f1340ef7a0d5d413d56361180 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 00:35:52 -0300 Subject: [PATCH 04/54] add boringtun copyright notice --- libs/tunnel/src/index.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/libs/tunnel/src/index.rs b/libs/tunnel/src/index.rs index f03a89c..6949bcc 100644 --- a/libs/tunnel/src/index.rs +++ b/libs/tunnel/src/index.rs @@ -1,13 +1,26 @@ use rand_core::{OsRng, RngCore}; // Taken from boringtun (todo) -/// A basic linear-feedback shift register implemented as xorshift, used to -/// distribute peer indexes across the 24-bit address space reserved for peer -/// identification. -/// The purpose is to obscure the total number of peers using the system and to -/// ensure it requires a non-trivial amount of processing power and/or samples -/// to guess other peers' indices. Anything more ambitious than this is wasted -/// with only 24 bits of space. +// Note that the following code is directly copy pasted from [boringtun](https://github.com/cloudflare/boringtun) +// As per boringtun's license we are including the following copy-right notice regarding only boringtun: +// === +// Copyright (c) 2019 Cloudflare, Inc. All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// ==== +// A basic linear-feedback shift register implemented as xorshift, used to +// distribute peer indexes across the 24-bit address space reserved for peer +// identification. +// The purpose is to obscure the total number of peers using the system and to +// ensure it requires a non-trivial amount of processing power and/or samples +// to guess other peers' indices. Anything more ambitious than this is wasted +// with only 24 bits of space. pub(crate) struct IndexLfsr { initial: u32, lfsr: u32, From ff4aa6e805a9d4da55a07596c953c0a75f0b3e75 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 01:13:54 -0300 Subject: [PATCH 05/54] temporarily depend on my own swift-bridge fork to fix CI --- clients/apple/Cargo.toml | 4 ++-- libs/common/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/apple/Cargo.toml b/clients/apple/Cargo.toml index acddc54..380a202 100644 --- a/clients/apple/Cargo.toml +++ b/clients/apple/Cargo.toml @@ -6,11 +6,11 @@ edition = "2021" build = "build.rs" [build-dependencies] -swift-bridge-build = { path = "../../../swift-bridge/crates/swift-bridge-build" } +swift-bridge-build = { git = "https://github.com/conectado/swift-bridge.git" branch = "fix-already-declared" } [dependencies] libc = "0.2" -swift-bridge = { path = "../../../swift-bridge" } +swift-bridge = { git = "https://github.com/conectado/swift-bridge.git" branch = "fix-already-declared" } firezone-client-connlib = { path = "../../libs/client" } [lib] diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index ba1eadf..007ec5f 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -29,4 +29,4 @@ backoff = { version = "0.4", default-features = false } macros = { path = "../../macros" } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -swift-bridge = { path = "../../../swift-bridge" } +swift-bridge = { git = "https://github.com/conectado/swift-bridge.git" branch = "fix-already-declared" } From cb296066309de9646c9a3eb2d697f080797108e0 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 01:33:11 -0300 Subject: [PATCH 06/54] fix cargo.toml --- clients/apple/Cargo.toml | 4 ++-- libs/common/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/apple/Cargo.toml b/clients/apple/Cargo.toml index 380a202..80469bb 100644 --- a/clients/apple/Cargo.toml +++ b/clients/apple/Cargo.toml @@ -6,11 +6,11 @@ edition = "2021" build = "build.rs" [build-dependencies] -swift-bridge-build = { git = "https://github.com/conectado/swift-bridge.git" branch = "fix-already-declared" } +swift-bridge-build = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } [dependencies] libc = "0.2" -swift-bridge = { git = "https://github.com/conectado/swift-bridge.git" branch = "fix-already-declared" } +swift-bridge = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } firezone-client-connlib = { path = "../../libs/client" } [lib] diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index 007ec5f..4988181 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -29,4 +29,4 @@ backoff = { version = "0.4", default-features = false } macros = { path = "../../macros" } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -swift-bridge = { git = "https://github.com/conectado/swift-bridge.git" branch = "fix-already-declared" } +swift-bridge = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } From 75caf24dfeca9f2ec255b82159ab9393c8c82bbe Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 01:57:31 -0300 Subject: [PATCH 07/54] fix tun_linux --- libs/common/src/error.rs | 3 + libs/tunnel/Cargo.toml | 1 + libs/tunnel/src/tun_linux.rs | 265 +++++++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+) diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs index 3ff0b9c..9b74dae 100644 --- a/libs/common/src/error.rs +++ b/libs/common/src/error.rs @@ -58,6 +58,9 @@ pub enum ConnlibError { /// Error regarding our own control protocol. #[error("Control plane protocol error. Unexpected messages or message order.")] ControlProtocolError, + /// Error when manipulating system's interface + #[error("Error while manipulating system's interface")] + IFaceError, /// Glob for errors without a type. #[error("Other error")] Other(&'static str), diff --git a/libs/tunnel/Cargo.toml b/libs/tunnel/Cargo.toml index ce513fe..42d1ab2 100644 --- a/libs/tunnel/Cargo.toml +++ b/libs/tunnel/Cargo.toml @@ -26,6 +26,7 @@ libs-common = { path = "../common", features = ["device"] } # Linux tunnel dependencies [target.'cfg(target_os = "linux")'.dependencies] libs-common = { path = "../common", features = ["device"] } +rtnetlink = "0.12" # Android tunnel dependencies [target.'cfg(target_os = "android")'.dependencies] diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index e69de29..108ee3a 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -0,0 +1,265 @@ +use futures::{ready, TryStreamExt}; +use libs_common::{boringtun::device::tun::TunSocket, Error, Result}; +use rtnetlink::{new_connection, packet::nlas::link::Nla, Handle}; +use std::{pin::Pin, task::Poll}; +use tokio::{ + io::{unix::AsyncFd, AsyncRead, AsyncWrite}, + task::JoinHandle, +}; + +use libc::{__errno_location, c_short, c_uchar, strerror, IFNAMSIZ}; + +use super::{resolvconf, InterfaceConfig}; + +const TUN_DRIVER: &str = "/dev/net/tun"; +const TUNSETIFF: u64 = 0x4004_54ca; + +// Re-implementing TunSocket would be much better but using it as this for now +#[derive(Debug)] +pub struct DeviceChannel(tokio::io::unix::AsyncFd); + +#[derive(Debug)] +pub struct IfaceDevice { + device_channel: Option, + name: String, + interface_index: u32, + handle: Handle, + join_handle: JoinHandle<()>, +} + +impl DeviceChannel { + pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { + loop { + let mut guard = self.0.readable().await?; + + match guard.try_io(|inner| { + inner.get_ref().read(out).map_err(|err| { + if let boringtun::device::Error::IfaceRead(e) = err { + std::io::Error::from_raw_os_error(e) + } else { + panic!("we expect read to only return ifaceread errors") + } + }) + }) { + Ok(result) => return result.map(|e| e.len()), + Err(_would_block) => continue, + } + } + } + + pub(crate) async fn write(&self, buf: &[u8]) -> std::io::Result { + loop { + let mut guard = self.0.writable().await?; + + // write4 and write6 does the same (must be different in macos or something) + match guard.try_io(|inner| { + let res = inner.get_ref().write4(buf); + if res < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(res) + } + }) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } +} + +impl AsyncRead for DeviceChannel { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + loop { + let mut guard = ready!(self.0.poll_read_ready(cx))?; + + let unfilled = buf.initialize_unfilled(); + match guard.try_io(|inner| { + inner + .get_ref() + .read(unfilled) + .map_err(|err| { + if let boringtun::device::Error::IfaceRead(e) = err { + std::io::Error::from_raw_os_error(e) + } else { + panic!("we expect read to only return ifaceread errors") + } + }) + .map(|res| res.len()) + }) { + Ok(Ok(res)) => { + buf.advance(res); + return Poll::Ready(Ok(())); + } + Ok(Err(err)) => return Poll::Ready(Err(err)), + Err(_would_block) => continue, + } + } + } +} + +impl AsyncWrite for DeviceChannel { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + loop { + let mut guard = ready!(self.0.poll_write_ready(cx))?; + + match guard.try_io(|inner| { + let res = inner.get_ref().write4(buf); + if res < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(res) + } + }) { + Ok(result) => return Poll::Ready(result), + Err(_would_block) => continue, + } + } + } + + fn poll_flush( + self: Pin<&mut Self>, + _: &mut std::task::Context<'_>, + ) -> Poll> { + // flush is a no-op + Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + _: &mut std::task::Context<'_>, + ) -> Poll> { + // Shutdown is also a no-op + Poll::Ready(Ok(())) + } +} + +impl Drop for IfaceDevice { + fn drop(&mut self) { + self.join_handle.abort(); + } +} + +impl IfaceDevice { + // TODO: I don't like this kind of API that you can't call this method twice. + // we could use an enum to represent the device state or return this upon iface creation. + pub(crate) fn get_device_channel(&mut self) -> Result { + self.device_channel.take().ok_or_else(|| Error) + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(crate) async fn set_iface_config(&self, config: &InterfaceConfig) -> Result<()> { + let ips = self + .handle + .address() + .get() + .set_link_index_filter(self.interface_index) + .execute(); + + ips.try_for_each(|ip| self.handle.address().del(ip).execute()) + .await?; + + for addr in config.address.values() { + self.handle + .address() + .add(self.interface_index, addr.addr(), addr.prefix_len()) + .execute() + .await? + } + + let name: String = self.name.clone().try_into()?; + for dns in &config.dns { + resolvconf::set_dns(&name, dns).await?; + } + + //nftables::enable_masquerade((config.ipv4_masquerade, config.ipv6_masquerade)).await?; + + Ok(()) + } + + pub(crate) async fn up(&self) -> Result<()> { + self.handle + .link() + .set(self.interface_index) + .up() + .execute() + .await?; + + Ok(()) + } + + pub(crate) async fn mtu(&self) -> Result { + while let Ok(Some(msg)) = self + .handle + .link() + .get() + .match_index(self.interface_index) + .execute() + .try_next() + .await + { + for nla in msg.nlas { + if let Nla::Mtu(mtu) = nla { + return Ok(mtu); + } + } + } + + Err(Error::IFaceError) + } + + // TODO: Do we need to set non-blocking? + // TODO: More importantly, here we are setting multi-queue + // however, to read multiqueued data we need to alloc multiple fd + // see: https://www.kernel.org/doc/Documentation/networking/tuntap.txt multiqueue section + // maybe we can hold a Vec and that's it? I think so + pub(crate) async fn new(name: String) -> Result { + // TODO unwrap(boringtun's error doesn't implement StdError) + let dev = TunSocket::new(&name).unwrap().set_non_blocking().unwrap(); + let dev = AsyncFd::new(dev)?; + + let (connection, handle, _) = + new_connection().context("Couldn't get netlink connection")?; + let join_handle = tokio::spawn(connection); + let interface_index = handle + .link() + .get() + .match_name( + name.clone() + .try_into() + .context("we are not supporting non utf-8 interface names for now")?, + ) + .execute() + .try_next() + .await + .context("Couldn't get index of created interface")? + .ok_or_else(|| Error::IFaceError)? + .header + .index; + Ok(Self { + device_channel: Some(DeviceChannel(dev)), + name, + interface_index, + handle, + join_handle, + }) + } +} + +#[repr(C)] +union IfrIfru { + ifru_flags: c_short, +} + +#[repr(C)] +pub struct Ifreq { + ifr_name: [c_uchar; IFNAMSIZ], + ifr_ifru: IfrIfru, +} From c03ac2b9dcf17ddfbfa970117dc27b894fa8e8e3 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 02:10:45 -0300 Subject: [PATCH 08/54] unify tunnel implementation for now --- libs/tunnel/src/lib.rs | 17 +- libs/tunnel/src/platform.rs | 19 -- libs/tunnel/src/platform/android.rs | 12 - libs/tunnel/src/platform/apple.rs | 22 -- libs/tunnel/src/platform/linux.rs | 20 -- libs/tunnel/src/platform/windows.rs | 9 - libs/tunnel/src/{tun_darwin.rs => tun.rs} | 0 libs/tunnel/src/tun_linux.rs | 264 ---------------------- 8 files changed, 7 insertions(+), 356 deletions(-) delete mode 100644 libs/tunnel/src/platform.rs delete mode 100644 libs/tunnel/src/platform/android.rs delete mode 100644 libs/tunnel/src/platform/apple.rs delete mode 100644 libs/tunnel/src/platform/linux.rs delete mode 100644 libs/tunnel/src/platform/windows.rs rename libs/tunnel/src/{tun_darwin.rs => tun.rs} (100%) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 41e7deb..81b48f2 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -43,8 +43,9 @@ use libs_common::{ Result, }; -use self::tun_device::DeviceChannel; -use self::tun_device::IfaceDevice; +use self::tun::create_iface; +use self::tun::DeviceChannel; +use self::tun::IfaceDevice; pub use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; @@ -56,13 +57,9 @@ mod index; mod multimap; mod peer; -#[cfg(target_os = "linux")] -#[path = "tun_linux.rs"] -mod tun_device; - -#[cfg(any(target_os = "macos", target_os = "ios"))] -#[path = "tun_darwin.rs"] -mod tun_device; +// TODO: For now all tunnel implementations are the same +// will divide when we start introducing differences. +mod tun; const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1); const REFRESH_PEERS_TIEMRS_INTERVAL: Duration = Duration::from_secs(1); @@ -147,7 +144,7 @@ where let rate_limiter = Arc::new(RateLimiter::new(&public_key, HANDSHAKE_RATE_LIMIT)); let peers_by_ip = Default::default(); let next_index = Default::default(); - let (iface_device, device_channel) = tun_device::create_iface().await?; + let (iface_device, device_channel) = create_iface().await?; let iface_device = Mutex::new(iface_device); let device_channel = Arc::new(device_channel); let peer_connections = Default::default(); diff --git a/libs/tunnel/src/platform.rs b/libs/tunnel/src/platform.rs deleted file mode 100644 index 43547b8..0000000 --- a/libs/tunnel/src/platform.rs +++ /dev/null @@ -1,19 +0,0 @@ -// Tunnel management for Linux -#[cfg(target_os = "linux")] -#[path = "platform/linux.rs"] -pub mod tunnel; - -// Tunnel management for macOS and iOS -#[cfg(any(target_os = "ios", target_os = "macos"))] -#[path = "platform/apple.rs"] -pub mod tunnel; - -// Tunnel management for Windows -#[cfg(target_os = "windows")] -#[path = "platform/windows.rs"] -pub mod tunnel; - -// Tunnel management for Android -#[cfg(target_os = "android")] -#[path = "platform/android.rs"] -pub mod tunnel; diff --git a/libs/tunnel/src/platform/android.rs b/libs/tunnel/src/platform/android.rs deleted file mode 100644 index a20b00b..0000000 --- a/libs/tunnel/src/platform/android.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[allow(dead_code)] -pub struct Tunnel { - fd: i32, -} - -impl Tunnel { - pub fn new() -> Result { - // On android, the file descriptor is passed from the VPN service. We'll need to accept it - // or set it later. - Ok(Self { fd: -1 }) - } -} diff --git a/libs/tunnel/src/platform/apple.rs b/libs/tunnel/src/platform/apple.rs deleted file mode 100644 index 4bb89c4..0000000 --- a/libs/tunnel/src/platform/apple.rs +++ /dev/null @@ -1,22 +0,0 @@ -use boringtun::device::tun::TunSocket; - -#[allow(dead_code)] -pub struct Tunnel { - socket: TunSocket, -} - -impl Tunnel { - pub fn new() -> Result { - // Loop through all utun interfaces and try to find an unused one - for index in 0..255 { - let utun_name = format!("utun{index}"); - if let Ok(socket) = TunSocket::new(utun_name.as_str()) { - return Ok(Tunnel { socket }); - } - } - Err(std::io::Error::new( - std::io::ErrorKind::Other, - "No more utun interfaces available", - )) - } -} diff --git a/libs/tunnel/src/platform/linux.rs b/libs/tunnel/src/platform/linux.rs deleted file mode 100644 index 6d29680..0000000 --- a/libs/tunnel/src/platform/linux.rs +++ /dev/null @@ -1,20 +0,0 @@ -use boringtun::device::tun::TunSocket; - -const TUN_NAME: &str = "wg-firezone"; - -#[allow(dead_code)] -pub struct Tunnel { - socket: TunSocket, -} - -impl Tunnel { - pub fn new() -> Result { - match TunSocket::new(TUN_NAME) { - Ok(socket) => Ok(Self { socket }), - Err(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "TunSocket::new() failed", - )), - } - } -} diff --git a/libs/tunnel/src/platform/windows.rs b/libs/tunnel/src/platform/windows.rs deleted file mode 100644 index 5ea15d0..0000000 --- a/libs/tunnel/src/platform/windows.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub struct Tunnel { - // TODO: Windows virtual adapter? -} - -impl Tunnel { - pub fn new() -> Result { - Ok(Tunnel {}) - } -} diff --git a/libs/tunnel/src/tun_darwin.rs b/libs/tunnel/src/tun.rs similarity index 100% rename from libs/tunnel/src/tun_darwin.rs rename to libs/tunnel/src/tun.rs diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index 108ee3a..8b13789 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -1,265 +1 @@ -use futures::{ready, TryStreamExt}; -use libs_common::{boringtun::device::tun::TunSocket, Error, Result}; -use rtnetlink::{new_connection, packet::nlas::link::Nla, Handle}; -use std::{pin::Pin, task::Poll}; -use tokio::{ - io::{unix::AsyncFd, AsyncRead, AsyncWrite}, - task::JoinHandle, -}; -use libc::{__errno_location, c_short, c_uchar, strerror, IFNAMSIZ}; - -use super::{resolvconf, InterfaceConfig}; - -const TUN_DRIVER: &str = "/dev/net/tun"; -const TUNSETIFF: u64 = 0x4004_54ca; - -// Re-implementing TunSocket would be much better but using it as this for now -#[derive(Debug)] -pub struct DeviceChannel(tokio::io::unix::AsyncFd); - -#[derive(Debug)] -pub struct IfaceDevice { - device_channel: Option, - name: String, - interface_index: u32, - handle: Handle, - join_handle: JoinHandle<()>, -} - -impl DeviceChannel { - pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { - loop { - let mut guard = self.0.readable().await?; - - match guard.try_io(|inner| { - inner.get_ref().read(out).map_err(|err| { - if let boringtun::device::Error::IfaceRead(e) = err { - std::io::Error::from_raw_os_error(e) - } else { - panic!("we expect read to only return ifaceread errors") - } - }) - }) { - Ok(result) => return result.map(|e| e.len()), - Err(_would_block) => continue, - } - } - } - - pub(crate) async fn write(&self, buf: &[u8]) -> std::io::Result { - loop { - let mut guard = self.0.writable().await?; - - // write4 and write6 does the same (must be different in macos or something) - match guard.try_io(|inner| { - let res = inner.get_ref().write4(buf); - if res < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(res) - } - }) { - Ok(result) => return result, - Err(_would_block) => continue, - } - } - } -} - -impl AsyncRead for DeviceChannel { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - loop { - let mut guard = ready!(self.0.poll_read_ready(cx))?; - - let unfilled = buf.initialize_unfilled(); - match guard.try_io(|inner| { - inner - .get_ref() - .read(unfilled) - .map_err(|err| { - if let boringtun::device::Error::IfaceRead(e) = err { - std::io::Error::from_raw_os_error(e) - } else { - panic!("we expect read to only return ifaceread errors") - } - }) - .map(|res| res.len()) - }) { - Ok(Ok(res)) => { - buf.advance(res); - return Poll::Ready(Ok(())); - } - Ok(Err(err)) => return Poll::Ready(Err(err)), - Err(_would_block) => continue, - } - } - } -} - -impl AsyncWrite for DeviceChannel { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> Poll> { - loop { - let mut guard = ready!(self.0.poll_write_ready(cx))?; - - match guard.try_io(|inner| { - let res = inner.get_ref().write4(buf); - if res < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(res) - } - }) { - Ok(result) => return Poll::Ready(result), - Err(_would_block) => continue, - } - } - } - - fn poll_flush( - self: Pin<&mut Self>, - _: &mut std::task::Context<'_>, - ) -> Poll> { - // flush is a no-op - Poll::Ready(Ok(())) - } - - fn poll_shutdown( - self: Pin<&mut Self>, - _: &mut std::task::Context<'_>, - ) -> Poll> { - // Shutdown is also a no-op - Poll::Ready(Ok(())) - } -} - -impl Drop for IfaceDevice { - fn drop(&mut self) { - self.join_handle.abort(); - } -} - -impl IfaceDevice { - // TODO: I don't like this kind of API that you can't call this method twice. - // we could use an enum to represent the device state or return this upon iface creation. - pub(crate) fn get_device_channel(&mut self) -> Result { - self.device_channel.take().ok_or_else(|| Error) - } - - #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn set_iface_config(&self, config: &InterfaceConfig) -> Result<()> { - let ips = self - .handle - .address() - .get() - .set_link_index_filter(self.interface_index) - .execute(); - - ips.try_for_each(|ip| self.handle.address().del(ip).execute()) - .await?; - - for addr in config.address.values() { - self.handle - .address() - .add(self.interface_index, addr.addr(), addr.prefix_len()) - .execute() - .await? - } - - let name: String = self.name.clone().try_into()?; - for dns in &config.dns { - resolvconf::set_dns(&name, dns).await?; - } - - //nftables::enable_masquerade((config.ipv4_masquerade, config.ipv6_masquerade)).await?; - - Ok(()) - } - - pub(crate) async fn up(&self) -> Result<()> { - self.handle - .link() - .set(self.interface_index) - .up() - .execute() - .await?; - - Ok(()) - } - - pub(crate) async fn mtu(&self) -> Result { - while let Ok(Some(msg)) = self - .handle - .link() - .get() - .match_index(self.interface_index) - .execute() - .try_next() - .await - { - for nla in msg.nlas { - if let Nla::Mtu(mtu) = nla { - return Ok(mtu); - } - } - } - - Err(Error::IFaceError) - } - - // TODO: Do we need to set non-blocking? - // TODO: More importantly, here we are setting multi-queue - // however, to read multiqueued data we need to alloc multiple fd - // see: https://www.kernel.org/doc/Documentation/networking/tuntap.txt multiqueue section - // maybe we can hold a Vec and that's it? I think so - pub(crate) async fn new(name: String) -> Result { - // TODO unwrap(boringtun's error doesn't implement StdError) - let dev = TunSocket::new(&name).unwrap().set_non_blocking().unwrap(); - let dev = AsyncFd::new(dev)?; - - let (connection, handle, _) = - new_connection().context("Couldn't get netlink connection")?; - let join_handle = tokio::spawn(connection); - let interface_index = handle - .link() - .get() - .match_name( - name.clone() - .try_into() - .context("we are not supporting non utf-8 interface names for now")?, - ) - .execute() - .try_next() - .await - .context("Couldn't get index of created interface")? - .ok_or_else(|| Error::IFaceError)? - .header - .index; - Ok(Self { - device_channel: Some(DeviceChannel(dev)), - name, - interface_index, - handle, - join_handle, - }) - } -} - -#[repr(C)] -union IfrIfru { - ifru_flags: c_short, -} - -#[repr(C)] -pub struct Ifreq { - ifr_name: [c_uchar; IFNAMSIZ], - ifr_ifru: IfrIfru, -} From fe4105d0910d867dbea06ce0d9966b9181a805de Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 02:27:16 -0300 Subject: [PATCH 09/54] compile mac only on mac --- clients/apple/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/apple/src/lib.rs b/clients/apple/src/lib.rs index 24580bd..effece4 100644 --- a/clients/apple/src/lib.rs +++ b/clients/apple/src/lib.rs @@ -1,5 +1,6 @@ // Swift bridge generated code triggers this below #![allow(improper_ctypes)] +#![cfg(any(target_os = "macos", target_os = "ios"))] use firezone_client_connlib::{ Callbacks, Error, ErrorType, ResourceList, Session, SwiftConnlibError, SwiftErrorType, From 513f23b99764653a14495109ae1271cae5b9e546 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 15:05:59 -0300 Subject: [PATCH 10/54] stub out tun for windows compilation --- libs/tunnel/src/lib.rs | 13 ++++++++++--- libs/tunnel/src/win_tun.rs | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 libs/tunnel/src/win_tun.rs diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 81b48f2..7f201a1 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -43,9 +43,12 @@ use libs_common::{ Result, }; -use self::tun::create_iface; -use self::tun::DeviceChannel; -use self::tun::IfaceDevice; +#[cfg(target_os = "windows")] +use win_tun as tun; + +use tun::create_iface; +use tun::DeviceChannel; +use tun::IfaceDevice; pub use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; @@ -59,8 +62,12 @@ mod peer; // TODO: For now all tunnel implementations are the same // will divide when we start introducing differences. +#[cfg(not(target_os = "windows"))] mod tun; +#[cfg(target_os = "windows")] +mod win_tun; + const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1); const REFRESH_PEERS_TIEMRS_INTERVAL: Duration = Duration::from_secs(1); diff --git a/libs/tunnel/src/win_tun.rs b/libs/tunnel/src/win_tun.rs new file mode 100644 index 0000000..43c0593 --- /dev/null +++ b/libs/tunnel/src/win_tun.rs @@ -0,0 +1,39 @@ +use super::InterfaceConfig; +use libs_common::Result; + +// This is an stubbed out module to be able to compile on windows. +#[derive(Debug)] +pub(crate) struct DeviceChannel; + +#[derive(Debug)] +pub(crate) struct IfaceDevice; + +impl DeviceChannel { + pub(crate) async fn mtu(&self) -> Result { + todo!() + } + + pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { + todo!() + } + + pub(crate) async fn write(&self, buf: &[u8]) -> std::io::Result { + todo!() + } +} + +impl IfaceDevice { + // It's easier to not make these functions async, setting these should not block the thread for too long + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + todo!() + } + + pub fn up(&mut self) -> Result<()> { + todo!() + } +} + +pub(crate) async fn create_iface() -> Result<(IfaceDevice, DeviceChannel)> { + todo!() +} From d2853c800825b39f30937c8a63149fb0ca756399 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 16:46:42 -0300 Subject: [PATCH 11/54] fix ci by excluding a matrix combination --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01f6983..a745a75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,11 @@ jobs: - macos-12 - windows-2019 - windows-2022 + # FIXME: There's this weird cargo thing where it stops finding the webrtc's related crates + # probably has to do with the cache + some cargo bug. + exclude: + - rust: nightly + runs-on: ubuntu-20.04 runs-on: ${{ matrix.runs-on }} steps: - name: Checkout From c81804f3525f36a838e652a8ae71602cae076297 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 17:13:00 -0300 Subject: [PATCH 12/54] fix windows ci and add boringtun notice --- NOTICE.txt | 20 ++++++++++++++++++++ libs/common/Cargo.toml | 10 ++++++++++ libs/tunnel/src/index.rs | 14 -------------- 3 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 NOTICE.txt diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..c34202b --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,20 @@ +NOTICES AND INFORMATION +Do Not Translate or Localize + +This software incorporates material from third parties. + +Please refer to this document for the license terms of the components that this product depends and use. + +=== + +This product depends on and uses Boringtun source code: + +Copyright (c) 2019 Cloudflare, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index 4988181..2a2baae 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -30,3 +30,13 @@ macros = { path = "../../macros" } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] swift-bridge = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } + +[target.'cfg(target_os = "linux")'.dependencies] +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } + +[target.'cfg(target_os = "android")'.dependencies] +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } + +[target.'cfg(target_os = "windows")'.dependencies] +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false } diff --git a/libs/tunnel/src/index.rs b/libs/tunnel/src/index.rs index 6949bcc..5bb0f54 100644 --- a/libs/tunnel/src/index.rs +++ b/libs/tunnel/src/index.rs @@ -1,19 +1,5 @@ use rand_core::{OsRng, RngCore}; -// Taken from boringtun (todo) -// Note that the following code is directly copy pasted from [boringtun](https://github.com/cloudflare/boringtun) -// As per boringtun's license we are including the following copy-right notice regarding only boringtun: -// === -// Copyright (c) 2019 Cloudflare, Inc. All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// ==== // A basic linear-feedback shift register implemented as xorshift, used to // distribute peer indexes across the 24-bit address space reserved for peer // identification. From 5ca670470995c1333e25e99078b3ca4e15c29bb5 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 17:41:34 -0300 Subject: [PATCH 13/54] oops --- libs/common/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index 2a2baae..0189d13 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -21,7 +21,6 @@ tracing = { version = "0.1", default-features = false, features = ["std", "attri serde_json = { version = "1.0", default-features = false, features = ["std"] } tokio = { version = "1.28", default-features = false, features = ["rt", "rt-multi-thread"]} url = { version = "2.3.1", default-features = false } -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } rand_core = { version = "0.6.4", default-features = false, features = ["std"] } async-trait = { version = "0.1", default-features = false } backoff = { version = "0.4", default-features = false } From 8f822f27b45607c0c38bacc2d48ae84708161650 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 25 May 2023 18:15:30 -0300 Subject: [PATCH 14/54] device feature come from external crate --- libs/common/Cargo.toml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index 0189d13..dfb7fa0 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -24,18 +24,9 @@ url = { version = "2.3.1", default-features = false } rand_core = { version = "0.6.4", default-features = false, features = ["std"] } async-trait = { version = "0.1", default-features = false } backoff = { version = "0.4", default-features = false } +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false } macros = { path = "../../macros" } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] swift-bridge = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } - -[target.'cfg(target_os = "linux")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } - -[target.'cfg(target_os = "android")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false, features = ["device"] } - -[target.'cfg(target_os = "windows")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false } From 98efcaa785cde0d94d30e11f5bf5f4e32699965d Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 18:57:02 -0300 Subject: [PATCH 15/54] simplify workflow file by using Swantinem's cache --- .github/workflows/build.yml | 39 +++---------------------------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a745a75..1cabe07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,20 +43,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/.crates.toml - ~/.cargo/.crates2.json - ~/.cargo/.package-cache - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Update toolchain run: rustup update --no-self-update ${{ matrix.rust }} && rustup default ${{ matrix.rust }} && rustup component add clippy + - uses: Swatinem/rust-cache@v2 - name: Run cargo static analysis checks run: | cargo check @@ -75,18 +64,7 @@ jobs: rust: [stable] steps: - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/.crates.toml - ~/.cargo/.crates2.json - ~/.cargo/.package-cache - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: Swatinem/rust-cache@v2 - name: Setup toolchain run: | rustup update --no-self-update ${{ matrix.rust }} \ @@ -134,18 +112,7 @@ jobs: rust: [stable] steps: - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/.crates.toml - ~/.cargo/.crates2.json - ~/.cargo/.package-cache - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: Swatinem/rust-cache@v2 - name: Setup toolchain run: | rustup update --no-self-update ${{ matrix.rust }} \ From 7d36b565fcb9470d9f1326d42494b931bbae2872 Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 20:30:02 -0300 Subject: [PATCH 16/54] add ci workflow line for debug --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1cabe07..45cf533 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,6 +48,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run cargo static analysis checks run: | + cargo tree -e features -i boringtun cargo check cargo clippy -- -D clippy::all cargo test From d7fdaa85f23726043dace333ed4252074c185b8e Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 21:01:12 -0300 Subject: [PATCH 17/54] more ci debug --- .github/workflows/build.yml | 2 +- libs/common/src/error.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45cf533..882b825 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: - name: Run cargo static analysis checks run: | cargo tree -e features -i boringtun - cargo check + cargo check -p libs-commons cargo clippy -- -D clippy::all cargo test diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs index 9b74dae..79c84ae 100644 --- a/libs/common/src/error.rs +++ b/libs/common/src/error.rs @@ -44,6 +44,7 @@ pub enum ConnlibError { #[error("Error while stablishing connection between peers")] ConnectionStablishError, /// Error regarding boringtun's devices + #[cfg(device)] #[error("Error while using boringtun's device")] BoringtunError(#[from] boringtun::device::Error), /// Error related to wireguard protocol. From c9e631d1f048b3f2be40aafb437e91e964773ab3 Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 21:04:27 -0300 Subject: [PATCH 18/54] oooops --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 882b825..07f5d71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: - name: Run cargo static analysis checks run: | cargo tree -e features -i boringtun - cargo check -p libs-commons + cargo check -p libs-common cargo clippy -- -D clippy::all cargo test From 82771d40b18d3d5bb958e734db0e2e2649dd142a Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 21:08:47 -0300 Subject: [PATCH 19/54] more oops --- libs/common/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs index 79c84ae..7eda14a 100644 --- a/libs/common/src/error.rs +++ b/libs/common/src/error.rs @@ -44,7 +44,7 @@ pub enum ConnlibError { #[error("Error while stablishing connection between peers")] ConnectionStablishError, /// Error regarding boringtun's devices - #[cfg(device)] + #[cfg(feature = "device")] #[error("Error while using boringtun's device")] BoringtunError(#[from] boringtun::device::Error), /// Error related to wireguard protocol. From 3c62965428bc0d7fefeac76d538757e0c7b66070 Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 21:47:33 -0300 Subject: [PATCH 20/54] more debugging the ci --- .github/workflows/build.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07f5d71..c8e028d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,22 @@ jobs: - name: Run cargo static analysis checks run: | cargo tree -e features -i boringtun + echo "common" cargo check -p libs-common + echo "macros" + cargo check -p macros + echo "gateway" + cargo check -p gateway + echo "libs client" + cargo check -p firezone-client-connlib + echo "libs tunnel" + cargo check -p firezone-tunnel + echo "libs gateway" + cargo check -p firezone-gateway-connlib + echo "libs apple" + cargo check -p connlib-apple + echo "libs android" + cargo check -p connlib-android cargo clippy -- -D clippy::all cargo test From b976e59127ae9ff344d3fc868d8e2cd87e3065bc Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 21:54:54 -0300 Subject: [PATCH 21/54] more more debugging the ci --- .github/workflows/build.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8e028d..94c6f3f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,21 +49,29 @@ jobs: - name: Run cargo static analysis checks run: | cargo tree -e features -i boringtun - echo "common" - cargo check -p libs-common echo "macros" + cargo tree -e features -i boringtun -p macros cargo check -p macros - echo "gateway" - cargo check -p gateway - echo "libs client" - cargo check -p firezone-client-connlib + echo "common" + cargo tree -e features -i boringtun -p libs-common + cargo check -p libs-common echo "libs tunnel" + cargo tree -e features -i boringtun -p firezone-tunnel cargo check -p firezone-tunnel echo "libs gateway" + cargo tree -e features -i boringtun -p firezone-gateway-connlib cargo check -p firezone-gateway-connlib + echo "libs client" + cargo tree -e features -i boringtun -p firezone-client-connlib + cargo check -p firezone-client-connlib + echo "gateway" + cargo tree -e features -i boringtun -p gateway + cargo check -p gateway echo "libs apple" + cargo tree -e features -i boringtun -p connlib-apple cargo check -p connlib-apple echo "libs android" + cargo tree -e features -i boringtun -p connlib-android cargo check -p connlib-android cargo clippy -- -D clippy::all cargo test From 6d44fcbc306d404fc0f022887ac041d3ca1c9159 Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 26 May 2023 21:57:54 -0300 Subject: [PATCH 22/54] oops again again --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94c6f3f..bd91de8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,6 @@ jobs: run: | cargo tree -e features -i boringtun echo "macros" - cargo tree -e features -i boringtun -p macros cargo check -p macros echo "common" cargo tree -e features -i boringtun -p libs-common From 1a1edc2794ed02185bf055e1d74682f2bff0221d Mon Sep 17 00:00:00 2001 From: conectado Date: Mon, 29 May 2023 22:55:50 -0300 Subject: [PATCH 23/54] use local tun impls so that we don't depend on device features --- .github/workflows/build.yml | 25 +- libs/common/Cargo.toml | 1 - libs/common/src/error.rs | 13 +- libs/tunnel/Cargo.toml | 9 +- libs/tunnel/src/device_channel_unix.rs | 70 ++++++ libs/tunnel/src/device_channel_windows.rs | 28 +++ libs/tunnel/src/lib.rs | 56 +++-- libs/tunnel/src/tun.rs | 75 ------ libs/tunnel/src/tun_darwin.rs | 278 ++++++++++++++++++++++ libs/tunnel/src/tun_linux.rs | 175 ++++++++++++++ libs/tunnel/src/tun_win.rs | 17 ++ libs/tunnel/src/win_tun.rs | 39 --- 12 files changed, 613 insertions(+), 173 deletions(-) create mode 100644 libs/tunnel/src/device_channel_unix.rs create mode 100644 libs/tunnel/src/device_channel_windows.rs delete mode 100644 libs/tunnel/src/tun.rs create mode 100644 libs/tunnel/src/tun_darwin.rs create mode 100644 libs/tunnel/src/tun_win.rs delete mode 100644 libs/tunnel/src/win_tun.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd91de8..a8145ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,30 +48,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run cargo static analysis checks run: | - cargo tree -e features -i boringtun - echo "macros" - cargo check -p macros - echo "common" - cargo tree -e features -i boringtun -p libs-common - cargo check -p libs-common - echo "libs tunnel" - cargo tree -e features -i boringtun -p firezone-tunnel - cargo check -p firezone-tunnel - echo "libs gateway" - cargo tree -e features -i boringtun -p firezone-gateway-connlib - cargo check -p firezone-gateway-connlib - echo "libs client" - cargo tree -e features -i boringtun -p firezone-client-connlib - cargo check -p firezone-client-connlib - echo "gateway" - cargo tree -e features -i boringtun -p gateway - cargo check -p gateway - echo "libs apple" - cargo tree -e features -i boringtun -p connlib-apple - cargo check -p connlib-apple - echo "libs android" - cargo tree -e features -i boringtun -p connlib-android - cargo check -p connlib-android + cargo check --workspace cargo clippy -- -D clippy::all cargo test diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index dfb7fa0..3a8b070 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -device = ["boringtun/device"] jni-bindings = ["boringtun/jni-bindings"] [dependencies] diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs index 7eda14a..1df24b5 100644 --- a/libs/common/src/error.rs +++ b/libs/common/src/error.rs @@ -43,10 +43,6 @@ pub enum ConnlibError { /// Error when trying to stablish connection between peers. #[error("Error while stablishing connection between peers")] ConnectionStablishError, - /// Error regarding boringtun's devices - #[cfg(feature = "device")] - #[error("Error while using boringtun's device")] - BoringtunError(#[from] boringtun::device::Error), /// Error related to wireguard protocol. #[error("Wireguard error")] WireguardError(WireGuardError), @@ -59,12 +55,15 @@ pub enum ConnlibError { /// Error regarding our own control protocol. #[error("Control plane protocol error. Unexpected messages or message order.")] ControlProtocolError, - /// Error when manipulating system's interface - #[error("Error while manipulating system's interface")] - IFaceError, + /// Error when reading system's interface + #[error("Error while reading system's interface")] + IfaceRead(std::io::Error), /// Glob for errors without a type. #[error("Other error")] Other(&'static str), + /// Invalid tunnel name + #[error("Invalid tunnel name")] + InvalidTunnelName, } /// Type auto-generated by [SwiftEnum] intended to be used with rust-swift-bridge. diff --git a/libs/tunnel/Cargo.toml b/libs/tunnel/Cargo.toml index 42d1ab2..ff331b3 100644 --- a/libs/tunnel/Cargo.toml +++ b/libs/tunnel/Cargo.toml @@ -15,26 +15,21 @@ tracing = { version = "0.1", default-features = false, features = ["std", "attri parking_lot = { version = "0.12", default-features = false } bytes = { version = "1.4", default-features = false, features = ["std"] } itertools = { version = "0.10", default-features = false, features = ["use_std"] } +libs-common = { path = "../common" } +libc = { version = "0.2", default-features = false, features = ["std", "const-extern-fn", "extra_traits"] } # TODO: research replacing for https://github.com/algesten/str0m webrtc = { version = "0.7" } -# Apple tunnel dependencies -[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -libs-common = { path = "../common", features = ["device"] } - # Linux tunnel dependencies [target.'cfg(target_os = "linux")'.dependencies] -libs-common = { path = "../common", features = ["device"] } rtnetlink = "0.12" # Android tunnel dependencies [target.'cfg(target_os = "android")'.dependencies] -libs-common = { path = "../common", features = ["jni-bindings"] } android_logger = "0.13" log = "0.4.14" # Windows tunnel dependencies [target.'cfg(target_os = "windows")'.dependencies] -libs-common = { path = "../common" } wintun = "0.2.1" diff --git a/libs/tunnel/src/device_channel_unix.rs b/libs/tunnel/src/device_channel_unix.rs new file mode 100644 index 0000000..1eb0186 --- /dev/null +++ b/libs/tunnel/src/device_channel_unix.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use libs_common::{Error, Result}; +use tokio::io::unix::AsyncFd; + +use crate::tun::{IfaceConfig, IfaceDevice}; + +#[derive(Debug)] +pub(crate) struct DeviceChannel(AsyncFd>); + +impl DeviceChannel { + pub(crate) async fn mtu(&self) -> Result { + self.0.get_ref().mtu() + } + + pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { + loop { + let mut guard = self.0.readable().await?; + + match guard.try_io(|inner| { + inner.get_ref().read(out).map_err(|err| match err { + Error::IfaceRead(e) => e, + _ => panic!("Unexpected error while trying to read network interface"), + }) + }) { + Ok(result) => return result.map(|e| e.len()), + Err(_would_block) => continue, + } + } + } + + pub(crate) async fn write4(&self, buf: &[u8]) -> std::io::Result { + loop { + let mut guard = self.0.writable().await?; + + // write4 and write6 does the same + match guard.try_io(|inner| match inner.get_ref().write4(buf) { + 0 => Err(std::io::Error::last_os_error()), + i => Ok(i), + }) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } + + pub(crate) async fn write6(&self, buf: &[u8]) -> std::io::Result { + loop { + let mut guard = self.0.writable().await?; + + // write4 and write6 does the same + match guard.try_io(|inner| match inner.get_ref().write6(buf) { + 0 => Err(std::io::Error::last_os_error()), + i => Ok(i), + }) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } +} + +pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> { + let dev = Arc::new(IfaceDevice::new("utun")?.set_non_blocking()?); + let async_dev = Arc::clone(&dev); + let device_channel = DeviceChannel(AsyncFd::new(async_dev)?); + let iface_config = IfaceConfig(dev); + + Ok((iface_config, device_channel)) +} diff --git a/libs/tunnel/src/device_channel_windows.rs b/libs/tunnel/src/device_channel_windows.rs new file mode 100644 index 0000000..90fa6a0 --- /dev/null +++ b/libs/tunnel/src/device_channel_windows.rs @@ -0,0 +1,28 @@ +use super::InterfaceConfig; +use crate::tun::IfaceConfig; +use libs_common::Result; + +#[derive(Debug)] +pub(crate) struct DeviceChannel; + +impl DeviceChannel { + pub(crate) async fn mtu(&self) -> Result { + todo!() + } + + pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { + todo!() + } + + pub(crate) async fn write4(&self, buf: &[u8]) -> std::io::Result { + todo!() + } + + pub(crate) async fn write6(&self, buf: &[u8]) -> std::io::Result { + todo!() + } +} + +pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> { + todo!() +} diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 7f201a1..c4b321f 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -43,12 +43,8 @@ use libs_common::{ Result, }; -#[cfg(target_os = "windows")] -use win_tun as tun; - -use tun::create_iface; -use tun::DeviceChannel; -use tun::IfaceDevice; +use device_channel::{create_iface, DeviceChannel}; +use tun::IfaceConfig; pub use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; @@ -62,11 +58,25 @@ mod peer; // TODO: For now all tunnel implementations are the same // will divide when we start introducing differences. -#[cfg(not(target_os = "windows"))] +#[cfg(target_os = "windows")] +#[path = "tun_windows.rs"] mod tun; -#[cfg(target_os = "windows")] -mod win_tun; +#[cfg(any(target_os = "windows"))] +#[path = "device_channel_windows.rs"] +mod device_channel; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[path = "tun_darwin.rs"] +mod tun; + +#[cfg(target_os = "linux")] +#[path = "tun_linux.rs"] +mod tun; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[path = "device_channel_unix.rs"] +mod device_channel; const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1); const REFRESH_PEERS_TIEMRS_INTERVAL: Duration = Duration::from_secs(1); @@ -119,7 +129,7 @@ pub trait ControlSignal { /// to communicate between peers. pub struct Tunnel { next_index: Mutex, - iface_device: Mutex, + iface_config: Mutex, device_channel: Arc, rate_limiter: Arc, private_key: StaticSecret, @@ -151,8 +161,8 @@ where let rate_limiter = Arc::new(RateLimiter::new(&public_key, HANDSHAKE_RATE_LIMIT)); let peers_by_ip = Default::default(); let next_index = Default::default(); - let (iface_device, device_channel) = create_iface().await?; - let iface_device = Mutex::new(iface_device); + let (iface_config, device_channel) = create_iface().await?; + let iface_config = Mutex::new(iface_config); let device_channel = Arc::new(device_channel); let peer_connections = Default::default(); let resources = Default::default(); @@ -185,7 +195,7 @@ where peers_by_ip, next_index, webrtc_api, - iface_device, + iface_config, device_channel, resources, awaiting_connection, @@ -213,11 +223,11 @@ where #[tracing::instrument(level = "trace", skip(self))] pub async fn set_interface(self: &Arc, config: &InterfaceConfig) -> Result<()> { { - let mut iface_device = self.iface_device.lock(); - iface_device + let mut iface_config = self.iface_config.lock(); + iface_config .set_iface_config(config) .expect("Couldn't initiate interface"); - iface_device.up().expect("Couldn't initiate interface"); + iface_config.up().expect("Couldn't initiate interface"); } self.start_timers(); @@ -346,12 +356,12 @@ where } TunnResult::WriteToTunnelV4(packet, addr) => { if peer.is_allowed_ipv4(&addr) { - tunnel.write_device_infallible(packet).await; + tunnel.write4_device_infallible(packet).await; } } TunnResult::WriteToTunnelV6(packet, addr) => { if peer.is_allowed_ipv6(&addr) { - tunnel.write_device_infallible(packet).await; + tunnel.write6_device_infallible(packet).await; } } }; @@ -369,8 +379,14 @@ where }); } - async fn write_device_infallible(&self, packet: &[u8]) { - if let Err(e) = self.device_channel.write(packet).await { + async fn write4_device_infallible(&self, packet: &[u8]) { + if let Err(e) = self.device_channel.write4(packet).await { + CB::on_error(&e.into(), Recoverable); + } + } + + async fn write6_device_infallible(&self, packet: &[u8]) { + if let Err(e) = self.device_channel.write6(packet).await { CB::on_error(&e.into(), Recoverable); } } diff --git a/libs/tunnel/src/tun.rs b/libs/tunnel/src/tun.rs deleted file mode 100644 index c964cee..0000000 --- a/libs/tunnel/src/tun.rs +++ /dev/null @@ -1,75 +0,0 @@ -use libs_common::{boringtun::device::tun::TunSocket, Result}; -use std::os::fd::{AsRawFd, RawFd}; -use tokio::io::unix::AsyncFd; - -use super::InterfaceConfig; - -// TODO: we have to replace TunSocket because we need to use netpacketprovider to get approved in the app store -#[derive(Debug)] -pub(crate) struct DeviceChannel(tokio::io::unix::AsyncFd); - -#[derive(Debug)] -pub(crate) struct IfaceDevice { - _fd: RawFd, -} - -impl DeviceChannel { - pub(crate) async fn mtu(&self) -> Result { - Ok(self.0.get_ref().mtu()?) - } - - pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { - loop { - let mut guard = self.0.readable().await?; - - match guard.try_io(|inner| { - inner.get_ref().read(out).map_err(|err| match err { - libs_common::boringtun::device::Error::IfaceRead(e) => e, - _ => panic!("Unexpected error while trying to read network interface"), - }) - }) { - Ok(result) => return result.map(|e| e.len()), - Err(_would_block) => continue, - } - } - } - - pub(crate) async fn write(&self, buf: &[u8]) -> std::io::Result { - loop { - let mut guard = self.0.writable().await?; - - // write4 and write6 does the same - match guard.try_io(|inner| match inner.get_ref().write4(buf) { - 0 => Err(std::io::Error::last_os_error()), - i => Ok(i), - }) { - Ok(result) => return result, - Err(_would_block) => continue, - } - } - } -} - -impl IfaceDevice { - // It's easier to not make these functions async, setting these should not block the thread for too long - #[tracing::instrument(level = "trace", skip(self))] - pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { - // TODO - - Ok(()) - } - - pub fn up(&mut self) -> Result<()> { - // TODO - Ok(()) - } -} - -pub(crate) async fn create_iface() -> Result<(IfaceDevice, DeviceChannel)> { - let dev = TunSocket::new("utun").unwrap().set_non_blocking().unwrap(); - let _fd = dev.as_raw_fd(); - tracing::trace!("Started new interface with name: {:?}", dev.name()); - let dev = AsyncFd::new(dev)?; - - Ok((IfaceDevice { _fd }, DeviceChannel(dev))) -} diff --git a/libs/tunnel/src/tun_darwin.rs b/libs/tunnel/src/tun_darwin.rs new file mode 100644 index 0000000..4836091 --- /dev/null +++ b/libs/tunnel/src/tun_darwin.rs @@ -0,0 +1,278 @@ +use libc::{ + close, connect, ctl_info, fcntl, getsockopt, ioctl, iovec, msghdr, recvmsg, sendmsg, sockaddr, + sockaddr_ctl, sockaddr_in, socket, socklen_t, AF_INET, AF_INET6, AF_SYSTEM, AF_SYS_CONTROL, + CTLIOCGINFO, F_GETFL, F_SETFL, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, PF_SYSTEM, SOCK_DGRAM, + SOCK_STREAM, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, +}; +use libs_common::{Error, Result}; +use std::{ + ffi::{c_int, c_short, c_uchar}, + io, + mem::{size_of, size_of_val}, + os::fd::{AsRawFd, RawFd}, + sync::Arc, +}; + +use super::InterfaceConfig; + +const CTRL_NAME: &[u8] = b"com.apple.net.utun_control"; +const SIOCGIFMTU: u64 = 0x0000_0000_c020_6933; + +#[derive(Debug)] +pub(crate) struct IfaceConfig(pub(crate) Arc); + +#[derive(Debug)] +pub(crate) struct IfaceDevice { + fd: RawFd, +} + +impl AsRawFd for IfaceDevice { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl Drop for IfaceDevice { + fn drop(&mut self) { + unsafe { close(self.fd) }; + } +} +// For some reason this is not available in libc for darwin :c +#[repr(C)] +pub struct ifreq { + ifr_name: [c_uchar; IF_NAMESIZE], + ifr_ifru: IfrIfru, +} + +#[repr(C)] +union IfrIfru { + ifru_addr: sockaddr, + ifru_addr_v4: sockaddr_in, + ifru_addr_v6: sockaddr_in, + ifru_dstaddr: sockaddr, + ifru_broadaddr: sockaddr, + ifru_flags: c_short, + ifru_metric: c_int, + ifru_mtu: c_int, + ifru_phys: c_int, + ifru_media: c_int, + ifru_intval: c_int, + ifru_wake_flags: u32, + ifru_route_refcnt: u32, + ifru_cap: [c_int; 2], + ifru_functional_type: u32, +} + +// On Darwin tunnel can only be named utunXXX +pub fn parse_utun_name(name: &str) -> Result { + if !name.starts_with("utun") { + return Err(Error::InvalidTunnelName); + } + + match name.get(4..) { + None | Some("") => { + // The name is simply "utun" + Ok(0) + } + Some(idx) => { + // Everything past utun should represent an integer index + idx.parse::() + .map_err(|_| Error::InvalidTunnelName) + .map(|x| x + 1) + } + } +} + +impl IfaceDevice { + fn write(&self, src: &[u8], af: u8) -> usize { + let mut hdr = [0u8, 0u8, 0u8, af]; + let mut iov = [ + iovec { + iov_base: hdr.as_mut_ptr() as _, + iov_len: hdr.len(), + }, + iovec { + iov_base: src.as_ptr() as _, + iov_len: src.len(), + }, + ]; + + let msg_hdr = msghdr { + msg_name: std::ptr::null_mut(), + msg_namelen: 0, + msg_iov: &mut iov[0], + msg_iovlen: iov.len() as _, + msg_control: std::ptr::null_mut(), + msg_controllen: 0, + msg_flags: 0, + }; + + match unsafe { sendmsg(self.fd, &msg_hdr, 0) } { + -1 => 0, + n => n as usize, + } + } + + pub fn new(name: &str) -> Result { + let idx = parse_utun_name(name)?; + + let fd = match unsafe { socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL) } { + -1 => return Err(get_last_error()), + fd => fd, + }; + + let mut info = ctl_info { + ctl_id: 0, + ctl_name: [0i8; 96], + }; + info.ctl_name[..CTRL_NAME.len()] + // SAFETY: We only care about mantaing the same byte value not the same value, + // meaning that the slice &[u8] here is just a blob of bytes for us, we need this conversion + // just because `c_char` is i8 (for some reason). + // One thing I don't like about this is that `ctl_name` is actually a nul-terminated string, + // which we are only getting because `CTRL_NAME` is less than 96 bytes long and we are 0-value + // initializing the array we should be using a CStr to be explicit... but this is slightly easier. + .copy_from_slice(unsafe { &*(CTRL_NAME as *const [u8] as *const [i8]) }); + + if unsafe { ioctl(fd, CTLIOCGINFO, &mut info as *mut ctl_info) } < 0 { + unsafe { close(fd) }; + return Err(get_last_error()); + } + + let addr = sockaddr_ctl { + sc_len: size_of::() as u8, + sc_family: AF_SYSTEM as u8, + ss_sysaddr: AF_SYS_CONTROL as u16, + sc_id: info.ctl_id, + sc_unit: idx, + sc_reserved: Default::default(), + }; + + if unsafe { + connect( + fd, + &addr as *const sockaddr_ctl as _, + size_of_val(&addr) as _, + ) + } < 0 + { + unsafe { close(fd) }; + return Err(get_last_error()); + } + + Ok(Self { fd }) + } + + pub fn set_non_blocking(self) -> Result { + match unsafe { fcntl(self.fd, F_GETFL) } { + -1 => Err(get_last_error()), + flags => match unsafe { fcntl(self.fd, F_SETFL, flags | O_NONBLOCK) } { + -1 => Err(get_last_error()), + _ => Ok(self), + }, + } + } + + pub fn name(&self) -> Result { + let mut tunnel_name = [0u8; 256]; + let mut tunnel_name_len: socklen_t = tunnel_name.len() as u32; + if unsafe { + getsockopt( + self.fd, + SYSPROTO_CONTROL, + UTUN_OPT_IFNAME, + tunnel_name.as_mut_ptr() as _, + &mut tunnel_name_len, + ) + } < 0 + || tunnel_name_len == 0 + { + return Err(get_last_error()); + } + + Ok(String::from_utf8_lossy(&tunnel_name[..(tunnel_name_len - 1) as usize]).to_string()) + } + + /// Get the current MTU value + pub fn mtu(&self) -> Result { + let fd = match unsafe { socket(AF_INET, SOCK_STREAM, IPPROTO_IP) } { + -1 => return Err(get_last_error()), + fd => fd, + }; + + let name = self.name()?; + let iface_name: &[u8] = name.as_ref(); + let mut ifr = ifreq { + ifr_name: [0; IF_NAMESIZE], + ifr_ifru: IfrIfru { ifru_mtu: 0 }, + }; + + ifr.ifr_name[..iface_name.len()].copy_from_slice(iface_name); + + if unsafe { ioctl(fd, SIOCGIFMTU, &ifr) } < 0 { + return Err(get_last_error()); + } + + unsafe { close(fd) }; + + Ok(unsafe { ifr.ifr_ifru.ifru_mtu } as _) + } + + pub fn write4(&self, src: &[u8]) -> usize { + self.write(src, AF_INET as u8) + } + + pub fn write6(&self, src: &[u8]) -> usize { + self.write(src, AF_INET6 as u8) + } + + pub fn read<'a>(&self, dst: &'a mut [u8]) -> Result<&'a mut [u8]> { + let mut hdr = [0u8; 4]; + + let mut iov = [ + iovec { + iov_base: hdr.as_mut_ptr() as _, + iov_len: hdr.len(), + }, + iovec { + iov_base: dst.as_mut_ptr() as _, + iov_len: dst.len(), + }, + ]; + + let mut msg_hdr = msghdr { + msg_name: std::ptr::null_mut(), + msg_namelen: 0, + msg_iov: &mut iov[0], + msg_iovlen: iov.len() as _, + msg_control: std::ptr::null_mut(), + msg_controllen: 0, + msg_flags: 0, + }; + + match unsafe { recvmsg(self.fd, &mut msg_hdr, 0) } { + -1 => Err(Error::IfaceRead(io::Error::last_os_error())), + 0..=4 => Ok(&mut dst[..0]), + n => Ok(&mut dst[..(n - 4) as usize]), + } + } +} + +// So, these functions take a mutable &self, this is not neccesary in theory but it's correct! +impl IfaceConfig { + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + // TODO + + Ok(()) + } + + pub fn up(&mut self) -> Result<()> { + // TODO + Ok(()) + } +} + +fn get_last_error() -> Error { + Error::Io(io::Error::last_os_error()) +} diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index 8b13789..6a57a20 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -1 +1,176 @@ +use libc::{ + close, connect, ctl_info, fcntl, getsockopt, ioctl, iovec, msghdr, recvmsg, sendmsg, sockaddr, + sockaddr_ctl, sockaddr_in, socket, socklen_t, AF_INET, AF_INET6, AF_SYSTEM, AF_SYS_CONTROL, + CTLIOCGINFO, F_GETFL, F_SETFL, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, PF_SYSTEM, SOCK_DGRAM, + SOCK_STREAM, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, +}; +use libs_common::{Error, Result}; +use std::{ + ffi::{c_int, c_short, c_uchar}, + io, + mem::{size_of, size_of_val}, + os::fd::{AsRawFd, RawFd}, + sync::Arc, +}; +use super::InterfaceConfig; + +#[derive(Debug)] +pub(crate) struct IfaceConfig; + +const TUNSETIFF: u64 = 0x4004_54ca; +const TUN_FILE: &[u8] = b"/dev/net/tun\0"; + +#[repr(C)] +union IfrIfru { + ifru_addr: sockaddr, + ifru_addr_v4: sockaddr_in, + ifru_addr_v6: sockaddr_in, + ifru_dstaddr: sockaddr, + ifru_broadaddr: sockaddr, + ifru_flags: c_short, + ifru_metric: c_int, + ifru_mtu: c_int, + ifru_phys: c_int, + ifru_media: c_int, + ifru_intval: c_int, + ifru_wake_flags: u32, + ifru_route_refcnt: u32, + ifru_cap: [c_int; 2], + ifru_functional_type: u32, +} + +#[repr(C)] +pub struct ifreq { + ifr_name: [c_uchar; IFNAMSIZ], + ifr_ifru: IfrIfru, +} + +#[derive(Default, Debug)] +pub struct IfaceDevice { + fd: RawFd, + name: String, +} + +impl Drop for IfaceDevice { + fn drop(&mut self) { + unsafe { close(self.fd) }; + } +} + +impl AsRawFd for IfaceDevice { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl IfaceDevice { + fn write(&self, buf: &[u8]) -> usize { + match unsafe { write(self.fd, buf.as_ptr() as _, buf.len() as _) } { + -1 => 0, + n => n as usize, + } + } + + pub fn new(name: &str) -> Result { + let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } { + -1 => return Err(Error::Socket(io::Error::last_os_error())), + fd => fd, + }; + + let iface_name = name.as_bytes(); + let mut ifr = ifreq { + ifr_name: [0; IFNAMSIZ], + ifr_ifru: IfrIfru { + ifru_flags: (IFF_TUN | IFF_NO_PI | IFF_MULTI_QUEUE) as _, + }, + }; + + if iface_name.len() >= ifr.ifr_name.len() { + return Err(Error::InvalidTunnelName); + } + + ifr.ifr_name[..iface_name.len()].copy_from_slice(iface_name); + + if unsafe { ioctl(fd, TUNSETIFF as _, &ifr) } < 0 { + return Err(get_last_error()); + } + + let name = name.to_string(); + Ok(TunSocket { fd, name }) + } + + pub fn set_non_blocking(self) -> Result { + match unsafe { fcntl(self.fd, F_GETFL) } { + -1 => Err(Error::FCntl(io::Error::last_os_error())), + flags => match unsafe { fcntl(self.fd, F_SETFL, flags | O_NONBLOCK) } { + -1 => Err(get_last_error()), + _ => Ok(self), + }, + } + } + + pub fn name(&self) -> Result { + Ok(self.name.clone()) + } + + /// Get the current MTU value + pub fn mtu(&self) -> Result { + let provided_fd = self.name.parse::(); + if provided_fd.is_ok() { + return Ok(1500); + } + + let fd = match unsafe { socket(AF_INET, SOCK_STREAM, IPPROTO_IP) } { + -1 => return Err(get_last_error()), + fd => fd, + }; + + let name = self.name()?; + let iface_name: &[u8] = name.as_ref(); + let mut ifr = ifreq { + ifr_name: [0; IF_NAMESIZE], + ifr_ifru: IfrIfru { ifru_mtu: 0 }, + }; + + ifr.ifr_name[..iface_name.len()].copy_from_slice(iface_name); + + if unsafe { ioctl(fd, SIOCGIFMTU as _, &ifr) } < 0 { + return Err(get_last_error()); + } + + unsafe { close(fd) }; + + Ok(unsafe { ifr.ifr_ifru.ifru_mtu } as _) + } + + pub fn write4(&self, src: &[u8]) -> usize { + self.write(src) + } + + pub fn write6(&self, src: &[u8]) -> usize { + self.write(src) + } + + pub fn read<'a>(&self, dst: &'a mut [u8]) -> Result<&'a mut [u8]> { + match unsafe { read(self.fd, dst.as_mut_ptr() as _, dst.len()) } { + -1 => Err(Error::IfaceRead(io::Error::last_os_error())), + n => Ok(&mut dst[..n as usize]), + } + } +} + +fn get_last_error() -> Error { + Error::Io(io::Error::last_os_error()) +} + +impl IfaceConfig { + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + todo!() + } + + pub fn up(&mut self) -> Result<()> { + todo!() + } +} diff --git a/libs/tunnel/src/tun_win.rs b/libs/tunnel/src/tun_win.rs new file mode 100644 index 0000000..cf3e942 --- /dev/null +++ b/libs/tunnel/src/tun_win.rs @@ -0,0 +1,17 @@ +use super::InterfaceConfig; +use libs_common::Result; + +#[derive(Debug)] +pub(crate) struct IfaceConfig; + +impl IfaceConfig { + // It's easier to not make these functions async, setting these should not block the thread for too long + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + todo!() + } + + pub fn up(&mut self) -> Result<()> { + todo!() + } +} diff --git a/libs/tunnel/src/win_tun.rs b/libs/tunnel/src/win_tun.rs deleted file mode 100644 index 43c0593..0000000 --- a/libs/tunnel/src/win_tun.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::InterfaceConfig; -use libs_common::Result; - -// This is an stubbed out module to be able to compile on windows. -#[derive(Debug)] -pub(crate) struct DeviceChannel; - -#[derive(Debug)] -pub(crate) struct IfaceDevice; - -impl DeviceChannel { - pub(crate) async fn mtu(&self) -> Result { - todo!() - } - - pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { - todo!() - } - - pub(crate) async fn write(&self, buf: &[u8]) -> std::io::Result { - todo!() - } -} - -impl IfaceDevice { - // It's easier to not make these functions async, setting these should not block the thread for too long - #[tracing::instrument(level = "trace", skip(self))] - pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { - todo!() - } - - pub fn up(&mut self) -> Result<()> { - todo!() - } -} - -pub(crate) async fn create_iface() -> Result<(IfaceDevice, DeviceChannel)> { - todo!() -} From 6d0e6e352c096c45024b3b14d726e332d12c92c8 Mon Sep 17 00:00:00 2001 From: conectado Date: Mon, 29 May 2023 23:25:15 -0300 Subject: [PATCH 24/54] fix linux tun impl --- libs/tunnel/src/tun_linux.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index 6a57a20..d0a9c43 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -1,14 +1,12 @@ use libc::{ - close, connect, ctl_info, fcntl, getsockopt, ioctl, iovec, msghdr, recvmsg, sendmsg, sockaddr, - sockaddr_ctl, sockaddr_in, socket, socklen_t, AF_INET, AF_INET6, AF_SYSTEM, AF_SYS_CONTROL, - CTLIOCGINFO, F_GETFL, F_SETFL, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, PF_SYSTEM, SOCK_DGRAM, - SOCK_STREAM, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, + close, fcntl, ioctl, sockaddr, sockaddr_in, socket, write, AF_INET, F_GETFL, F_SETFL, + IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, O_RDWR, + SIOCGIFMTU, SOCK_STREAM, }; use libs_common::{Error, Result}; use std::{ ffi::{c_int, c_short, c_uchar}, io, - mem::{size_of, size_of_val}, os::fd::{AsRawFd, RawFd}, sync::Arc, }; @@ -16,7 +14,7 @@ use std::{ use super::InterfaceConfig; #[derive(Debug)] -pub(crate) struct IfaceConfig; +pub(crate) struct IfaceConfig(pub(crate) Arc); const TUNSETIFF: u64 = 0x4004_54ca; const TUN_FILE: &[u8] = b"/dev/net/tun\0"; @@ -74,7 +72,7 @@ impl IfaceDevice { pub fn new(name: &str) -> Result { let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } { - -1 => return Err(Error::Socket(io::Error::last_os_error())), + -1 => return Err(get_last_error()), fd => fd, }; @@ -102,7 +100,7 @@ impl IfaceDevice { pub fn set_non_blocking(self) -> Result { match unsafe { fcntl(self.fd, F_GETFL) } { - -1 => Err(Error::FCntl(io::Error::last_os_error())), + -1 => Err(get_last_error()), flags => match unsafe { fcntl(self.fd, F_SETFL, flags | O_NONBLOCK) } { -1 => Err(get_last_error()), _ => Ok(self), @@ -116,11 +114,6 @@ impl IfaceDevice { /// Get the current MTU value pub fn mtu(&self) -> Result { - let provided_fd = self.name.parse::(); - if provided_fd.is_ok() { - return Ok(1500); - } - let fd = match unsafe { socket(AF_INET, SOCK_STREAM, IPPROTO_IP) } { -1 => return Err(get_last_error()), fd => fd, From 7a1d8dd9874bd85c02f78c6ab1af83e47b41d6e0 Mon Sep 17 00:00:00 2001 From: conectado Date: Mon, 29 May 2023 23:37:13 -0300 Subject: [PATCH 25/54] forgot imports from libc --- libs/tunnel/src/tun_linux.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index d0a9c43..59bb67b 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -1,7 +1,7 @@ use libc::{ - close, fcntl, ioctl, sockaddr, sockaddr_in, socket, write, AF_INET, F_GETFL, F_SETFL, - IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, O_RDWR, - SIOCGIFMTU, SOCK_STREAM, + close, fcntl, ioctl, open, read, sockaddr, sockaddr_in, socket, write, AF_INET, F_GETFL, + F_SETFL, IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, + O_RDWR, SIOCGIFMTU, SOCK_STREAM, }; use libs_common::{Error, Result}; use std::{ From 97f2bea064a6a62ba25228cc39b19709fa1e7f5c Mon Sep 17 00:00:00 2001 From: conectado Date: Mon, 29 May 2023 23:43:15 -0300 Subject: [PATCH 26/54] fix imports --- libs/tunnel/src/lib.rs | 10 +++++----- libs/tunnel/src/tun_linux.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index c4b321f..67aa717 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -62,10 +62,6 @@ mod peer; #[path = "tun_windows.rs"] mod tun; -#[cfg(any(target_os = "windows"))] -#[path = "device_channel_windows.rs"] -mod device_channel; - #[cfg(any(target_os = "macos", target_os = "ios"))] #[path = "tun_darwin.rs"] mod tun; @@ -74,10 +70,14 @@ mod tun; #[path = "tun_linux.rs"] mod tun; -#[cfg(any(target_os = "macos", target_os = "ios"))] +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "linux"))] #[path = "device_channel_unix.rs"] mod device_channel; +#[cfg(any(target_os = "windows"))] +#[path = "device_channel_windows.rs"] +mod device_channel; + const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1); const REFRESH_PEERS_TIEMRS_INTERVAL: Duration = Duration::from_secs(1); diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index 59bb67b..dc64a74 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -95,7 +95,7 @@ impl IfaceDevice { } let name = name.to_string(); - Ok(TunSocket { fd, name }) + Ok(Self { fd, name }) } pub fn set_non_blocking(self) -> Result { From 9e32754dc97ef1374a767d8dfafa043588af695e Mon Sep 17 00:00:00 2001 From: conectado Date: Mon, 29 May 2023 23:46:40 -0300 Subject: [PATCH 27/54] agggh --- libs/tunnel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 67aa717..32137d2 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -74,7 +74,7 @@ mod tun; #[path = "device_channel_unix.rs"] mod device_channel; -#[cfg(any(target_os = "windows"))] +#[cfg(target_os = "windows")] #[path = "device_channel_windows.rs"] mod device_channel; From 22bc9df4325cae4397c4f6a549389c44da406ba9 Mon Sep 17 00:00:00 2001 From: conectado Date: Tue, 30 May 2023 00:45:37 -0300 Subject: [PATCH 28/54] fix windows module names --- .../src/{device_channel_windows.rs => device_channel_win.rs} | 0 libs/tunnel/src/lib.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename libs/tunnel/src/{device_channel_windows.rs => device_channel_win.rs} (100%) diff --git a/libs/tunnel/src/device_channel_windows.rs b/libs/tunnel/src/device_channel_win.rs similarity index 100% rename from libs/tunnel/src/device_channel_windows.rs rename to libs/tunnel/src/device_channel_win.rs diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 32137d2..9737d33 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -59,7 +59,7 @@ mod peer; // TODO: For now all tunnel implementations are the same // will divide when we start introducing differences. #[cfg(target_os = "windows")] -#[path = "tun_windows.rs"] +#[path = "tun_win.rs"] mod tun; #[cfg(any(target_os = "macos", target_os = "ios"))] @@ -75,7 +75,7 @@ mod tun; mod device_channel; #[cfg(target_os = "windows")] -#[path = "device_channel_windows.rs"] +#[path = "device_channel_win.rs"] mod device_channel; const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1); From 0453224905c431fa1d8679a1d5d81dd3f8090550 Mon Sep 17 00:00:00 2001 From: conectado Date: Tue, 30 May 2023 00:52:52 -0300 Subject: [PATCH 29/54] fix windows warnings --- libs/tunnel/src/device_channel_win.rs | 6 +++--- libs/tunnel/src/tun_win.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/tunnel/src/device_channel_win.rs b/libs/tunnel/src/device_channel_win.rs index 90fa6a0..3a1e4a3 100644 --- a/libs/tunnel/src/device_channel_win.rs +++ b/libs/tunnel/src/device_channel_win.rs @@ -10,15 +10,15 @@ impl DeviceChannel { todo!() } - pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { + pub(crate) async fn read(&self, _out: &mut [u8]) -> std::io::Result { todo!() } - pub(crate) async fn write4(&self, buf: &[u8]) -> std::io::Result { + pub(crate) async fn write4(&self, _buf: &[u8]) -> std::io::Result { todo!() } - pub(crate) async fn write6(&self, buf: &[u8]) -> std::io::Result { + pub(crate) async fn write6(&self, _buf: &[u8]) -> std::io::Result { todo!() } } diff --git a/libs/tunnel/src/tun_win.rs b/libs/tunnel/src/tun_win.rs index cf3e942..66fcf4e 100644 --- a/libs/tunnel/src/tun_win.rs +++ b/libs/tunnel/src/tun_win.rs @@ -7,7 +7,7 @@ pub(crate) struct IfaceConfig; impl IfaceConfig { // It's easier to not make these functions async, setting these should not block the thread for too long #[tracing::instrument(level = "trace", skip(self))] - pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + pub fn set_iface_config(&mut self, _config: &InterfaceConfig) -> Result<()> { todo!() } From b4d543ba7f2927e8f713ba97c713d4f8dd439c90 Mon Sep 17 00:00:00 2001 From: conectado Date: Tue, 30 May 2023 01:03:24 -0300 Subject: [PATCH 30/54] remove windows unused import --- libs/tunnel/src/device_channel_win.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/tunnel/src/device_channel_win.rs b/libs/tunnel/src/device_channel_win.rs index 3a1e4a3..2edf498 100644 --- a/libs/tunnel/src/device_channel_win.rs +++ b/libs/tunnel/src/device_channel_win.rs @@ -1,4 +1,3 @@ -use super::InterfaceConfig; use crate::tun::IfaceConfig; use libs_common::Result; From cd1eafe125e330fa0b4e1926c281fac1cdb4344b Mon Sep 17 00:00:00 2001 From: conectado Date: Tue, 30 May 2023 14:44:32 -0300 Subject: [PATCH 31/54] fix ci directories --- .github/workflows/build.yml | 7 ++++--- .gitignore | 2 +- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 61608 bytes 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 clients/android/gradle/wrapper/gradle-wrapper.jar diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8145ab..12442f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,8 +75,8 @@ jobs: - uses: actions/cache@v3 with: path: | - ~/.gradle/caches - ~/.gradle/wrapper + ~/clients/android/.gradle/caches + ~/clients/android/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- @@ -93,7 +93,7 @@ jobs: build-root-directory: android - name: Move artifact run: | - mv ./android/lib/build/outputs/aar/lib-release.aar ./connlib-${{ needs.draft-release.outputs.tag_name }}.aar + mv ./clients/android/lib/build/outputs/aar/lib-release.aar ./connlib-${{ needs.draft-release.outputs.tag_name }}.aar - uses: actions/upload-artifact@v3 with: name: connlib-android @@ -132,6 +132,7 @@ jobs: env: CONFIGURATION: Release PROJECT_DIR: . + working-directory: ./clients/apple run: | cd apple # build-xcframework.sh calls build-rust.sh indirectly via `xcodebuild`, but it pollutes the environment diff --git a/.gitignore b/.gitignore index 49605b4..5400d93 100644 --- a/.gitignore +++ b/.gitignore @@ -164,7 +164,7 @@ fabric.properties ### AndroidStudio Patch ### -!/gradle/wrapper/gradle-wrapper.jar +!clients/android/gradle/wrapper/gradle-wrapper.jar ### Apple ### .DS_Store diff --git a/clients/android/gradle/wrapper/gradle-wrapper.jar b/clients/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..ccebba7710deaf9f98673a68957ea02138b60d0a GIT binary patch literal 61608 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 literal 0 HcmV?d00001 From 96f94f6604cbef7aab3ff09c13725888cc1fe5ee Mon Sep 17 00:00:00 2001 From: conectado Date: Tue, 30 May 2023 14:53:28 -0300 Subject: [PATCH 32/54] fix ci for android and apple --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12442f1..5d25eb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,7 @@ jobs: uses: gradle/gradle-build-action@v2 with: arguments: build assembleRelease - build-root-directory: android + build-root-directory: clients/android - name: Move artifact run: | mv ./clients/android/lib/build/outputs/aar/lib-release.aar ./connlib-${{ needs.draft-release.outputs.tag_name }}.aar @@ -134,7 +134,6 @@ jobs: PROJECT_DIR: . working-directory: ./clients/apple run: | - cd apple # build-xcframework.sh calls build-rust.sh indirectly via `xcodebuild`, but it pollutes the environment # to the point that it causes the `ring` build to fail for the aarch64-apple-darwin target. So, explicitly # build first. See https://github.com/briansmith/ring/issues/1332 From 7ee4afeb8f879c1e79ecf3227066d1222cf09f2e Mon Sep 17 00:00:00 2001 From: conectado Date: Tue, 30 May 2023 19:20:13 -0300 Subject: [PATCH 33/54] fix android compilation --- libs/tunnel/src/lib.rs | 11 ++++++++++- libs/tunnel/src/tun_android.rs | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 libs/tunnel/src/tun_android.rs diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 9737d33..7ad80d8 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -70,7 +70,16 @@ mod tun; #[path = "tun_linux.rs"] mod tun; -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "linux"))] +#[cfg(target_os = "android")] +#[path = "tun_android.rs"] +mod tun; + +#[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "linux", + target_os = "android" +))] #[path = "device_channel_unix.rs"] mod device_channel; diff --git a/libs/tunnel/src/tun_android.rs b/libs/tunnel/src/tun_android.rs new file mode 100644 index 0000000..66fcf4e --- /dev/null +++ b/libs/tunnel/src/tun_android.rs @@ -0,0 +1,17 @@ +use super::InterfaceConfig; +use libs_common::Result; + +#[derive(Debug)] +pub(crate) struct IfaceConfig; + +impl IfaceConfig { + // It's easier to not make these functions async, setting these should not block the thread for too long + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, _config: &InterfaceConfig) -> Result<()> { + todo!() + } + + pub fn up(&mut self) -> Result<()> { + todo!() + } +} From dfa6695d8714c7fab3c8b860c6b6e7a9f7507c76 Mon Sep 17 00:00:00 2001 From: conectado Date: Tue, 30 May 2023 19:32:44 -0300 Subject: [PATCH 34/54] more android fixes --- libs/tunnel/src/tun_android.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/tunnel/src/tun_android.rs b/libs/tunnel/src/tun_android.rs index 66fcf4e..d47d94a 100644 --- a/libs/tunnel/src/tun_android.rs +++ b/libs/tunnel/src/tun_android.rs @@ -2,7 +2,10 @@ use super::InterfaceConfig; use libs_common::Result; #[derive(Debug)] -pub(crate) struct IfaceConfig; +pub(crate) struct IfaceConfig(pub(crate) Arc); + +#[derive(Debug)] +pub(crate) struct IfaceDevice; impl IfaceConfig { // It's easier to not make these functions async, setting these should not block the thread for too long From 0af5416e955477c1817499b1495a53dd607eb7bd Mon Sep 17 00:00:00 2001 From: conectado Date: Wed, 31 May 2023 15:28:20 -0300 Subject: [PATCH 35/54] fix phoenix channel requests to fit portal's formatting --- gateway/src/main.rs | 7 +++++-- libs/client/src/control.rs | 6 +++++- libs/common/src/control.rs | 40 +++++++++++++++++++++++++++++++++---- libs/common/src/session.rs | 39 +++++++++++++++++++++++++++--------- libs/gateway/src/control.rs | 10 +++++++--- 5 files changed, 83 insertions(+), 19 deletions(-) diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 1c05235..8d60914 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -15,8 +15,11 @@ impl Callbacks for CallbackHandler { todo!() } - fn on_error(_error: &Error, _error_type: ErrorType) { - todo!() + fn on_error(error: &Error, error_type: ErrorType) { + match error_type { + ErrorType::Recoverable => tracing::warn!("Encountered error: {error}"), + ErrorType::Fatal => panic!("Encountered fatal error: {error}"), + } } } diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs index a7d31e9..16b2469 100644 --- a/libs/client/src/control.rs +++ b/libs/client/src/control.rs @@ -183,7 +183,11 @@ impl ControlSession &'static str { + fn socket_path() -> &'static str { "client" } + + fn external_id() -> Option { + None + } } diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs index 8430230..39ffe86 100644 --- a/libs/common/src/control.rs +++ b/libs/common/src/control.rs @@ -6,17 +6,22 @@ //! Entrypoint for this module is [PhoenixChannel]. use std::{marker::PhantomData, time::Duration}; +use base64::Engine; use futures::{ channel::mpsc::{channel, Receiver, Sender}, TryStreamExt, }; use futures_util::{Future, SinkExt, StreamExt}; +use rand_core::{OsRng, RngCore}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use tokio_tungstenite::{connect_async, tungstenite}; +use tokio_tungstenite::{ + connect_async, + tungstenite::{self, handshake::client::Request}, +}; use tungstenite::Message; use url::Url; -use crate::Result; +use crate::{Error, Result}; const CHANNEL_SIZE: usize = 1_000; @@ -43,6 +48,33 @@ pub struct PhoenixChannel { _phantom: PhantomData, } +// This is basically the same as tungstenite does but we add some new headers (namely user-agent) +fn make_request(uri: &Url) -> Result { + let host = uri.host().ok_or(Error::UriError)?; + let host = if let Some(port) = uri.port() { + format!("{host}:{port}") + } else { + host.to_string() + }; + + let mut r = [0u8; 16]; + OsRng.fill_bytes(&mut r); + let key = base64::engine::general_purpose::STANDARD.encode(r); + + let req = Request::builder() + .method("GET") + .header("Host", host) + .header("Connection", "Upgrade") + .header("Upgrade", "websocket") + .header("Sec-WebSocket-Version", "13") + .header("Sec-WebSocket-Key", key) + // TODO: Get OS Info here (os_info crate) + .header("User-Agent", "MacOs/13.3 (Mac) connlib/0.1.0") + .uri(uri.as_str()) + .body(())?; + Ok(req) +} + impl PhoenixChannel where I: DeserializeOwned, @@ -53,10 +85,10 @@ where /// /// See [struct-level docs][PhoenixChannel] for more info. #[tracing::instrument(level = "trace", skip(self))] - pub async fn start(&mut self) -> std::result::Result<(), tungstenite::Error> { + pub async fn start(&mut self) -> Result<()> { tracing::trace!("Trying to connect to the portal..."); - let (ws_stream, _) = connect_async(&self.uri).await?; + let (ws_stream, _) = connect_async(make_request(&self.uri)?).await?; tracing::trace!("Successfully connected to portal"); diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs index a19a930..602cedd 100644 --- a/libs/common/src/session.rs +++ b/libs/common/src/session.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use backoff::{backoff::Backoff, ExponentialBackoffBuilder}; -use boringtun::x25519::StaticSecret; +use boringtun::x25519::{PublicKey, StaticSecret}; use rand_core::OsRng; use std::{ marker::PhantomData, @@ -12,7 +12,7 @@ use tokio::{ }; use url::Url; -use crate::{control::PhoenixChannel, error_type::ErrorType, Error, Result}; +use crate::{control::PhoenixChannel, error_type::ErrorType, messages::Key, Error, Result}; // TODO: Not the most tidy trait for a control-plane. /// Trait that represents a control-plane. @@ -22,7 +22,10 @@ pub trait ControlSession { async fn start(private_key: StaticSecret) -> Result<(Sender, Receiver)>; /// Either "gateway" or "client" used to ge the control-plane URL. - fn mode() -> &'static str; + fn socket_path() -> &'static str; + + /// Gateways should have an external id. + fn external_id() -> Option; } // TODO: Currently I'm using Session for both gateway and clients @@ -104,6 +107,7 @@ where /// The generic parameter `C` should implement all the handlers and that's how errors will be surfaced. /// /// On a fatal error you should call `[Session::disconnect]` and start a new one. + // TODO: token should be something like SecretString but we need to think about FFI compatibiltiy pub fn connect(portal_url: impl TryInto, token: String) -> Result { // TODO: We could use tokio::runtime::current() to get the current runtime // which could work with swif-rust that already runs a runtime. But IDK if that will work @@ -120,9 +124,9 @@ where runtime.spawn(async move { let private_key = StaticSecret::random_from_rng(OsRng); - let (sender, mut receiver) = fatal_error!(T::start(private_key).await, C); + let connect_url = fatal_error!(get_websocket_path(portal_url, token, T::socket_path(), &Key(PublicKey::from(&private_key).to_bytes()), T::external_id()), C); - let connect_url = fatal_error!(get_websocket_path(portal_url, token, T::mode()), C); + let (sender, mut receiver) = fatal_error!(T::start(private_key).await, C); let mut connection = PhoenixChannel::new(connect_url, move |msg| { let sender = sender.clone(); @@ -145,13 +149,13 @@ where tokio::time::sleep(t).await; match result { Ok(()) => C::on_error(&tokio_tungstenite::tungstenite::Error::ConnectionClosed.into(), ErrorType::Recoverable), - Err(e) => C::on_error(&e.into(), ErrorType::Recoverable) + Err(e) => C::on_error(&e, ErrorType::Recoverable) } } else { tracing::error!("Conneciton to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); match result { Ok(()) => C::on_error(&crate::Error::PortalConnectionError(tokio_tungstenite::tungstenite::Error::ConnectionClosed), ErrorType::Fatal), - Err(e) => C::on_error(&crate::Error::PortalConnectionError(e), ErrorType::Fatal) + Err(e) => C::on_error(&e, ErrorType::Fatal) } break; } @@ -207,13 +211,30 @@ where } } -fn get_websocket_path(mut url: Url, secret: String, mode: &str) -> Result { +fn get_websocket_path( + mut url: Url, + secret: String, + mode: &str, + public_key: &Key, + external_id: Option, +) -> Result { { let mut paths = url.path_segments_mut().map_err(|_| Error::UriError)?; paths.pop_if_empty(); paths.push(mode); paths.push("websocket"); } - url.set_query(Some(&format!("secret={secret}"))); + + { + let mut query_pairs = url.query_pairs_mut(); + query_pairs.clear(); + query_pairs.append_pair("token", &secret); + query_pairs.append_pair("public_key", &public_key.to_string()); + + if let Some(external_id) = external_id { + query_pairs.append_pair("external_id", &external_id); + } + } + Ok(url) } diff --git a/libs/gateway/src/control.rs b/libs/gateway/src/control.rs index 11215ac..050fb97 100644 --- a/libs/gateway/src/control.rs +++ b/libs/gateway/src/control.rs @@ -4,7 +4,7 @@ use firezone_tunnel::{ControlSignal, Tunnel}; use libs_common::{ boringtun::x25519::StaticSecret, error_type::ErrorType::{Fatal, Recoverable}, - messages::ResourceDescription, + messages::{Id, ResourceDescription}, Callbacks, ControlSession, Result, }; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -153,7 +153,11 @@ where Ok((sender, internal_receiver)) } - fn mode() -> &'static str { - "client" + fn socket_path() -> &'static str { + "gateway" + } + + fn external_id() -> Option { + Some(Id::new_v4().to_string()) } } From caa3f9618715e8b88d9dd87146da6bf8075d7428 Mon Sep 17 00:00:00 2001 From: conectado Date: Wed, 31 May 2023 22:12:21 -0300 Subject: [PATCH 36/54] fix phoenix channel message serialization --- libs/client/src/control.rs | 4 -- libs/client/src/messages.rs | 2 +- libs/common/src/control.rs | 76 +++++++++++++++++++++++++----------- libs/common/src/messages.rs | 2 + libs/common/src/session.rs | 22 +++++------ libs/gateway/Cargo.toml | 1 + libs/gateway/src/control.rs | 8 +--- libs/gateway/src/messages.rs | 76 +++++++++++++++++++++++++++++++----- 8 files changed, 136 insertions(+), 55 deletions(-) diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs index 16b2469..ec6ee16 100644 --- a/libs/client/src/control.rs +++ b/libs/client/src/control.rs @@ -186,8 +186,4 @@ impl ControlSession &'static str { "client" } - - fn external_id() -> Option { - None - } } diff --git a/libs/client/src/messages.rs b/libs/client/src/messages.rs index 0f611b8..ff15fce 100644 --- a/libs/client/src/messages.rs +++ b/libs/client/src/messages.rs @@ -31,7 +31,7 @@ pub struct Relays { // These messages are the messages that can be recieved // by a client. #[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] // TODO: We will need to re-visit webrtc-rs #[allow(clippy::large_enum_variant)] pub enum IngressMessages { diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs index 39ffe86..eba01d9 100644 --- a/libs/common/src/control.rs +++ b/libs/common/src/control.rs @@ -83,16 +83,19 @@ where { /// Starts the tunnel with the parameters given in [Self::new]. /// + // (Note: we could add a generic list of messages but this is easier) + /// Additionally, you can add a list of topic to join after connection ASAP. + /// /// See [struct-level docs][PhoenixChannel] for more info. #[tracing::instrument(level = "trace", skip(self))] - pub async fn start(&mut self) -> Result<()> { + pub async fn start(&mut self, topics: Vec) -> Result<()> { tracing::trace!("Trying to connect to the portal..."); let (ws_stream, _) = connect_async(make_request(&self.uri)?).await?; tracing::trace!("Successfully connected to portal"); - let (write, read) = ws_stream.split(); + let (mut write, read) = ws_stream.split(); let mut sender = self.sender(); let Self { @@ -104,6 +107,22 @@ where Ok(()) }); + // Would we like to do write.send_all(futures::stream(Message::text(...))) ? + // yes. + // but since write is taken by reference rust doesn't believe this future is sendable anymore + // so this works for now, since we only use it with 1 topic. + for topic in topics { + write + .send(Message::Text( + serde_json::to_string(&PhoenixMessage::new( + topic, + EgressControlMessage::PhxJoin(Empty {}), + )) + .expect("we should always be able to serialize a join topic message"), + )) + .await?; + } + // TODO: is Forward cancel safe? // I would assume it is and that's the advantage over // while let Some(item) = reciever.next().await { write.send(item) } ... @@ -118,7 +137,7 @@ where let mut timer = tokio::time::interval(Duration::from_secs(30)); loop { timer.tick().await; - let Ok(_) = sender.send("phoenix", "heartbeat", Empty {}).await else {break}; + let Ok(_) = sender.send("phoenix", EgressControlMessage::Heartbeat(Empty {})).await else {break}; } }); @@ -143,9 +162,9 @@ where Ok(m_str) => match serde_json::from_str::>(&m_str) { Ok(m) => match m.payload { Payload::Message(m) => handler(m).await, - Payload::PhoenixReply { status, .. } => { - // TODO: This could be an error - tracing::trace!("Recieved phoenix status message: {status}") + Payload::PhoenixControl(status) => { + // TODO: handle differents statuses + tracing::trace!("Recieved phoenix status message: {status:?}") } }, Err(e) => { @@ -189,27 +208,25 @@ where #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] #[serde(untagged)] enum Payload { - // TODO: We should be able to extract a Result from this. - PhoenixReply { response: Empty, status: String }, + PhoenixControl(IngressControlMessage), Message(T), } #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -struct PhoenixMessage { +pub struct PhoenixMessage { topic: String, - event: String, + #[serde(flatten)] payload: Payload, #[serde(rename = "ref")] reference: Option, } impl PhoenixMessage { - fn new(topic: impl Into, event: impl Into, payload: T) -> Self { + pub fn new(topic: impl Into, payload: T) -> Self { Self { topic: topic.into(), - event: event.into(), payload: Payload::Message(payload), - reference: Some(0), + reference: None, } } } @@ -218,6 +235,26 @@ impl PhoenixMessage { #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] struct Empty {} +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +enum EgressControlMessage { + PhxJoin(Empty), + Heartbeat(Empty), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +enum IngressControlMessage { + PhxReply(PhxReply), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "status", content = "response")] +enum PhxReply { + Ok(Empty), + Error { reason: String }, +} + /// You can use this sender to send messages through a `PhoenixChannel`. /// /// Messages won't be sent unless [PhoenixChannel::start] is running, internally @@ -231,15 +268,9 @@ impl PhoenixSender { /// /// # Parameters /// - topic: Phoenix topic - /// - event: Phoenix event /// - payload: Message's payload - pub async fn send( - &mut self, - topic: impl Into, - event: impl Into, - payload: impl Serialize, - ) -> Result<()> { - let str = serde_json::to_string(&PhoenixMessage::new(topic, event, payload))?; + pub async fn send(&mut self, topic: impl Into, payload: impl Serialize) -> Result<()> { + let str = serde_json::to_string(&PhoenixMessage::new(topic, payload))?; self.sender.send(Message::text(str)).await?; Ok(()) } @@ -247,7 +278,8 @@ impl PhoenixSender { /// Join a phoenix topic, meaning that after this method is invoked [PhoenixChannel] will /// recieve messages from that topic, given that upstream accepts you into the given topic. pub async fn join_topic(&mut self, topic: impl Into) -> Result<()> { - self.send(topic, "phx_join", Empty {}).await + self.send(topic, EgressControlMessage::PhxJoin(Empty {})) + .await } /// Closes the [PhoenixChannel] diff --git a/libs/common/src/messages.rs b/libs/common/src/messages.rs index 5e60c06..c978c1a 100644 --- a/libs/common/src/messages.rs +++ b/libs/common/src/messages.rs @@ -71,5 +71,7 @@ pub struct Interface { /// Interface's Ipv6. pub ipv6: Ipv6Addr, /// DNS that will be used to query for DNS that aren't within our resource list. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub upstream_dns: Vec, } diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs index 602cedd..a9315bc 100644 --- a/libs/common/src/session.rs +++ b/libs/common/src/session.rs @@ -21,11 +21,8 @@ pub trait ControlSession { /// Start control-plane with the given private-key in the background. async fn start(private_key: StaticSecret) -> Result<(Sender, Receiver)>; - /// Either "gateway" or "client" used to ge the control-plane URL. + /// Either "gateway" or "client" used to get the control-plane URL. fn socket_path() -> &'static str; - - /// Gateways should have an external id. - fn external_id() -> Option; } // TODO: Currently I'm using Session for both gateway and clients @@ -123,8 +120,9 @@ where runtime.spawn(async move { let private_key = StaticSecret::random_from_rng(OsRng); + let self_id = uuid::Uuid::new_v4(); - let connect_url = fatal_error!(get_websocket_path(portal_url, token, T::socket_path(), &Key(PublicKey::from(&private_key).to_bytes()), T::external_id()), C); + let connect_url = fatal_error!(get_websocket_path(portal_url, token, T::socket_path(), &Key(PublicKey::from(&private_key).to_bytes()), &self_id.to_string()), C); let (sender, mut receiver) = fatal_error!(T::start(private_key).await, C); @@ -138,12 +136,15 @@ where } }); + // Used to send internal messages let mut internal_sender = connection.sender(); + let topic = format!("{}:{}", T::socket_path(), self_id); + let topic_send = topic.clone(); tokio::spawn(async move { let mut exponential_backoff = ExponentialBackoffBuilder::default().build(); loop { - let result = connection.start().await; + let result = connection.start(vec![topic.clone()]).await; if let Some(t) = exponential_backoff.next_backoff() { tracing::warn!("Error during connection to the portal, retrying in {} seconds", t.as_secs()); tokio::time::sleep(t).await; @@ -167,7 +168,7 @@ where // that way we can simply do receiver.forward(sender) tokio::spawn(async move { while let Some(message) = receiver.recv().await { - if let Err(err) = internal_sender.send("TODO", "TODO", message).await { + if let Err(err) = internal_sender.send(&topic_send, message).await { tracing::error!("Channel already closed when trying to send message: {err}. Probably trying to send a message during session clean up."); } } @@ -216,7 +217,7 @@ fn get_websocket_path( secret: String, mode: &str, public_key: &Key, - external_id: Option, + external_id: &str, ) -> Result { { let mut paths = url.path_segments_mut().map_err(|_| Error::UriError)?; @@ -230,10 +231,7 @@ fn get_websocket_path( query_pairs.clear(); query_pairs.append_pair("token", &secret); query_pairs.append_pair("public_key", &public_key.to_string()); - - if let Some(external_id) = external_id { - query_pairs.append_pair("external_id", &external_id); - } + query_pairs.append_pair("external_id", external_id); } Ok(url) diff --git a/libs/gateway/Cargo.toml b/libs/gateway/Cargo.toml index 944c29b..c703dcf 100644 --- a/libs/gateway/Cargo.toml +++ b/libs/gateway/Cargo.toml @@ -10,3 +10,4 @@ firezone-tunnel = { path = "../tunnel" } tokio = { version = "1.27", default-features = false, features = ["sync"] } tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } +serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/libs/gateway/src/control.rs b/libs/gateway/src/control.rs index 050fb97..a8d183f 100644 --- a/libs/gateway/src/control.rs +++ b/libs/gateway/src/control.rs @@ -4,7 +4,7 @@ use firezone_tunnel::{ControlSignal, Tunnel}; use libs_common::{ boringtun::x25519::StaticSecret, error_type::ErrorType::{Fatal, Recoverable}, - messages::{Id, ResourceDescription}, + messages::ResourceDescription, Callbacks, ControlSession, Result, }; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -106,7 +106,7 @@ where #[tracing::instrument(level = "trace", skip(self))] pub(super) async fn handle_message(&mut self, msg: IngressMessages) { match msg { - IngressMessages::InitGateway(init) => self.init(init).await, + IngressMessages::Init(init) => self.init(init).await, IngressMessages::ConnectionRequest(connection_request) => { self.connection_request(connection_request) } @@ -156,8 +156,4 @@ where fn socket_path() -> &'static str { "gateway" } - - fn external_id() -> Option { - Some(Id::new_v4().to_string()) - } } diff --git a/libs/gateway/src/messages.rs b/libs/gateway/src/messages.rs index 4d84cd0..c7da70d 100644 --- a/libs/gateway/src/messages.rs +++ b/libs/gateway/src/messages.rs @@ -7,12 +7,14 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] pub struct InitGateway { pub interface: Interface, - pub ipv4_masquerade: bool, - pub ipv6_masquerade: bool, + pub ipv4_masquerade_enabled: bool, + pub ipv6_masquerade_enabled: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub resources: Vec, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct Client { pub id: Id, pub peer: Peer, @@ -27,6 +29,19 @@ pub struct ConnectionRequest { pub resource: Resource, } +// rtc_sdp is ignored from eq since RTCSessionDescription doesn't implement this +// this will probably be changed in the future. +impl PartialEq for ConnectionRequest { + fn eq(&self, other: &Self) -> bool { + self.user_id == other.user_id + && self.client == other.client + && self.relays == other.relays + && self.resource == other.resource + } +} + +impl Eq for ConnectionRequest {} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub enum Destination { DnsName(String), @@ -41,12 +56,12 @@ pub struct Resource { pub resource_address: Destination, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct Metrics { peers_metrics: Vec, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct Metric { pub client_id: Id, pub resource_id: Id, @@ -54,19 +69,19 @@ pub struct Metric { pub tx_bytes: u32, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct RemoveResource { pub id: Id, } // These messages are the messages that can be recieved // either by a client or a gateway by the client. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "snake_case")] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] // TODO: We will need to re-visit webrtc-rs #[allow(clippy::large_enum_variant)] pub enum IngressMessages { - InitGateway(InitGateway), + Init(InitGateway), ConnectionRequest(ConnectionRequest), AddResource(Resource), RemoveResource(RemoveResource), @@ -76,7 +91,7 @@ pub enum IngressMessages { // These messages can be sent from a gateway // to a control pane. #[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] // TODO: We will need to re-visit webrtc-rs #[allow(clippy::large_enum_variant)] pub enum EgressMessages { @@ -89,3 +104,44 @@ pub struct ConnectionReady { pub client_id: Id, pub gateway_rtc_sdp: RTCSessionDescription, } + +#[cfg(test)] +mod test { + use libs_common::{control::PhoenixMessage, messages::Interface}; + + use super::{IngressMessages, InitGateway}; + + #[test] + fn init_phoenix_message() { + let m = PhoenixMessage::new( + "gateway:83d28051-324e-48fe-98ed-19690899b3b6", + IngressMessages::Init(InitGateway { + interface: Interface { + ipv4: "100.115.164.78".parse().unwrap(), + ipv6: "fd00:2011:1111::2c:f6ab".parse().unwrap(), + upstream_dns: vec![], + }, + ipv4_masquerade_enabled: true, + ipv6_masquerade_enabled: true, + resources: vec![], + }), + ); + println!("{}", serde_json::to_string(&m).unwrap()); + let message = r#"{ + "event": "init", + "payload": { + "interface": { + "ipv4": "100.115.164.78", + "ipv6": "fd00:2011:1111::2c:f6ab" + }, + "ipv4_masquerade_enabled": true, + "ipv6_masquerade_enabled": true + }, + "ref": null, + "topic": "gateway:83d28051-324e-48fe-98ed-19690899b3b6" + }"#; + let ingress_message: PhoenixMessage = + serde_json::from_str(message).unwrap(); + assert_eq!(m, ingress_message); + } +} From e7f866032ec4f9a197aca07533325dd1a0e41cb9 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 1 Jun 2023 15:00:41 -0300 Subject: [PATCH 37/54] update phoenix channel topic --- libs/common/src/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs index a9315bc..d9cd8e5 100644 --- a/libs/common/src/session.rs +++ b/libs/common/src/session.rs @@ -138,7 +138,7 @@ where // Used to send internal messages let mut internal_sender = connection.sender(); - let topic = format!("{}:{}", T::socket_path(), self_id); + let topic = T::socket_path().to_string(); let topic_send = topic.clone(); tokio::spawn(async move { @@ -153,7 +153,7 @@ where Err(e) => C::on_error(&e, ErrorType::Recoverable) } } else { - tracing::error!("Conneciton to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); + tracing::error!("Conneciton to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); match result { Ok(()) => C::on_error(&crate::Error::PortalConnectionError(tokio_tungstenite::tungstenite::Error::ConnectionClosed), ErrorType::Fatal), Err(e) => C::on_error(&e, ErrorType::Fatal) From b63810f32abc46ff9327cef5bb96e1aa432100b4 Mon Sep 17 00:00:00 2001 From: conectado Date: Fri, 2 Jun 2023 17:10:34 -0300 Subject: [PATCH 38/54] fix message formatting and first headless client version --- Cargo.toml | 1 + clients/headless/Cargo.toml | 13 +++++++ clients/headless/src/main.rs | 46 ++++++++++++++++++++++ libs/client/Cargo.toml | 3 ++ libs/client/src/control.rs | 4 +- libs/client/src/messages.rs | 75 ++++++++++++++++++++++++++++++++++-- libs/common/src/control.rs | 1 + libs/common/src/messages.rs | 3 +- libs/common/src/session.rs | 2 +- libs/gateway/Cargo.toml | 2 + libs/gateway/src/messages.rs | 2 +- 11 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 clients/headless/Cargo.toml create mode 100644 clients/headless/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 11c0607..aa60248 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "clients/android", "clients/apple", + "clients/headless", "libs/tunnel", "libs/client", "libs/gateway", diff --git a/clients/headless/Cargo.toml b/clients/headless/Cargo.toml new file mode 100644 index 0000000..deda96f --- /dev/null +++ b/clients/headless/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "headless" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +firezone-client-connlib = { path = "../../libs/client" } +clap = { version = "4.2", features = ["derive"] } +url = { version = "2.3.1", default-features = false } +tracing-subscriber = { version = "0.3" } +tracing = { version = "0.1" } \ No newline at end of file diff --git a/clients/headless/src/main.rs b/clients/headless/src/main.rs new file mode 100644 index 0000000..fc96d7d --- /dev/null +++ b/clients/headless/src/main.rs @@ -0,0 +1,46 @@ +use clap::Parser; +use firezone_client_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, TunnelAddresses, +}; +use url::Url; + +enum CallbackHandler {} + +impl Callbacks for CallbackHandler { + fn on_update_resources(_resource_list: ResourceList) { + todo!() + } + + fn on_set_tunnel_adresses(_tunnel_addresses: TunnelAddresses) { + todo!() + } + + fn on_error(error: &Error, error_type: ErrorType) { + match error_type { + ErrorType::Recoverable => tracing::warn!("Encountered error: {error}"), + ErrorType::Fatal => panic!("Encountered fatal error: {error}"), + } + } +} + +fn main() { + tracing_subscriber::fmt::init(); + // TODO: read args from env instead + let args = Args::parse(); + // TODO: This is disgusting + let mut session = + Session::::connect::(args.url, args.secret).unwrap(); + tracing::info!("Started new session"); + session.wait_for_ctrl_c().unwrap(); + session.disconnect(); +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + #[arg(long)] + pub secret: String, + #[arg(long)] + pub url: Url, +} diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index 69b489d..c81bbc1 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -10,3 +10,6 @@ async-trait = { version = "0.1", default-features = false } libs-common = { path = "../common" } firezone-tunnel = { path = "../tunnel" } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } + +[dev-dependencies] +serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs index ec6ee16..0b84237 100644 --- a/libs/client/src/control.rs +++ b/libs/client/src/control.rs @@ -139,7 +139,7 @@ where #[tracing::instrument(level = "trace", skip(self))] pub(super) async fn handle_message(&mut self, msg: IngressMessages) { match msg { - IngressMessages::InitClient(init) => self.init(init).await, + IngressMessages::Init(init) => self.init(init).await, IngressMessages::Relays(connection_details) => self.relays(connection_details), IngressMessages::Connect(connect) => self.connect(connect).await, IngressMessages::AddResource(resource) => self.add_resource(resource), @@ -184,6 +184,6 @@ impl ControlSession &'static str { - "client" + "device" } } diff --git a/libs/client/src/messages.rs b/libs/client/src/messages.rs index ff15fce..cbe752b 100644 --- a/libs/client/src/messages.rs +++ b/libs/client/src/messages.rs @@ -22,7 +22,16 @@ pub struct Connect { pub gateway_public_key: Key, } -#[derive(Debug, Deserialize, Serialize, Clone)] +// Just because RTCSessionDescription doesn't implement partialeq +impl PartialEq for Connect { + fn eq(&self, other: &Self) -> bool { + self.resource_id == other.resource_id && self.gateway_public_key == other.gateway_public_key + } +} + +impl Eq for Connect {} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct Relays { pub resource_id: Id, pub relays: Vec, @@ -30,12 +39,12 @@ pub struct Relays { // These messages are the messages that can be recieved // by a client. -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case", tag = "event", content = "payload")] // TODO: We will need to re-visit webrtc-rs #[allow(clippy::large_enum_variant)] pub enum IngressMessages { - InitClient(InitClient), + Init(InitClient), Relays(Relays), Connect(Connect), @@ -54,3 +63,63 @@ pub enum EgressMessages { GetConnectionDetails(Id), RequestConnection(RequestConnection), } + +#[cfg(test)] +mod test { + use libs_common::{ + control::PhoenixMessage, + messages::{Interface, ResourceDescription}, + }; + + use super::{IngressMessages, InitClient}; + + #[test] + fn init_phoenix_message() { + let m = PhoenixMessage::new( + "device", + IngressMessages::Init(InitClient { + interface: Interface { + ipv4: "100.76.112.111".parse().unwrap(), + ipv6: "fd00:2011:1111::13:efb9".parse().unwrap(), + upstream_dns: vec![], + }, + resources: vec![ + ResourceDescription { + id: "030c2869-6e0d-4dc1-a186-5f1962a1a02b".parse().unwrap(), + address: Some("172.172.0.1/16".to_string()), + ipv4: "100.69.89.84".parse().unwrap(), + ipv6: "fd00:2011:1111::1f:5317".parse().unwrap(), + }, + ResourceDescription { + id: "a25fce02-de8e-48e0-b664-287623cfa85e".parse().unwrap(), + address: Some("gitlab.mycorp.com".to_string()), + ipv4: "100.72.207.207".parse().unwrap(), + ipv6: "fd00:2011:1111::1b:3120".parse().unwrap(), + }, + ], + }), + ); + println!("{}", serde_json::to_string(&m).unwrap()); + let message = r#" + { + "event": "init", + "payload": { + "interface": { + "ipv4": "100.76.112.111", + "ipv6": "fd00:2011:1111::13:efb9", + "upstream_dns": [] + }, + "resources": [ + {"address": "172.172.0.1/16", "id": "030c2869-6e0d-4dc1-a186-5f1962a1a02b", "ipv4": "100.69.89.84", "ipv6": "fd00:2011:1111::1f:5317"}, + {"address": "gitlab.mycorp.com", "id": "a25fce02-de8e-48e0-b664-287623cfa85e", "ipv4": "100.72.207.207", "ipv6": "fd00:2011:1111::1b:3120"} + ] + }, + "ref":null, + "topic": "device" + } + "#; + let ingress_message: PhoenixMessage = + serde_json::from_str(message).unwrap(); + assert_eq!(m, ingress_message); + } +} diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs index eba01d9..bd7a731 100644 --- a/libs/common/src/control.rs +++ b/libs/common/src/control.rs @@ -246,6 +246,7 @@ enum EgressControlMessage { #[serde(rename_all = "snake_case", tag = "event", content = "payload")] enum IngressControlMessage { PhxReply(PhxReply), + PhxError(Empty), } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] diff --git a/libs/common/src/messages.rs b/libs/common/src/messages.rs index c978c1a..1851071 100644 --- a/libs/common/src/messages.rs +++ b/libs/common/src/messages.rs @@ -47,7 +47,8 @@ pub struct ResourceDescription { /// Resource's id. pub id: Id, /// Internal resource's domain name if any. - pub dns_name: Option, + // TODO: this is either a dns name or a cidr + pub address: Option, /// Resource's ipv4 mapping. /// /// Note that this is not the actual ipv4 for the resource not even wireguard's ipv4 for the resource. diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs index d9cd8e5..0ca6ac2 100644 --- a/libs/common/src/session.rs +++ b/libs/common/src/session.rs @@ -147,11 +147,11 @@ where let result = connection.start(vec![topic.clone()]).await; if let Some(t) = exponential_backoff.next_backoff() { tracing::warn!("Error during connection to the portal, retrying in {} seconds", t.as_secs()); - tokio::time::sleep(t).await; match result { Ok(()) => C::on_error(&tokio_tungstenite::tungstenite::Error::ConnectionClosed.into(), ErrorType::Recoverable), Err(e) => C::on_error(&e, ErrorType::Recoverable) } + tokio::time::sleep(t).await; } else { tracing::error!("Conneciton to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); match result { diff --git a/libs/gateway/Cargo.toml b/libs/gateway/Cargo.toml index c703dcf..32245ad 100644 --- a/libs/gateway/Cargo.toml +++ b/libs/gateway/Cargo.toml @@ -10,4 +10,6 @@ firezone-tunnel = { path = "../tunnel" } tokio = { version = "1.27", default-features = false, features = ["sync"] } tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } + +[dev-dependencies] serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/libs/gateway/src/messages.rs b/libs/gateway/src/messages.rs index c7da70d..6ab72ab 100644 --- a/libs/gateway/src/messages.rs +++ b/libs/gateway/src/messages.rs @@ -126,7 +126,7 @@ mod test { resources: vec![], }), ); - println!("{}", serde_json::to_string(&m).unwrap()); + let message = r#"{ "event": "init", "payload": { From 2230ea88c0c0e64088930216218eca092f8fbc4f Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 5 Jun 2023 18:19:02 -0300 Subject: [PATCH 39/54] Apply suggestions from code review Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- clients/apple/src/lib.rs | 4 ++-- clients/headless/Cargo.toml | 2 +- libs/common/src/control.rs | 2 +- libs/common/src/error.rs | 8 ++++---- libs/common/src/session.rs | 2 +- libs/gateway/src/control.rs | 2 +- libs/tunnel/src/control_protocol.rs | 20 +++++++++----------- libs/tunnel/src/device_channel_unix.rs | 6 +++--- libs/tunnel/src/index.rs | 2 +- libs/tunnel/src/peer.rs | 2 +- libs/tunnel/src/tun_darwin.rs | 9 +++++---- 11 files changed, 29 insertions(+), 30 deletions(-) diff --git a/clients/apple/src/lib.rs b/clients/apple/src/lib.rs index effece4..9c6ec4b 100644 --- a/clients/apple/src/lib.rs +++ b/clients/apple/src/lib.rs @@ -95,9 +95,9 @@ impl Callbacks for CallbackHandler { } impl WrappedSession { - fn connect(portal_url: String, token: String) -> Result { + fn connect(portal_url: String, token: String) -> Result { let session = Session::connect::(portal_url.as_str(), token)?; - Ok(WrappedSession { session }) + Ok(Self { session }) } fn bump_sockets(&self) -> bool { diff --git a/clients/headless/Cargo.toml b/clients/headless/Cargo.toml index deda96f..4ff21e6 100644 --- a/clients/headless/Cargo.toml +++ b/clients/headless/Cargo.toml @@ -10,4 +10,4 @@ firezone-client-connlib = { path = "../../libs/client" } clap = { version = "4.2", features = ["derive"] } url = { version = "2.3.1", default-features = false } tracing-subscriber = { version = "0.3" } -tracing = { version = "0.1" } \ No newline at end of file +tracing = { version = "0.1" } diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs index bd7a731..3d1695a 100644 --- a/libs/common/src/control.rs +++ b/libs/common/src/control.rs @@ -137,7 +137,7 @@ where let mut timer = tokio::time::interval(Duration::from_secs(30)); loop { timer.tick().await; - let Ok(_) = sender.send("phoenix", EgressControlMessage::Heartbeat(Empty {})).await else {break}; + let Ok(_) = sender.send("phoenix", EgressControlMessage::Heartbeat(Empty {})).await else { break }; } }); diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs index 1df24b5..e92cf07 100644 --- a/libs/common/src/error.rs +++ b/libs/common/src/error.rs @@ -14,10 +14,10 @@ pub enum ConnlibError { #[error(transparent)] Io(#[from] std::io::Error), /// Error while decoding a base64 value. - #[error("There was an error while decoding a base64 value: `{0}`")] + #[error("There was an error while decoding a base64 value: {0}")] Base64DecodeError(#[from] DecodeError), /// Error while decoding a base64 value from a slice. - #[error("There was an error while decoding a base64 value: `{0}`")] + #[error("There was an error while decoding a base64 value: {0}")] Base64DecodeSliceError(#[from] DecodeSliceError), /// Request error for websocket connection. #[error("Error forming request: {0}")] @@ -26,7 +26,7 @@ pub enum ConnlibError { #[error("Portal connection error: {0}")] PortalConnectionError(#[from] tokio_tungstenite::tungstenite::error::Error), /// Provided string was not formatted as a URL. - #[error("Baddly formatted URI")] + #[error("Badly formatted URI")] UriError, /// Serde's serialize error. #[error(transparent)] @@ -59,7 +59,7 @@ pub enum ConnlibError { #[error("Error while reading system's interface")] IfaceRead(std::io::Error), /// Glob for errors without a type. - #[error("Other error")] + #[error("Other error: {0}")] Other(&'static str), /// Invalid tunnel name #[error("Invalid tunnel name")] diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs index 0ca6ac2..b3170f4 100644 --- a/libs/common/src/session.rs +++ b/libs/common/src/session.rs @@ -153,7 +153,7 @@ where } tokio::time::sleep(t).await; } else { - tracing::error!("Conneciton to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); + tracing::error!("Connection to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); match result { Ok(()) => C::on_error(&crate::Error::PortalConnectionError(tokio_tungstenite::tungstenite::Error::ConnectionClosed), ErrorType::Fatal), Err(e) => C::on_error(&e, ErrorType::Fatal) diff --git a/libs/gateway/src/control.rs b/libs/gateway/src/control.rs index a8d183f..19745bd 100644 --- a/libs/gateway/src/control.rs +++ b/libs/gateway/src/control.rs @@ -54,7 +54,7 @@ where #[tracing::instrument(level = "trace", skip_all)] async fn init(&mut self, init: InitGateway) { if let Err(e) = self.tunnel.set_interface(&init.interface).await { - tracing::error!("Couldn't intialize interface: {e}"); + tracing::error!("Couldn't initialize interface: {e}"); C::on_error(&e, Fatal); return; } diff --git a/libs/tunnel/src/control_protocol.rs b/libs/tunnel/src/control_protocol.rs index a645035..3ecc3a7 100644 --- a/libs/tunnel/src/control_protocol.rs +++ b/libs/tunnel/src/control_protocol.rs @@ -153,18 +153,16 @@ where CB::on_error(&Error::ControlProtocolError, Recoverable); return; }; - let peer_config = { - PeerConfig { - persistent_keepalive: None, - public_key: gateway_public_key, - ipv4: resource_description.ipv4, - ipv6: resource_description.ipv6, - preshared_key: p_key, - } + let peer_config = PeerConfig { + persistent_keepalive: None, + public_key: gateway_public_key, + ipv4: resource_description.ipv4, + ipv6: resource_description.ipv6, + preshared_key: p_key, }; if let Err(e) = tunnel.handle_channel_open(d, index, peer_config).await { - tracing::error!("Couldn't stablish wireguard link after channel was opened: {e}"); + tracing::error!("Couldn't establish wireguard link after channel was opened: {e}"); CB::on_error(&e, Recoverable); tunnel.cleanup_connection(resource_id); } @@ -274,7 +272,7 @@ where { CB::on_error(&e, Recoverable); tracing::error!( - "Couldn't stablish wireguard link after opening channel: {e}" + "Couldn't establish wireguard link after opening channel: {e}" ); // Note: handle_channel_open can only error out before insert to peers_by_ip // otherwise we would need to clean that up too! @@ -294,7 +292,7 @@ where let local_desc = peer_connection .local_description() .await - .ok_or(Error::ConnectionStablishError)?; + .ok_or(Error::ConnectionEstablishError)?; Ok(local_desc) } diff --git a/libs/tunnel/src/device_channel_unix.rs b/libs/tunnel/src/device_channel_unix.rs index 1eb0186..3e0dd1f 100644 --- a/libs/tunnel/src/device_channel_unix.rs +++ b/libs/tunnel/src/device_channel_unix.rs @@ -23,7 +23,7 @@ impl DeviceChannel { _ => panic!("Unexpected error while trying to read network interface"), }) }) { - Ok(result) => return result.map(|e| e.len()), + Ok(result) => break result.map(|e| e.len()), Err(_would_block) => continue, } } @@ -38,7 +38,7 @@ impl DeviceChannel { 0 => Err(std::io::Error::last_os_error()), i => Ok(i), }) { - Ok(result) => return result, + Ok(result) => break result, Err(_would_block) => continue, } } @@ -53,7 +53,7 @@ impl DeviceChannel { 0 => Err(std::io::Error::last_os_error()), i => Ok(i), }) { - Ok(result) => return result, + Ok(result) => break result, Err(_would_block) => continue, } } diff --git a/libs/tunnel/src/index.rs b/libs/tunnel/src/index.rs index 5bb0f54..ca67457 100644 --- a/libs/tunnel/src/index.rs +++ b/libs/tunnel/src/index.rs @@ -21,7 +21,7 @@ impl IndexLfsr { let i = OsRng.next_u32() & LFSR_MAX; if i > 0 { // LFSR seed must be non-zero - return i; + break i; } } } diff --git a/libs/tunnel/src/peer.rs b/libs/tunnel/src/peer.rs index e1699f9..cc830c4 100644 --- a/libs/tunnel/src/peer.rs +++ b/libs/tunnel/src/peer.rs @@ -25,7 +25,7 @@ pub(crate) struct Peer { impl Peer { pub(crate) async fn send_infallible(&self, data: &[u8]) { if let Err(e) = self.channel.write(&Bytes::copy_from_slice(data)).await { - tracing::error!("Couldn't send packet to connected peer: {e}"); + tracing::error!("Couldn't send packet to connected peer: {e}"); CB::on_error(&e.into(), ErrorType::Recoverable); } } diff --git a/libs/tunnel/src/tun_darwin.rs b/libs/tunnel/src/tun_darwin.rs index 4836091..21e4ecc 100644 --- a/libs/tunnel/src/tun_darwin.rs +++ b/libs/tunnel/src/tun_darwin.rs @@ -38,6 +38,7 @@ impl Drop for IfaceDevice { } } // For some reason this is not available in libc for darwin :c +#[allow(non_camel_case_types)] #[repr(C)] pub struct ifreq { ifr_name: [c_uchar; IF_NAMESIZE], @@ -85,7 +86,7 @@ pub fn parse_utun_name(name: &str) -> Result { impl IfaceDevice { fn write(&self, src: &[u8], af: u8) -> usize { - let mut hdr = [0u8, 0u8, 0u8, af]; + let mut hdr = [0, 0, 0, af]; let mut iov = [ iovec { iov_base: hdr.as_mut_ptr() as _, @@ -123,10 +124,10 @@ impl IfaceDevice { let mut info = ctl_info { ctl_id: 0, - ctl_name: [0i8; 96], + ctl_name: [0; 96], }; info.ctl_name[..CTRL_NAME.len()] - // SAFETY: We only care about mantaing the same byte value not the same value, + // SAFETY: We only care about maintaining the same byte value not the same value, // meaning that the slice &[u8] here is just a blob of bytes for us, we need this conversion // just because `c_char` is i8 (for some reason). // One thing I don't like about this is that `ctl_name` is actually a nul-terminated string, @@ -175,7 +176,7 @@ impl IfaceDevice { pub fn name(&self) -> Result { let mut tunnel_name = [0u8; 256]; - let mut tunnel_name_len: socklen_t = tunnel_name.len() as u32; + let mut tunnel_name_len = tunnel_name.len() as socklen_t; if unsafe { getsockopt( self.fd, From 3c508a0ce768e59bdb47818166862315602a6459 Mon Sep 17 00:00:00 2001 From: conectado Date: Wed, 7 Jun 2023 01:26:31 -0300 Subject: [PATCH 40/54] update message handle to conside phoenix replies --- libs/client/src/control.rs | 34 ++--- libs/client/src/lib.rs | 5 +- libs/client/src/messages.rs | 150 +++++++++++++++++++-- libs/common/Cargo.toml | 3 + libs/common/src/control.rs | 76 ++++++++--- libs/common/src/error.rs | 8 ++ libs/common/src/messages.rs | 42 ++++++ libs/common/src/session.rs | 13 +- libs/gateway/src/lib.rs | 9 +- libs/gateway/src/messages.rs | 6 +- libs/tunnel/Cargo.toml | 5 +- libs/tunnel/src/control_protocol.rs | 27 ++-- libs/tunnel/src/device_channel_unix.rs | 4 +- libs/tunnel/src/lib.rs | 49 +++++-- libs/tunnel/src/tun_linux.rs | 173 ++++++++++++++++++++----- 15 files changed, 489 insertions(+), 115 deletions(-) diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs index 0b84237..0d510d1 100644 --- a/libs/client/src/control.rs +++ b/libs/client/src/control.rs @@ -1,6 +1,6 @@ use std::{marker::PhantomData, sync::Arc, time::Duration}; -use crate::messages::{Connect, EgressMessages, IngressMessages, InitClient, Relays}; +use crate::messages::{Connect, EgressMessages, InitClient, Messages, Relays}; use libs_common::{ boringtun::x25519::StaticSecret, error_type::ErrorType::{Fatal, Recoverable}, @@ -18,7 +18,9 @@ const INTERNAL_CHANNEL_SIZE: usize = 256; impl ControlSignal for ControlSignaler { async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()> { self.internal_sender - .send(EgressMessages::GetConnectionDetails(resource.id)) + .send(EgressMessages::ListRelays { + resource_id: resource.id, + }) .await?; Ok(()) } @@ -41,7 +43,7 @@ where C: Send + Sync + 'static, { #[tracing::instrument(level = "trace", skip(self))] - async fn start(mut self, mut receiver: Receiver) { + async fn start(mut self, mut receiver: Receiver) { let mut interval = tokio::time::interval(Duration::from_secs(10)); loop { tokio::select! { @@ -67,7 +69,7 @@ where } for resource_description in resources { - self.add_resource(resource_description) + self.add_resource(resource_description).await } tracing::info!("Firezoned Started!"); @@ -92,8 +94,8 @@ where } #[tracing::instrument(level = "trace", skip(self))] - fn add_resource(&self, resource_description: ResourceDescription) { - self.tunnel.add_resource(resource_description); + async fn add_resource(&self, resource_description: ResourceDescription) { + self.tunnel.add_resource(resource_description).await; } #[tracing::instrument(level = "trace", skip(self))] @@ -137,14 +139,14 @@ where } #[tracing::instrument(level = "trace", skip(self))] - pub(super) async fn handle_message(&mut self, msg: IngressMessages) { + pub(super) async fn handle_message(&mut self, msg: Messages) { match msg { - IngressMessages::Init(init) => self.init(init).await, - IngressMessages::Relays(connection_details) => self.relays(connection_details), - IngressMessages::Connect(connect) => self.connect(connect).await, - IngressMessages::AddResource(resource) => self.add_resource(resource), - IngressMessages::RemoveResource(resource) => self.remove_resource(resource.id), - IngressMessages::UpdateResource(resource) => self.update_resource(resource), + Messages::Init(init) => self.init(init).await, + Messages::Relays(connection_details) => self.relays(connection_details), + Messages::Connect(connect) => self.connect(connect).await, + Messages::AddResource(resource) => self.add_resource(resource).await, + Messages::RemoveResource(resource) => self.remove_resource(resource.id), + Messages::UpdateResource(resource) => self.update_resource(resource), } } @@ -155,17 +157,17 @@ where } #[async_trait] -impl ControlSession +impl ControlSession for ControlPlane { #[tracing::instrument(level = "trace", skip(private_key))] async fn start( private_key: StaticSecret, - ) -> Result<(Sender, Receiver)> { + ) -> Result<(Sender, Receiver)> { // This is kinda hacky, the buffer size is 1 so that we make sure that we // process one message at a time, blocking if a previous message haven't been processed // to force queue ordering. - let (sender, receiver) = channel::(1); + let (sender, receiver) = channel::(1); let (internal_sender, internal_receiver) = channel(INTERNAL_CHANNEL_SIZE); let internal_sender = Arc::new(internal_sender); diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 01824f6..1a04c75 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -9,10 +9,13 @@ mod messages; /// Session type for clients. /// /// For more information see libs_common docs on [Session][libs_common::Session]. -pub type Session = libs_common::Session, IngressMessages, EgressMessages>; +pub type Session = + libs_common::Session, IngressMessages, EgressMessages, ReplyMessages, Messages>; pub use libs_common::{ error::SwiftConnlibError, error_type::{ErrorType, SwiftErrorType}, Callbacks, Error, ResourceList, TunnelAddresses, }; +use messages::Messages; +use messages::ReplyMessages; diff --git a/libs/client/src/messages.rs b/libs/client/src/messages.rs index cbe752b..b3fdca7 100644 --- a/libs/client/src/messages.rs +++ b/libs/client/src/messages.rs @@ -1,7 +1,7 @@ use firezone_tunnel::RTCSessionDescription; use serde::{Deserialize, Serialize}; -use libs_common::messages::{Id, Interface, Key, RequestConnection, ResourceDescription}; +use libs_common::messages::{Id, Interface, Key, Relay, RequestConnection, ResourceDescription}; #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] pub struct InitClient { @@ -31,10 +31,13 @@ impl PartialEq for Connect { impl Eq for Connect {} +/// List of relays #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct Relays { + /// Resource id corresponding to the relay pub resource_id: Id, - pub relays: Vec, + /// The actual list of relays + pub relays: Vec, } // These messages are the messages that can be recieved @@ -44,6 +47,26 @@ pub struct Relays { // TODO: We will need to re-visit webrtc-rs #[allow(clippy::large_enum_variant)] pub enum IngressMessages { + Init(InitClient), + Connect(Connect), + + // Resources: arrive in an orderly fashion + AddResource(ResourceDescription), + RemoveResource(RemoveResource), + UpdateResource(ResourceDescription), +} + +/// The replies that can arrive from the channel by a client +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum ReplyMessages { + Relays(Relays), +} + +/// The totality of all messages (might have a macro in the future to derive the other types) +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum Messages { Init(InitClient), Relays(Relays), Connect(Connect), @@ -54,13 +77,33 @@ pub enum IngressMessages { UpdateResource(ResourceDescription), } +impl From for Messages { + fn from(value: IngressMessages) -> Self { + match value { + IngressMessages::Init(m) => Self::Init(m), + IngressMessages::Connect(m) => Self::Connect(m), + IngressMessages::AddResource(m) => Self::AddResource(m), + IngressMessages::RemoveResource(m) => Self::RemoveResource(m), + IngressMessages::UpdateResource(m) => Self::UpdateResource(m), + } + } +} + +impl From for Messages { + fn from(value: ReplyMessages) -> Self { + match value { + ReplyMessages::Relays(m) => Self::Relays(m), + } + } +} + // These messages can be sent from a client to a control pane -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "snake_case")] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] // TODO: We will need to re-visit webrtc-rs #[allow(clippy::large_enum_variant)] pub enum EgressMessages { - GetConnectionDetails(Id), + ListRelays { resource_id: Id }, RequestConnection(RequestConnection), } @@ -68,9 +111,11 @@ pub enum EgressMessages { mod test { use libs_common::{ control::PhoenixMessage, - messages::{Interface, ResourceDescription}, + messages::{Interface, Relay, ResourceDescription, Stun, Turn}, }; + use crate::messages::{EgressMessages, Relays, ReplyMessages}; + use super::{IngressMessages, InitClient}; #[test] @@ -99,7 +144,6 @@ mod test { ], }), ); - println!("{}", serde_json::to_string(&m).unwrap()); let message = r#" { "event": "init", @@ -118,8 +162,98 @@ mod test { "topic": "device" } "#; - let ingress_message: PhoenixMessage = + let ingress_message: PhoenixMessage = serde_json::from_str(message).unwrap(); assert_eq!(m, ingress_message); } + + #[test] + fn list_relays_message() { + let m = PhoenixMessage::::new( + "device", + EgressMessages::ListRelays { + resource_id: "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3".parse().unwrap(), + }, + ); + let message = r#" + { + "event": "list_relays", + "payload": { + "resource_id": "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3" + }, + "ref":null, + "topic": "device" + } + "#; + let egress_message = serde_json::from_str(&message).unwrap(); + assert_eq!(m, egress_message); + } + + #[test] + fn list_relays_reply() { + let m = PhoenixMessage::::new_reply( + "device", + ReplyMessages::Relays(Relays { + resource_id: "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3".parse().unwrap(), + relays: vec![ + Relay::Stun(Stun { + uri: "stun:189.172.73.111:3478".to_string(), + }), + Relay::Turn(Turn { + expires_at: 1686629954, + uri: "turn:189.172.73.111:3478".to_string(), + username: "1686629954:C7I74wXYFdFugMYM".to_string(), + password: "OXXRDJ7lJN1cm+4+2BWgL87CxDrvpVrn5j3fnJHye98".to_string(), + }), + Relay::Stun(Stun { + uri: "stun:::1:3478".to_string(), + }), + Relay::Turn(Turn { + expires_at: 1686629954, + uri: "turn:::1:3478".to_string(), + username: "1686629954:dpHxHfNfOhxPLfMG".to_string(), + password: "8Wtb+3YGxO6ia23JUeSEfZ2yFD6RhGLkbgZwqjebyKY".to_string(), + }), + ], + }), + ); + println!("{}", serde_json::to_string(&m).unwrap()); + let message = r#" + { + "ref":null, + "topic":"device", + "event": "phx_reply", + "payload": { + "response": { + "relays": [ + { + "type":"stun", + "uri":"stun:189.172.73.111:3478" + }, + { + "expires_at": 1686629954, + "password": "OXXRDJ7lJN1cm+4+2BWgL87CxDrvpVrn5j3fnJHye98", + "type": "turn", + "uri": "turn:189.172.73.111:3478", + "username":"1686629954:C7I74wXYFdFugMYM" + }, + { + "type": "stun", + "uri": "stun:::1:3478" + }, + { + "expires_at": 1686629954, + "password": "8Wtb+3YGxO6ia23JUeSEfZ2yFD6RhGLkbgZwqjebyKY", + "type": "turn", + "uri": "turn:::1:3478", + "username": "1686629954:dpHxHfNfOhxPLfMG" + }], + "resource_id": "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3" + }, + "status":"ok" + } + }"#; + let reply_message = serde_json::from_str(&message).unwrap(); + assert_eq!(m, reply_message); + } } diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index 3a8b070..01eaf15 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -29,3 +29,6 @@ macros = { path = "../../macros" } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] swift-bridge = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } + +[target.'cfg(target_os = "linux")'.dependencies] +rtnetlink = { version = "0.12", default-features = false, features = ["tokio_socket"] } diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs index 3d1695a..1104737 100644 --- a/libs/common/src/control.rs +++ b/libs/common/src/control.rs @@ -40,12 +40,12 @@ const CHANNEL_SIZE: usize = 1_000; /// /// The future returned by [PhoenixChannel::start] will finish when the websocket closes (by an error), meaning that if you /// `await` it, it will block until you use `close` in a [PhoenixSender], the portal close the connection or something goes wrong. -pub struct PhoenixChannel { +pub struct PhoenixChannel { uri: Url, handler: F, sender: Sender, receiver: Receiver, - _phantom: PhantomData, + _phantom: PhantomData<(I, R, M)>, } // This is basically the same as tungstenite does but we add some new headers (namely user-agent) @@ -75,10 +75,12 @@ fn make_request(uri: &Url) -> Result { Ok(req) } -impl PhoenixChannel +impl PhoenixChannel where I: DeserializeOwned, - F: Fn(I) -> Fut, + R: DeserializeOwned, + M: From + From, + F: Fn(M) -> Fut, Fut: Future + Send + 'static, { /// Starts the tunnel with the parameters given in [Self::new]. @@ -114,7 +116,8 @@ where for topic in topics { write .send(Message::Text( - serde_json::to_string(&PhoenixMessage::new( + // We don't care about the reply type when serializing + serde_json::to_string(&PhoenixMessage::<_, ()>::new( topic, EgressControlMessage::PhxJoin(Empty {}), )) @@ -159,13 +162,21 @@ where tracing::trace!("{message:?}"); match message.into_text() { - Ok(m_str) => match serde_json::from_str::>(&m_str) { + Ok(m_str) => match serde_json::from_str::>(&m_str) { Ok(m) => match m.payload { - Payload::Message(m) => handler(m).await, - Payload::PhoenixControl(status) => { - // TODO: handle differents statuses - tracing::trace!("Recieved phoenix status message: {status:?}") - } + Payload::Message(m) => handler(m.into()).await, + Payload::Reply(status) => match status { + ReplyMessage::PhxReply(phx_reply) => match phx_reply { + PhxReply::Error { reason } => tracing::error!("Portal error: {reason}"), + PhxReply::Ok(reply) => match reply { + OkReply::NoMessage(Empty {}) => { + tracing::trace!("Phoenix status message") + } + OkReply::Message(m) => handler(m.into()).await, + }, + }, + ReplyMessage::PhxError(Empty {}) => tracing::error!("Phoenix error"), + }, }, Err(e) => { tracing::error!("Error deserializing message {m_str}: {e:?}"); @@ -207,21 +218,24 @@ where #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] #[serde(untagged)] -enum Payload { - PhoenixControl(IngressControlMessage), +enum Payload { + // We might want other type for the reply message + // but that makes everything even more convoluted! + // and we need to think how to make this whole mess less convoluted. + Reply(ReplyMessage), Message(T), } #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -pub struct PhoenixMessage { +pub struct PhoenixMessage { topic: String, #[serde(flatten)] - payload: Payload, + payload: Payload, #[serde(rename = "ref")] reference: Option, } -impl PhoenixMessage { +impl PhoenixMessage { pub fn new(topic: impl Into, payload: T) -> Self { Self { topic: topic.into(), @@ -229,10 +243,22 @@ impl PhoenixMessage { reference: None, } } + + pub fn new_reply(topic: impl Into, payload: R) -> Self { + Self { + topic: topic.into(), + // There has to be a better way :\ + payload: Payload::Reply(ReplyMessage::PhxReply(PhxReply::Ok(OkReply::Message( + payload, + )))), + reference: None, + } + } } // Awful hack to get serde_json to generate an empty "{}" instead of using "null" #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] struct Empty {} #[derive(Debug, Deserialize, Serialize, Clone)] @@ -244,15 +270,22 @@ enum EgressControlMessage { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case", tag = "event", content = "payload")] -enum IngressControlMessage { - PhxReply(PhxReply), +enum ReplyMessage { + PhxReply(PhxReply), PhxError(Empty), } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +enum OkReply { + Message(T), + NoMessage(Empty), +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case", tag = "status", content = "response")] -enum PhxReply { - Ok(Empty), +enum PhxReply { + Ok(OkReply), Error { reason: String }, } @@ -271,7 +304,8 @@ impl PhoenixSender { /// - topic: Phoenix topic /// - payload: Message's payload pub async fn send(&mut self, topic: impl Into, payload: impl Serialize) -> Result<()> { - let str = serde_json::to_string(&PhoenixMessage::new(topic, payload))?; + // We don't care about the reply type when serializing + let str = serde_json::to_string(&PhoenixMessage::<_, ()>::new(topic, payload))?; self.sender.send(Message::text(str)).await?; Ok(()) } diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs index e92cf07..521f6fb 100644 --- a/libs/common/src/error.rs +++ b/libs/common/src/error.rs @@ -64,6 +64,14 @@ pub enum ConnlibError { /// Invalid tunnel name #[error("Invalid tunnel name")] InvalidTunnelName, + #[error(transparent)] + NetlinkError(#[from] rtnetlink::Error), + /// No iface found + #[error("No iface found")] + NoIface, + /// No MTU found + #[error("No MTU found")] + NoMtu, } /// Type auto-generated by [SwiftEnum] intended to be used with rust-swift-bridge. diff --git a/libs/common/src/messages.rs b/libs/common/src/messages.rs index 1851071..74dd4dc 100644 --- a/libs/common/src/messages.rs +++ b/libs/common/src/messages.rs @@ -41,6 +41,16 @@ pub struct RequestConnection { pub client_rtc_sdp: RTCSessionDescription, } +// Custom implementation of partial eq to ignore client_rtc_sdp +impl PartialEq for RequestConnection { + fn eq(&self, other: &Self) -> bool { + self.resource_id == other.resource_id + && self.client_preshared_key == other.client_preshared_key + } +} + +impl Eq for RequestConnection {} + /// Description of a resource from a client's perspective. #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct ResourceDescription { @@ -76,3 +86,35 @@ pub struct Interface { #[serde(default)] pub upstream_dns: Vec, } + +/// A single relay +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Relay { + /// STUN type of relay + Stun(Stun), + /// TURN type of relay + Turn(Turn), +} + +/// Represent a TURN relay +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Turn { + // TODO: DateTIme + //// Expire time of the username/password in unix millisecond timestamp UTC + pub expires_at: u64, + /// URI of the relay + pub uri: String, + /// Username for the relay + pub username: String, + // TODO: SecretString + /// Password for the relay + pub password: String, +} + +/// Stun kind of relay +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Stun { + /// URI for the relay + pub uri: String, +} diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs index b3170f4..975d368 100644 --- a/libs/common/src/session.rs +++ b/libs/common/src/session.rs @@ -31,9 +31,9 @@ pub trait ControlSession { /// A session is the entry-point for connlib, mantains the runtime and the tunnel. /// /// A session is created using [Session::connect], then to stop a session we use [Session::disconnect]. -pub struct Session { +pub struct Session { runtime: Option, - _phantom: PhantomData<(T, U, V)>, + _phantom: PhantomData<(T, U, V, R, M)>, } /// Resource list that will be displayed to the users. @@ -76,11 +76,13 @@ macro_rules! fatal_error { }; } -impl Session +impl Session where - T: ControlSession, + T: ControlSession, U: for<'de> serde::Deserialize<'de> + std::fmt::Debug + Send + 'static, + R: for<'de> serde::Deserialize<'de> + std::fmt::Debug + Send + 'static, V: serde::Serialize + Send + 'static, + M: From + From + Send + 'static + std::fmt::Debug, { /// Block on waiting for ctrl+c to terminate the runtime. /// (Used for the gateways). @@ -126,7 +128,7 @@ where let (sender, mut receiver) = fatal_error!(T::start(private_key).await, C); - let mut connection = PhoenixChannel::new(connect_url, move |msg| { + let mut connection = PhoenixChannel::<_, U, R, M>::new(connect_url, move |msg| { let sender = sender.clone(); async move { tracing::trace!("Recieved message: {msg:?}"); @@ -232,6 +234,7 @@ fn get_websocket_path( query_pairs.append_pair("token", &secret); query_pairs.append_pair("public_key", &public_key.to_string()); query_pairs.append_pair("external_id", external_id); + query_pairs.append_pair("name_suffix", "todo"); } Ok(url) diff --git a/libs/gateway/src/lib.rs b/libs/gateway/src/lib.rs index e3076fc..6fa63b7 100644 --- a/libs/gateway/src/lib.rs +++ b/libs/gateway/src/lib.rs @@ -9,6 +9,13 @@ mod messages; /// Session type for gateway. /// /// For more information see libs_common docs on [Session][libs_common::Session]. -pub type Session = libs_common::Session, IngressMessages, EgressMessages>; +// TODO: Still working on gateway messages +pub type Session = libs_common::Session< + ControlPlane, + IngressMessages, + EgressMessages, + IngressMessages, + IngressMessages, +>; pub use libs_common::{error_type::ErrorType, Callbacks, Error, ResourceList, TunnelAddresses}; diff --git a/libs/gateway/src/messages.rs b/libs/gateway/src/messages.rs index 6ab72ab..e1b1a6c 100644 --- a/libs/gateway/src/messages.rs +++ b/libs/gateway/src/messages.rs @@ -1,7 +1,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use firezone_tunnel::RTCSessionDescription; -use libs_common::messages::{Id, Interface, Peer}; +use libs_common::messages::{Id, Interface, Peer, Relay}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] @@ -25,7 +25,7 @@ pub struct ConnectionRequest { pub user_id: Id, pub client: Client, pub rtc_sdp: RTCSessionDescription, - pub relays: Vec, + pub relays: Vec, pub resource: Resource, } @@ -140,7 +140,7 @@ mod test { "ref": null, "topic": "gateway:83d28051-324e-48fe-98ed-19690899b3b6" }"#; - let ingress_message: PhoenixMessage = + let ingress_message: PhoenixMessage = serde_json::from_str(message).unwrap(); assert_eq!(m, ingress_message); } diff --git a/libs/tunnel/Cargo.toml b/libs/tunnel/Cargo.toml index ff331b3..9188c24 100644 --- a/libs/tunnel/Cargo.toml +++ b/libs/tunnel/Cargo.toml @@ -17,13 +17,16 @@ bytes = { version = "1.4", default-features = false, features = ["std"] } itertools = { version = "0.10", default-features = false, features = ["use_std"] } libs-common = { path = "../common" } libc = { version = "0.2", default-features = false, features = ["std", "const-extern-fn", "extra_traits"] } +ipnet = { version = "2.7", default-features = false } # TODO: research replacing for https://github.com/algesten/str0m webrtc = { version = "0.7" } # Linux tunnel dependencies [target.'cfg(target_os = "linux")'.dependencies] -rtnetlink = "0.12" +netlink-packet-route = { version = "0.15", default-features = false } +netlink-packet-core = { version = "0.5", default-features = false } +rtnetlink = { version = "0.12", default-features = false, features = ["tokio_socket"] } # Android tunnel dependencies [target.'cfg(target_os = "android")'.dependencies] diff --git a/libs/tunnel/src/control_protocol.rs b/libs/tunnel/src/control_protocol.rs index 3ecc3a7..5ccfe8d 100644 --- a/libs/tunnel/src/control_protocol.rs +++ b/libs/tunnel/src/control_protocol.rs @@ -6,13 +6,13 @@ use libs_common::{ x25519::{PublicKey, StaticSecret}, }, error_type::ErrorType::Recoverable, - messages::{Id, Key, RequestConnection}, + messages::{Id, Key, Relay, RequestConnection}, Callbacks, Error, Result, }; use rand_core::OsRng; use webrtc::{ data_channel::RTCDataChannel, - ice_transport::ice_server::RTCIceServer, + ice_transport::{ice_credential_type::RTCIceCredentialType, ice_server::RTCIceServer}, peer_connection::{ configuration::RTCConfiguration, peer_connection_state::RTCPeerConnectionState, sdp::session_description::RTCSessionDescription, RTCPeerConnection, @@ -62,14 +62,23 @@ where #[tracing::instrument(level = "trace", skip(self))] async fn initialize_peer_request( self: &Arc, - relays: Vec, + relays: Vec, ) -> Result> { let config = RTCConfiguration { ice_servers: relays .into_iter() - .map(|srv| RTCIceServer { - urls: vec![srv], - ..Default::default() + .map(|srv| match srv { + Relay::Stun(stun) => RTCIceServer { + urls: vec![stun.uri], + ..Default::default() + }, + Relay::Turn(turn) => RTCIceServer { + urls: vec![turn.uri], + username: turn.username, + credential: turn.password, + // TODO: check what this is used for + credential_type: RTCIceCredentialType::Unspecified, + }, }) .collect(), ..Default::default() @@ -125,7 +134,7 @@ where pub async fn request_connection( self: &Arc, resource_id: Id, - relays: Vec, + relays: Vec, ) -> Result { let peer_connection = self.initialize_peer_request(relays).await?; self.set_connection_state_update(&peer_connection); @@ -247,7 +256,7 @@ where self: &Arc, sdp_session: RTCSessionDescription, peer: PeerConfig, - relays: Vec, + relays: Vec, client_id: Id, ) -> Result { let peer_connection = self.initialize_peer_request(relays).await?; @@ -292,7 +301,7 @@ where let local_desc = peer_connection .local_description() .await - .ok_or(Error::ConnectionEstablishError)?; + .ok_or(Error::ConnectionStablishError)?; Ok(local_desc) } diff --git a/libs/tunnel/src/device_channel_unix.rs b/libs/tunnel/src/device_channel_unix.rs index 3e0dd1f..2168a4c 100644 --- a/libs/tunnel/src/device_channel_unix.rs +++ b/libs/tunnel/src/device_channel_unix.rs @@ -10,7 +10,7 @@ pub(crate) struct DeviceChannel(AsyncFd>); impl DeviceChannel { pub(crate) async fn mtu(&self) -> Result { - self.0.get_ref().mtu() + self.0.get_ref().mtu().await } pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { @@ -61,7 +61,7 @@ impl DeviceChannel { } pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> { - let dev = Arc::new(IfaceDevice::new("utun")?.set_non_blocking()?); + let dev = Arc::new(IfaceDevice::new("utun").await?.set_non_blocking()?); let async_dev = Arc::clone(&dev); let device_channel = DeviceChannel(AsyncFd::new(async_dev)?); let iface_config = IfaceConfig(dev); diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 7ad80d8..8d90e73 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -10,7 +10,7 @@ use libs_common::{ }, x25519::{PublicKey, StaticSecret}, }, - error_type::ErrorType::Recoverable, + error_type::ErrorType::{Fatal, Recoverable}, Callbacks, }; @@ -138,7 +138,9 @@ pub trait ControlSignal { /// to communicate between peers. pub struct Tunnel { next_index: Mutex, - iface_config: Mutex, + // We use a tokio's mutex here since it makes things easier and we only need it + // during init, so the performance hit is neglibile + iface_config: tokio::sync::Mutex, device_channel: Arc, rate_limiter: Arc, private_key: StaticSecret, @@ -171,7 +173,7 @@ where let peers_by_ip = Default::default(); let next_index = Default::default(); let (iface_config, device_channel) = create_iface().await?; - let iface_config = Mutex::new(iface_config); + let iface_config = tokio::sync::Mutex::new(iface_config); let device_channel = Arc::new(device_channel); let peer_connections = Default::default(); let resources = Default::default(); @@ -218,25 +220,46 @@ where /// Once added, when a packet for the resource is intercepted a new data channel will be created /// and packets will be wrapped with wireguard and sent through it. #[tracing::instrument(level = "trace", skip(self))] - pub fn add_resource(&self, resource_description: ResourceDescription) { - let mut resources = self.resources.write(); - resources.insert( - resource_description.id, - Some(ResourceKind::Addr(resource_description.ipv4)), - Some(ResourceKind::Addr(resource_description.ipv6)), - resource_description, - ); + pub async fn add_resource(&self, resource_description: ResourceDescription) { + { + let mut resources = self.resources.write(); + resources.insert( + resource_description.id, + Some(ResourceKind::Addr(resource_description.ipv4)), + Some(ResourceKind::Addr(resource_description.ipv6)), + resource_description.clone(), + ); + } + { + let mut iface_config = self.iface_config.lock().await; + if let Err(err) = iface_config + .add_route(ipnet::Ipv4Net::from(resource_description.ipv4).into()) + .await + { + CB::on_error(&err, Fatal); + } + if let Err(err) = iface_config + .add_route(ipnet::Ipv6Net::from(resource_description.ipv6).into()) + .await + { + CB::on_error(&err, Fatal); + } + } } /// Sets the interface configuration and starts background tasks. #[tracing::instrument(level = "trace", skip(self))] pub async fn set_interface(self: &Arc, config: &InterfaceConfig) -> Result<()> { { - let mut iface_config = self.iface_config.lock(); + let mut iface_config = self.iface_config.lock().await; iface_config .set_iface_config(config) + .await + .expect("Couldn't initiate interface"); + iface_config + .up() + .await .expect("Couldn't initiate interface"); - iface_config.up().expect("Couldn't initiate interface"); } self.start_timers(); diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index dc64a74..bda7e04 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -1,9 +1,12 @@ +use futures::TryStreamExt; +use ipnet::IpNet; use libc::{ - close, fcntl, ioctl, open, read, sockaddr, sockaddr_in, socket, write, AF_INET, F_GETFL, - F_SETFL, IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, - O_RDWR, SIOCGIFMTU, SOCK_STREAM, + close, fcntl, ioctl, open, read, sockaddr, sockaddr_in, write, F_GETFL, F_SETFL, + IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, O_NONBLOCK, O_RDWR, }; use libs_common::{Error, Result}; +use netlink_packet_route::rtnl::link::nlas::Nla; +use rtnetlink::{new_connection, Handle}; use std::{ ffi::{c_int, c_short, c_uchar}, io, @@ -18,6 +21,9 @@ pub(crate) struct IfaceConfig(pub(crate) Arc); const TUNSETIFF: u64 = 0x4004_54ca; const TUN_FILE: &[u8] = b"/dev/net/tun\0"; +const RT_SCOPE_LINK: u8 = 253; +const RT_PROT_UNSPEC: u8 = 0; +const NETLINK_ERROR_FILE_EXISTS: i32 = -17; #[repr(C)] union IfrIfru { @@ -44,14 +50,17 @@ pub struct ifreq { ifr_ifru: IfrIfru, } -#[derive(Default, Debug)] +#[derive(Debug)] pub struct IfaceDevice { fd: RawFd, - name: String, + handle: Handle, + connection: tokio::task::JoinHandle<()>, + interface_index: u32, } impl Drop for IfaceDevice { fn drop(&mut self) { + self.connection.abort(); unsafe { close(self.fd) }; } } @@ -70,7 +79,7 @@ impl IfaceDevice { } } - pub fn new(name: &str) -> Result { + pub async fn new(name: &str) -> Result { let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } { -1 => return Err(get_last_error()), fd => fd, @@ -95,7 +104,26 @@ impl IfaceDevice { } let name = name.to_string(); - Ok(Self { fd, name }) + + let (connection, handle, _) = new_connection()?; + let join_handle = tokio::spawn(connection); + let interface_index = handle + .link() + .get() + .match_name(name.clone()) + .execute() + .try_next() + .await? + .ok_or(Error::NoIface)? + .header + .index; + + Ok(Self { + fd, + handle, + connection: join_handle, + interface_index, + }) } pub fn set_non_blocking(self) -> Result { @@ -108,33 +136,25 @@ impl IfaceDevice { } } - pub fn name(&self) -> Result { - Ok(self.name.clone()) - } - /// Get the current MTU value - pub fn mtu(&self) -> Result { - let fd = match unsafe { socket(AF_INET, SOCK_STREAM, IPPROTO_IP) } { - -1 => return Err(get_last_error()), - fd => fd, - }; - - let name = self.name()?; - let iface_name: &[u8] = name.as_ref(); - let mut ifr = ifreq { - ifr_name: [0; IF_NAMESIZE], - ifr_ifru: IfrIfru { ifru_mtu: 0 }, - }; - - ifr.ifr_name[..iface_name.len()].copy_from_slice(iface_name); - - if unsafe { ioctl(fd, SIOCGIFMTU as _, &ifr) } < 0 { - return Err(get_last_error()); + pub async fn mtu(&self) -> Result { + while let Ok(Some(msg)) = self + .handle + .link() + .get() + .match_index(self.interface_index) + .execute() + .try_next() + .await + { + for nla in msg.nlas { + if let Nla::Mtu(mtu) = nla { + return Ok(mtu as usize); + } + } } - unsafe { close(fd) }; - - Ok(unsafe { ifr.ifr_ifru.ifru_mtu } as _) + Err(Error::NoMtu) } pub fn write4(&self, src: &[u8]) -> usize { @@ -158,12 +178,95 @@ fn get_last_error() -> Error { } impl IfaceConfig { + pub async fn add_route(&mut self, route: IpNet) -> Result<()> { + let req = self + .0 + .handle + .route() + .add() + .output_interface(self.0.interface_index) + .protocol(RT_PROT_UNSPEC) + .scope(RT_SCOPE_LINK); + match route { + IpNet::V4(ipnet) => { + req.v4() + .source_prefix(ipnet.addr(), ipnet.prefix_len()) + .destination_prefix(ipnet.addr(), ipnet.prefix_len()) + .execute() + .await? + } + IpNet::V6(ipnet) => { + req.v6() + .source_prefix(ipnet.addr(), ipnet.prefix_len()) + .destination_prefix(ipnet.addr(), ipnet.prefix_len()) + .execute() + .await? + } + } + /* + TODO: This works for ignoring the error but the route isn't added afterwards + let's try removing all routes on init for the given interface I think that will work. + match res { + Ok(_) + | Err(rtnetlink::Error::NetlinkError(netlink_packet_core::error::ErrorMessage { + code: NETLINK_ERROR_FILE_EXISTS, + .. + })) => Ok(()), + + Err(err) => Err(err.into()), + } + */ + + Ok(()) + } #[tracing::instrument(level = "trace", skip(self))] - pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { - todo!() + pub async fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + let ips = self + .0 + .handle + .address() + .get() + .set_link_index_filter(self.0.interface_index) + .execute(); + + ips.try_for_each(|ip| self.0.handle.address().del(ip).execute()) + .await?; + + self.0 + .handle + .address() + .add(self.0.interface_index, config.ipv4.into(), 32) + .execute() + .await?; + + self.0 + .handle + .address() + .add(self.0.interface_index, config.ipv6.into(), 128) + .execute() + .await?; + + //TODO! + /* + let name: String = self.name.clone().try_into()?; + for dns in &config.dns { + //resolvconf::set_dns(&name, dns).await?; + } + */ + + //nftables::enable_masquerade((config.ipv4_masquerade, config.ipv6_masquerade)).await?; + + Ok(()) } - pub fn up(&mut self) -> Result<()> { - todo!() + pub async fn up(&mut self) -> Result<()> { + self.0 + .handle + .link() + .set(self.0.interface_index) + .up() + .execute() + .await?; + Ok(()) } } From e53b2fa2cdd9cbbe52f67205808bdab1fbd42f93 Mon Sep 17 00:00:00 2001 From: conectado Date: Thu, 8 Jun 2023 18:18:41 -0300 Subject: [PATCH 41/54] update libs to handle CIDRs --- libs/client/src/control.rs | 2 +- libs/client/src/messages.rs | 79 +++++++++------ libs/common/Cargo.toml | 1 + libs/common/src/control.rs | 12 ++- libs/common/src/messages.rs | 56 +++++++++-- libs/tunnel/Cargo.toml | 3 +- libs/tunnel/src/control_protocol.rs | 16 +-- libs/tunnel/src/lib.rs | 72 +++++-------- libs/tunnel/src/multimap.rs | 80 --------------- libs/tunnel/src/peer.rs | 36 +++---- libs/tunnel/src/resource_table.rs | 151 ++++++++++++++++++++++++++++ libs/tunnel/src/tun_linux.rs | 16 +-- 12 files changed, 315 insertions(+), 209 deletions(-) delete mode 100644 libs/tunnel/src/multimap.rs create mode 100644 libs/tunnel/src/resource_table.rs diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs index 0d510d1..897905f 100644 --- a/libs/client/src/control.rs +++ b/libs/client/src/control.rs @@ -19,7 +19,7 @@ impl ControlSignal for ControlSignaler { async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()> { self.internal_sender .send(EgressMessages::ListRelays { - resource_id: resource.id, + resource_id: resource.id(), }) .await?; Ok(()) diff --git a/libs/client/src/messages.rs b/libs/client/src/messages.rs index b3fdca7..51cb997 100644 --- a/libs/client/src/messages.rs +++ b/libs/client/src/messages.rs @@ -111,57 +111,73 @@ pub enum EgressMessages { mod test { use libs_common::{ control::PhoenixMessage, - messages::{Interface, Relay, ResourceDescription, Stun, Turn}, + messages::{ + Interface, Relay, ResourceDescription, ResourceDescriptionCidr, ResourceDescriptionDns, + Stun, Turn, + }, }; use crate::messages::{EgressMessages, Relays, ReplyMessages}; use super::{IngressMessages, InitClient}; + // TODO: request_connection tests + #[test] fn init_phoenix_message() { let m = PhoenixMessage::new( "device", IngressMessages::Init(InitClient { interface: Interface { - ipv4: "100.76.112.111".parse().unwrap(), + ipv4: "100.72.112.111".parse().unwrap(), ipv6: "fd00:2011:1111::13:efb9".parse().unwrap(), upstream_dns: vec![], }, resources: vec![ - ResourceDescription { - id: "030c2869-6e0d-4dc1-a186-5f1962a1a02b".parse().unwrap(), - address: Some("172.172.0.1/16".to_string()), - ipv4: "100.69.89.84".parse().unwrap(), - ipv6: "fd00:2011:1111::1f:5317".parse().unwrap(), - }, - ResourceDescription { - id: "a25fce02-de8e-48e0-b664-287623cfa85e".parse().unwrap(), - address: Some("gitlab.mycorp.com".to_string()), - ipv4: "100.72.207.207".parse().unwrap(), - ipv6: "fd00:2011:1111::1b:3120".parse().unwrap(), - }, + ResourceDescription::Cidr(ResourceDescriptionCidr { + id: "73037362-715d-4a83-a749-f18eadd970e6".parse().unwrap(), + address: "172.172.0.0/16".parse().unwrap(), + name: "172.172.0.0/16".to_string(), + }), + ResourceDescription::Dns(ResourceDescriptionDns { + id: "03000143-e25e-45c7-aafb-144990e57dcd".parse().unwrap(), + address: "gitlab.mycorp.com".to_string(), + ipv4: "100.126.44.50".parse().unwrap(), + ipv6: "fd00:2011:1111::e:7758".parse().unwrap(), + name: "gitlab.mycorp.com".to_string(), + }), ], }), ); - let message = r#" - { - "event": "init", - "payload": { - "interface": { - "ipv4": "100.76.112.111", - "ipv6": "fd00:2011:1111::13:efb9", - "upstream_dns": [] - }, - "resources": [ - {"address": "172.172.0.1/16", "id": "030c2869-6e0d-4dc1-a186-5f1962a1a02b", "ipv4": "100.69.89.84", "ipv6": "fd00:2011:1111::1f:5317"}, - {"address": "gitlab.mycorp.com", "id": "a25fce02-de8e-48e0-b664-287623cfa85e", "ipv4": "100.72.207.207", "ipv6": "fd00:2011:1111::1b:3120"} - ] + println!("{}", serde_json::to_string(&m).unwrap()); + let message = r#"{ + "event": "init", + "payload": { + "interface": { + "ipv4": "100.72.112.111", + "ipv6": "fd00:2011:1111::13:efb9", + "upstream_dns": [] }, - "ref":null, - "topic": "device" - } - "#; + "resources": [ + { + "address": "172.172.0.0/16", + "id": "73037362-715d-4a83-a749-f18eadd970e6", + "name": "172.172.0.0/16", + "type": "cidr" + }, + { + "address": "gitlab.mycorp.com", + "id": "03000143-e25e-45c7-aafb-144990e57dcd", + "ipv4": "100.126.44.50", + "ipv6": "fd00:2011:1111::e:7758", + "name": "gitlab.mycorp.com", + "type": "dns" + } + ] + }, + "ref": null, + "topic": "device" + }"#; let ingress_message: PhoenixMessage = serde_json::from_str(message).unwrap(); assert_eq!(m, ingress_message); @@ -217,7 +233,6 @@ mod test { ], }), ); - println!("{}", serde_json::to_string(&m).unwrap()); let message = r#" { "ref":null, diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index 01eaf15..eda1210 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -24,6 +24,7 @@ rand_core = { version = "0.6.4", default-features = false, features = ["std"] } async-trait = { version = "0.1", default-features = false } backoff = { version = "0.4", default-features = false } boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false } +ip_network = { version = "0.4", default-features = false, features = ["serde"] } macros = { path = "../../macros" } diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs index 1104737..6058379 100644 --- a/libs/common/src/control.rs +++ b/libs/common/src/control.rs @@ -167,7 +167,8 @@ where Payload::Message(m) => handler(m.into()).await, Payload::Reply(status) => match status { ReplyMessage::PhxReply(phx_reply) => match phx_reply { - PhxReply::Error { reason } => tracing::error!("Portal error: {reason}"), + // TODO: Here we should pass error info to a subscriber + PhxReply::Error(info) => tracing::error!("Portal error: {info:?}"), PhxReply::Ok(reply) => match reply { OkReply::NoMessage(Empty {}) => { tracing::trace!("Phoenix status message") @@ -282,11 +283,18 @@ enum OkReply { NoMessage(Empty), } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum ErrorInfo { + Reason(String), + Offline, +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case", tag = "status", content = "response")] enum PhxReply { Ok(OkReply), - Error { reason: String }, + Error(ErrorInfo), } /// You can use this sender to send messages through a `PhoenixChannel`. diff --git a/libs/common/src/messages.rs b/libs/common/src/messages.rs index 74dd4dc..f2c1470 100644 --- a/libs/common/src/messages.rs +++ b/libs/common/src/messages.rs @@ -1,6 +1,7 @@ //! Message types that are used by both the gateway and client. use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use ip_network::IpNetwork; use serde::{Deserialize, Serialize}; use uuid::Uuid; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; @@ -36,29 +37,35 @@ pub struct RequestConnection { /// Resource id the request is for. pub resource_id: Id, /// The preshared key the client generated for the connection that it is trying to establish. - pub client_preshared_key: Key, + pub device_preshared_key: Key, /// Client's local RTC Session Description that the client will use for this connection. - pub client_rtc_sdp: RTCSessionDescription, + pub device_rtc_session_description: RTCSessionDescription, } // Custom implementation of partial eq to ignore client_rtc_sdp impl PartialEq for RequestConnection { fn eq(&self, other: &Self) -> bool { self.resource_id == other.resource_id - && self.client_preshared_key == other.client_preshared_key + && self.device_preshared_key == other.device_preshared_key } } impl Eq for RequestConnection {} -/// Description of a resource from a client's perspective. #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct ResourceDescription { +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ResourceDescription { + Dns(ResourceDescriptionDns), + Cidr(ResourceDescriptionCidr), +} + +/// Description of a resource that maps to a DNS record. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ResourceDescriptionDns { /// Resource's id. pub id: Id, - /// Internal resource's domain name if any. - // TODO: this is either a dns name or a cidr - pub address: Option, + /// Internal resource's domain name. + pub address: String, /// Resource's ipv4 mapping. /// /// Note that this is not the actual ipv4 for the resource not even wireguard's ipv4 for the resource. @@ -69,6 +76,39 @@ pub struct ResourceDescription { /// Note that this is not the actual ipv6 for the resource not even wireguard's ipv6 for the resource. /// This is just the mapping we use internally between a resource and its ip for intercepting packets. pub ipv6: Ipv6Addr, + /// Name of the resource. + /// + /// Used only for display. + pub name: String, +} + +impl ResourceDescription { + pub fn ips(&self) -> Vec { + match self { + ResourceDescription::Dns(r) => vec![r.ipv4.into(), r.ipv6.into()], + ResourceDescription::Cidr(r) => vec![r.address], + } + } + + pub fn id(&self) -> Id { + match self { + ResourceDescription::Dns(r) => r.id, + ResourceDescription::Cidr(r) => r.id, + } + } +} + +/// Description of a resource that maps to a CIDR. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ResourceDescriptionCidr { + /// Resource's id. + pub id: Id, + /// CIDR that this resource points to. + pub address: IpNetwork, + /// Name of the resource. + /// + /// Used only for display. + pub name: String, } /// Represents a wireguard interface configuration. diff --git a/libs/tunnel/Cargo.toml b/libs/tunnel/Cargo.toml index 9188c24..9c2420b 100644 --- a/libs/tunnel/Cargo.toml +++ b/libs/tunnel/Cargo.toml @@ -17,7 +17,8 @@ bytes = { version = "1.4", default-features = false, features = ["std"] } itertools = { version = "0.10", default-features = false, features = ["use_std"] } libs-common = { path = "../common" } libc = { version = "0.2", default-features = false, features = ["std", "const-extern-fn", "extra_traits"] } -ipnet = { version = "2.7", default-features = false } +ip_network = { version = "0.4", default-features = false } +ip_network_table = { version = "0.2", default-features = false } # TODO: research replacing for https://github.com/algesten/str0m webrtc = { version = "0.7" } diff --git a/libs/tunnel/src/control_protocol.rs b/libs/tunnel/src/control_protocol.rs index 5ccfe8d..0459b38 100644 --- a/libs/tunnel/src/control_protocol.rs +++ b/libs/tunnel/src/control_protocol.rs @@ -51,8 +51,9 @@ where { let mut peers_by_ip = self.peers_by_ip.write(); - peers_by_ip.insert(peer_config.ipv4.into(), Arc::clone(&peer)); - peers_by_ip.insert(peer_config.ipv6.into(), Arc::clone(&peer)); + for ip in peer_config.ips { + peers_by_ip.insert(ip, Arc::clone(&peer)); + } } self.start_peer_handler(Arc::clone(&peer)); @@ -77,7 +78,7 @@ where username: turn.username, credential: turn.password, // TODO: check what this is used for - credential_type: RTCIceCredentialType::Unspecified, + credential_type: RTCIceCredentialType::Password, }, }) .collect(), @@ -149,7 +150,7 @@ where let resource_description = tunnel .resources .read() - .get_main(&resource_id) + .get_by_id(&resource_id) .expect("TODO") .clone(); data_channel.on_open(Box::new(move || { @@ -165,8 +166,7 @@ where let peer_config = PeerConfig { persistent_keepalive: None, public_key: gateway_public_key, - ipv4: resource_description.ipv4, - ipv6: resource_description.ipv6, + ips: resource_description.ips(), preshared_key: p_key, }; @@ -196,8 +196,8 @@ where Ok(RequestConnection { resource_id, - client_preshared_key: Key(preshared_key.to_bytes()), - client_rtc_sdp: local_description, + device_preshared_key: Key(preshared_key.to_bytes()), + device_rtc_session_description: local_description, }) } diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 8d90e73..534851c 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -2,6 +2,8 @@ //! //! This is both the wireguard and ICE implementation that should work in tandem. //! [Tunnel] is the main entry-point for this crate. +use ip_network::IpNetwork; +use ip_network_table::IpNetworkTable; use libs_common::{ boringtun::{ noise::{ @@ -19,6 +21,7 @@ use bytes::Bytes; use itertools::Itertools; use parking_lot::{Mutex, RwLock}; use peer::Peer; +use resource_table::ResourceTable; use tokio::time::MissedTickBehavior; use webrtc::{ api::{ @@ -31,9 +34,8 @@ use webrtc::{ use std::{ collections::{HashMap, HashSet}, - hash::Hash, marker::PhantomData, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, + net::IpAddr, sync::Arc, time::Duration, }; @@ -49,12 +51,11 @@ use tun::IfaceConfig; pub use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; use index::{check_packet_index, IndexLfsr}; -use multimap::MultiMap; mod control_protocol; mod index; -mod multimap; mod peer; +mod resource_table; // TODO: For now all tunnel implementations are the same // will divide when we start introducing differences. @@ -94,20 +95,13 @@ const REFRESH_PEERS_TIEMRS_INTERVAL: Duration = Duration::from_secs(1); const HANDSHAKE_RATE_LIMIT: u64 = 100; const MAX_UDP_SIZE: usize = (1 << 16) - 1; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum ResourceKind { - _Name(String), - Addr(T), -} - /// Represent's the tunnel actual peer's config /// Obtained from libs_common's Peer #[derive(Clone)] pub struct PeerConfig { pub(crate) persistent_keepalive: Option, pub(crate) public_key: PublicKey, - pub(crate) ipv4: Ipv4Addr, - pub(crate) ipv6: Ipv6Addr, + pub(crate) ips: Vec, pub(crate) preshared_key: StaticSecret, } @@ -116,8 +110,7 @@ impl From for PeerConfig { Self { persistent_keepalive: value.persistent_keepalive, public_key: value.public_key.0.into(), - ipv4: value.ipv4, - ipv6: value.ipv6, + ips: vec![value.ipv4.into(), value.ipv6.into()], preshared_key: value.preshared_key.0.into(), } } @@ -145,12 +138,11 @@ pub struct Tunnel { rate_limiter: Arc, private_key: StaticSecret, public_key: PublicKey, - peers_by_ip: RwLock>>, + peers_by_ip: RwLock>>, peer_connections: Mutex>>, awaiting_connection: Mutex>, webrtc_api: API, - resources: - RwLock, ResourceKind, ResourceDescription>>, + resources: RwLock, control_signaler: C, gateway_public_keys: Mutex>, _phantom: PhantomData, @@ -170,7 +162,7 @@ where pub async fn new(private_key: StaticSecret, control_signaler: C) -> Result { let public_key = (&private_key).into(); let rate_limiter = Arc::new(RateLimiter::new(&public_key, HANDSHAKE_RATE_LIMIT)); - let peers_by_ip = Default::default(); + let peers_by_ip = RwLock::new(IpNetworkTable::new()); let next_index = Default::default(); let (iface_config, device_channel) = create_iface().await?; let iface_config = tokio::sync::Mutex::new(iface_config); @@ -221,30 +213,15 @@ where /// and packets will be wrapped with wireguard and sent through it. #[tracing::instrument(level = "trace", skip(self))] pub async fn add_resource(&self, resource_description: ResourceDescription) { - { - let mut resources = self.resources.write(); - resources.insert( - resource_description.id, - Some(ResourceKind::Addr(resource_description.ipv4)), - Some(ResourceKind::Addr(resource_description.ipv6)), - resource_description.clone(), - ); - } { let mut iface_config = self.iface_config.lock().await; - if let Err(err) = iface_config - .add_route(ipnet::Ipv4Net::from(resource_description.ipv4).into()) - .await - { - CB::on_error(&err, Fatal); - } - if let Err(err) = iface_config - .add_route(ipnet::Ipv6Net::from(resource_description.ipv6).into()) - .await - { - CB::on_error(&err, Fatal); + for ip in resource_description.ips() { + if let Err(err) = iface_config.add_route(ip).await { + CB::on_error(&err, Fatal); + } } } + self.resources.write().insert(resource_description); } /// Sets the interface configuration and starts background tasks. @@ -308,7 +285,8 @@ where let peers: Vec<_> = tunnel .peers_by_ip .read() - .values() + .iter() + .map(|p| p.1) .unique_by(|p| p.index) .cloned() .collect(); @@ -387,12 +365,12 @@ where peer.send_infallible::(packet).await; } TunnResult::WriteToTunnelV4(packet, addr) => { - if peer.is_allowed_ipv4(&addr) { + if peer.is_allowed(addr) { tunnel.write4_device_infallible(packet).await; } } TunnResult::WriteToTunnelV6(packet, addr) => { - if peer.is_allowed_ipv6(&addr) { + if peer.is_allowed(addr) { tunnel.write6_device_infallible(packet).await; } } @@ -428,12 +406,8 @@ where let addr = Tunn::dst_address(buff)?; let resources = self.resources.read(); match addr { - IpAddr::V4(ipv4) => resources - .get_by_helper_1(&ResourceKind::Addr(ipv4)) - .cloned(), - IpAddr::V6(ipv6) => resources - .get_by_helper_2(&ResourceKind::Addr(ipv6)) - .cloned(), + IpAddr::V4(ipv4) => resources.get_by_ip(ipv4).cloned(), + IpAddr::V6(ipv6) => resources.get_by_ip(ipv6).cloned(), } } @@ -472,7 +446,7 @@ where let (encapsulate_result, channel) = { let peers_by_ip = dev.peers_by_ip.read(); - match peers_by_ip.get(&dst_addr) { + match peers_by_ip.longest_match(dst_addr).map(|p| p.1) { Some(peer) => ( peer.tunnel.lock().encapsulate(&src[..res], &mut dst[..]), peer.channel.clone(), @@ -484,7 +458,7 @@ where // create_peer_connection hasn't added the thing to peer_connections // and we are finding another packet to the same address (otherwise we would just use peer_connections here) let mut awaiting_connection = dev.awaiting_connection.lock(); - let id = resource.id; + let id = resource.id(); if !awaiting_connection.contains(&id) { tracing::trace!("Found new intent to send packets to resource with resource-ip: {dst_addr}, initalizing conection..."); diff --git a/libs/tunnel/src/multimap.rs b/libs/tunnel/src/multimap.rs deleted file mode 100644 index 9af7221..0000000 --- a/libs/tunnel/src/multimap.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{collections::HashMap, hash::Hash}; - -// Custom very simple multimap -// it has get_main for getting things through the main key -// it has get_by_helper for getting things through helper keys -// it has remove which can only erase things through helper keys -// initially used to get things though id or a biyection(unique key) and delete through id -// e.g resource's id and resource kind mapped to the resource -#[derive(Debug, Clone)] -pub(crate) struct MultiMap { - main_map: HashMap, - helper_map_1: HashMap)>, - helper_map_2: HashMap)>, -} - -impl Default for MultiMap { - fn default() -> Self { - Self { - main_map: Default::default(), - helper_map_1: Default::default(), - helper_map_2: Default::default(), - } - } -} - -impl MultiMap -where - K1: Eq + PartialEq + Hash + Clone, - K2: Eq + PartialEq + Hash + Clone, - K3: Eq + PartialEq + Hash + Clone, -{ - pub(crate) fn insert( - &mut self, - main_key: K1, - helper_key_1: Option, - helper_key_2: Option, - value: V, - ) -> Option { - if let Some(k) = &helper_key_1 { - self.helper_map_1 - .insert(k.clone(), (main_key.clone(), helper_key_2.clone())); - } - - if let Some(k) = helper_key_2 { - self.helper_map_2 - .insert(k, (main_key.clone(), helper_key_1)); - } - self.main_map.insert(main_key, value) - } - - pub(crate) fn get_main(&self, k: &K1) -> Option<&V> { - self.main_map.get(k) - } - - pub(crate) fn get_by_helper_1(&self, k: &K2) -> Option<&V> { - let (k, _) = self.helper_map_1.get(k)?; - self.main_map.get(k) - } - - pub(crate) fn get_by_helper_2(&self, k: &K3) -> Option<&V> { - let (k, _) = self.helper_map_2.get(k)?; - self.main_map.get(k) - } - - pub(crate) fn _remove_by_helper_1(&mut self, k: &K2) -> Option { - let (k, k1) = self.helper_map_1.get(k)?; - if let Some(k) = k1 { - self.helper_map_2.remove(k); - } - self.main_map.remove(k) - } - - pub(crate) fn _remove_by_helper_2(&mut self, k: &K3) -> Option { - let (k, k2) = self.helper_map_2.get(k)?; - if let Some(k) = k2 { - self.helper_map_1.remove(k); - } - self.main_map.remove(k) - } -} diff --git a/libs/tunnel/src/peer.rs b/libs/tunnel/src/peer.rs index cc830c4..00b25e9 100644 --- a/libs/tunnel/src/peer.rs +++ b/libs/tunnel/src/peer.rs @@ -1,9 +1,8 @@ -use std::{ - net::{Ipv4Addr, Ipv6Addr}, - sync::Arc, -}; +use std::{net::IpAddr, sync::Arc}; use bytes::Bytes; +use ip_network::IpNetwork; +use ip_network_table::IpNetworkTable; use libs_common::{ boringtun::noise::{Tunn, TunnResult}, error_type::ErrorType, @@ -15,11 +14,10 @@ use webrtc::data::data_channel::DataChannel; use super::PeerConfig; pub(crate) struct Peer { - pub(crate) tunnel: Mutex, - pub(crate) index: u32, - pub(crate) allowed_ipv4: Ipv4Addr, - pub(crate) allowed_ipv6: Ipv6Addr, - pub(crate) channel: Arc, + pub tunnel: Mutex, + pub index: u32, + pub allowed_ips: IpNetworkTable<()>, + pub channel: Arc, } impl Peer { @@ -36,21 +34,23 @@ impl Peer { config: &PeerConfig, channel: Arc, ) -> Self { - Self::new(Mutex::new(tunnel), index, config.ipv4, config.ipv6, channel) + Self::new(Mutex::new(tunnel), index, config.ips.clone(), channel) } pub(crate) fn new( tunnel: Mutex, index: u32, - ipv4: Ipv4Addr, - ipv6: Ipv6Addr, + ips: Vec, channel: Arc, ) -> Peer { + let mut allowed_ips = IpNetworkTable::new(); + for ip in ips { + allowed_ips.insert(ip, ()); + } Peer { tunnel, index, - allowed_ipv4: ipv4, - allowed_ipv6: ipv6, + allowed_ips, channel, } } @@ -59,11 +59,7 @@ impl Peer { self.tunnel.lock().update_timers(dst) } - pub(crate) fn is_allowed_ipv4(&self, addr: &Ipv4Addr) -> bool { - &self.allowed_ipv4 == addr - } - - pub(crate) fn is_allowed_ipv6(&self, addr: &Ipv6Addr) -> bool { - &self.allowed_ipv6 == addr + pub(crate) fn is_allowed(&self, addr: impl Into) -> bool { + self.allowed_ips.longest_match(addr).is_some() } } diff --git a/libs/tunnel/src/resource_table.rs b/libs/tunnel/src/resource_table.rs new file mode 100644 index 0000000..72e5818 --- /dev/null +++ b/libs/tunnel/src/resource_table.rs @@ -0,0 +1,151 @@ +//! A resource table is a custom type that allows us to store a resource under an id and possibly multiple ips or even network ranges +use std::{collections::HashMap, net::IpAddr, ptr::NonNull}; + +use ip_network_table::IpNetworkTable; +use libs_common::messages::{Id, ResourceDescription}; + +// Oh boy... here we go +/// The resource table type +/// +/// This is specifically crafted for our use case, so the API is particularly made for us and not generic +pub(crate) struct ResourceTable { + id_table: HashMap, + network_table: IpNetworkTable>, + dns_name: HashMap>, +} + +// SAFETY: We actually hold a `Vec` internally that the poitners points to +unsafe impl Send for ResourceTable {} +// SAFETY: we don't allow interior mutability of the pointers we hold, in fact we don't allow ANY mutability! +// (this is part of the reason why the API is so limiting, it is easier to reason about. +unsafe impl Sync for ResourceTable {} + +impl Default for ResourceTable { + fn default() -> ResourceTable { + ResourceTable::new() + } +} + +impl ResourceTable { + /// Creates a new `ResourceTable` + pub fn new() -> ResourceTable { + ResourceTable { + network_table: IpNetworkTable::new(), + id_table: HashMap::new(), + dns_name: HashMap::new(), + } + } + + /// Gets the resource by ip + pub fn get_by_ip(&self, ip: impl Into) -> Option<&ResourceDescription> { + // SAFETY: if we found the pointer, due to our internal consistency rules it is in the id_table + self.network_table + .longest_match(ip) + .map(|m| unsafe { m.1.as_ref() }) + } + + /// Gets the resource by id + pub fn get_by_id(&self, id: &Id) -> Option<&ResourceDescription> { + self.id_table.get(id) + } + + // SAFETY: resource_description must still be in storage since we are going to reference it. + unsafe fn remove_resource(&mut self, resource_description: NonNull) { + let id = { + let res = resource_description.as_ref(); + match res { + ResourceDescription::Dns(r) => { + self.dns_name.remove(&r.address); + self.network_table.remove(r.ipv4); + self.network_table.remove(r.ipv6); + r.id + } + ResourceDescription::Cidr(r) => { + self.network_table.remove(r.address); + r.id + } + } + }; + self.id_table.remove(&id); + } + + fn cleaup_resource(&mut self, resource_description: &ResourceDescription) { + match resource_description { + ResourceDescription::Dns(r) => { + if let Some(res) = self.id_table.get(&r.id) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res.into()); + } + // Don't use res after here + } + + if let Some(res) = self.dns_name.remove(&r.address) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + // Don't use res after here + } + + if let Some(res) = self.network_table.remove(r.ipv4) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + } + + if let Some(res) = self.network_table.remove(r.ipv6) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + } + } + ResourceDescription::Cidr(r) => { + if let Some(res) = self.id_table.get(&r.id) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res.into()); + } + // Don't use res after here + } + + if let Some(res) = self.network_table.remove(r.address) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + } + } + } + } + + // For soundness it's very important that this API only takes a resource_description + // doing this, we can assume that when removing a resource from the id table we have all the info + // about all the o + /// Inserts a new resource_description + /// + /// If the id was used previously the old value will be deleted. + /// Same goes if any of the ip matches exactly an old ip or dns name. + /// This means that a match in IP or dns name will discard all old values. + /// + /// This is done so that we don't have dangling values. + pub fn insert(&mut self, resource_description: ResourceDescription) { + self.cleaup_resource(&resource_description); + let id = resource_description.id(); + self.id_table.insert(id, resource_description); + // we just inserted it we can unwrap + let res = self.id_table.get(&id).unwrap(); + match res { + ResourceDescription::Dns(r) => { + self.network_table.insert(r.ipv4, res.into()); + self.network_table.insert(r.ipv6, res.into()); + self.dns_name.insert(r.address.clone(), res.into()); + } + ResourceDescription::Cidr(r) => { + self.network_table.insert(r.address, res.into()); + } + } + } +} diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index bda7e04..f980d9e 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -1,5 +1,5 @@ use futures::TryStreamExt; -use ipnet::IpNet; +use ip_network::IpNetwork; use libc::{ close, fcntl, ioctl, open, read, sockaddr, sockaddr_in, write, F_GETFL, F_SETFL, IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, O_NONBLOCK, O_RDWR, @@ -178,7 +178,7 @@ fn get_last_error() -> Error { } impl IfaceConfig { - pub async fn add_route(&mut self, route: IpNet) -> Result<()> { + pub async fn add_route(&mut self, route: IpNetwork) -> Result<()> { let req = self .0 .handle @@ -188,17 +188,17 @@ impl IfaceConfig { .protocol(RT_PROT_UNSPEC) .scope(RT_SCOPE_LINK); match route { - IpNet::V4(ipnet) => { + IpNetwork::V4(ipnet) => { req.v4() - .source_prefix(ipnet.addr(), ipnet.prefix_len()) - .destination_prefix(ipnet.addr(), ipnet.prefix_len()) + .source_prefix(ipnet.network_address(), ipnet.netmask()) + .destination_prefix(ipnet.network_address(), ipnet.netmask()) .execute() .await? } - IpNet::V6(ipnet) => { + IpNetwork::V6(ipnet) => { req.v6() - .source_prefix(ipnet.addr(), ipnet.prefix_len()) - .destination_prefix(ipnet.addr(), ipnet.prefix_len()) + .source_prefix(ipnet.network_address(), ipnet.netmask()) + .destination_prefix(ipnet.network_address(), ipnet.netmask()) .execute() .await? } From fdc1a9af985b33c5d4ea3a1055f9d41ca40cbfc6 Mon Sep 17 00:00:00 2001 From: conectado Date: Sun, 11 Jun 2023 19:09:42 -0300 Subject: [PATCH 42/54] fix formatting of gateway message to coincide with portal --- libs/client/src/control.rs | 6 ++-- libs/client/src/messages.rs | 18 ++++++------ libs/gateway/src/control.rs | 20 +++++++------- libs/gateway/src/messages.rs | 53 +++++++++++++++--------------------- libs/tunnel/src/tun_linux.rs | 1 - 5 files changed, 44 insertions(+), 54 deletions(-) diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs index 897905f..18215e2 100644 --- a/libs/client/src/control.rs +++ b/libs/client/src/control.rs @@ -144,9 +144,9 @@ where Messages::Init(init) => self.init(init).await, Messages::Relays(connection_details) => self.relays(connection_details), Messages::Connect(connect) => self.connect(connect).await, - Messages::AddResource(resource) => self.add_resource(resource).await, - Messages::RemoveResource(resource) => self.remove_resource(resource.id), - Messages::UpdateResource(resource) => self.update_resource(resource), + Messages::ResourceAdded(resource) => self.add_resource(resource).await, + Messages::ResourceRemoved(resource) => self.remove_resource(resource.id), + Messages::ResourceUpdated(resource) => self.update_resource(resource), } } diff --git a/libs/client/src/messages.rs b/libs/client/src/messages.rs index 51cb997..67e2a48 100644 --- a/libs/client/src/messages.rs +++ b/libs/client/src/messages.rs @@ -51,9 +51,9 @@ pub enum IngressMessages { Connect(Connect), // Resources: arrive in an orderly fashion - AddResource(ResourceDescription), - RemoveResource(RemoveResource), - UpdateResource(ResourceDescription), + ResourceAdded(ResourceDescription), + ResourceRemoved(RemoveResource), + ResourceUpdated(ResourceDescription), } /// The replies that can arrive from the channel by a client @@ -72,9 +72,9 @@ pub enum Messages { Connect(Connect), // Resources: arrive in an orderly fashion - AddResource(ResourceDescription), - RemoveResource(RemoveResource), - UpdateResource(ResourceDescription), + ResourceAdded(ResourceDescription), + ResourceRemoved(RemoveResource), + ResourceUpdated(ResourceDescription), } impl From for Messages { @@ -82,9 +82,9 @@ impl From for Messages { match value { IngressMessages::Init(m) => Self::Init(m), IngressMessages::Connect(m) => Self::Connect(m), - IngressMessages::AddResource(m) => Self::AddResource(m), - IngressMessages::RemoveResource(m) => Self::RemoveResource(m), - IngressMessages::UpdateResource(m) => Self::UpdateResource(m), + IngressMessages::ResourceAdded(m) => Self::ResourceAdded(m), + IngressMessages::ResourceRemoved(m) => Self::ResourceRemoved(m), + IngressMessages::ResourceUpdated(m) => Self::ResourceUpdated(m), } } } diff --git a/libs/gateway/src/control.rs b/libs/gateway/src/control.rs index 19745bd..06b4e21 100644 --- a/libs/gateway/src/control.rs +++ b/libs/gateway/src/control.rs @@ -10,7 +10,7 @@ use libs_common::{ use tokio::sync::mpsc::{channel, Receiver, Sender}; use super::messages::{ - ConnectionReady, ConnectionRequest, EgressMessages, IngressMessages, InitGateway, Resource, + ConnectionReady, EgressMessages, IngressMessages, InitGateway, RequestConnection, }; use async_trait::async_trait; @@ -64,16 +64,16 @@ where } #[tracing::instrument(level = "trace", skip(self))] - fn connection_request(&self, connection_request: ConnectionRequest) { + fn connection_request(&self, connection_request: RequestConnection) { let tunnel = Arc::clone(&self.tunnel); let control_signaler = self.control_signaler.clone(); tokio::spawn(async move { match tunnel .set_peer_connection_request( - connection_request.rtc_sdp, - connection_request.client.peer.into(), + connection_request.device.rtc_session_description, + connection_request.device.peer.into(), connection_request.relays, - connection_request.client.id, + connection_request.device.id, ) .await { @@ -81,17 +81,17 @@ where if let Err(err) = control_signaler .internal_sender .send(EgressMessages::ConnectionReady(ConnectionReady { - client_id: connection_request.client.id, + client_id: connection_request.device.id, gateway_rtc_sdp, })) .await { - tunnel.cleanup_peer_connection(connection_request.client.id); + tunnel.cleanup_peer_connection(connection_request.device.id); C::on_error(&err.into(), Recoverable); } } Err(err) => { - tunnel.cleanup_peer_connection(connection_request.client.id); + tunnel.cleanup_peer_connection(connection_request.device.id); C::on_error(&err, Recoverable); } } @@ -99,7 +99,7 @@ where } #[tracing::instrument(level = "trace", skip(self))] - fn add_resource(&self, resource: Resource) { + fn add_resource(&self, resource: ResourceDescription) { todo!() } @@ -107,7 +107,7 @@ where pub(super) async fn handle_message(&mut self, msg: IngressMessages) { match msg { IngressMessages::Init(init) => self.init(init).await, - IngressMessages::ConnectionRequest(connection_request) => { + IngressMessages::RequestConnection(connection_request) => { self.connection_request(connection_request) } IngressMessages::AddResource(resource) => self.add_resource(resource), diff --git a/libs/gateway/src/messages.rs b/libs/gateway/src/messages.rs index e1b1a6c..18d1038 100644 --- a/libs/gateway/src/messages.rs +++ b/libs/gateway/src/messages.rs @@ -1,59 +1,51 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::net::IpAddr; use firezone_tunnel::RTCSessionDescription; -use libs_common::messages::{Id, Interface, Peer, Relay}; +use libs_common::messages::{Id, Interface, Peer, Relay, ResourceDescription}; use serde::{Deserialize, Serialize}; +// TODO: Should this have a resource? #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] pub struct InitGateway { pub interface: Interface, pub ipv4_masquerade_enabled: bool, pub ipv6_masquerade_enabled: bool, - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde(default)] - pub resources: Vec, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct Client { +pub struct Actor { pub id: Id, - pub peer: Peer, } #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ConnectionRequest { - pub user_id: Id, - pub client: Client, - pub rtc_sdp: RTCSessionDescription, - pub relays: Vec, - pub resource: Resource, +pub struct Device { + pub id: Id, + pub rtc_session_description: RTCSessionDescription, + pub peer: Peer, } // rtc_sdp is ignored from eq since RTCSessionDescription doesn't implement this // this will probably be changed in the future. -impl PartialEq for ConnectionRequest { +impl PartialEq for Device { fn eq(&self, other: &Self) -> bool { - self.user_id == other.user_id - && self.client == other.client - && self.relays == other.relays - && self.resource == other.resource + self.id == other.id && self.peer == other.peer } } -impl Eq for ConnectionRequest {} +impl Eq for Device {} #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub enum Destination { - DnsName(String), - Ip(Vec), +pub struct RequestConnection { + pub actor: Actor, + pub relays: Vec, + pub resource: ResourceDescription, + pub device: Device, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct Resource { - pub id: Id, - pub internal_ipv4: Option, - pub internal_ipv6: Option, - pub resource_address: Destination, +pub enum Destination { + DnsName(String), + Ip(Vec), } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -82,10 +74,10 @@ pub struct RemoveResource { #[allow(clippy::large_enum_variant)] pub enum IngressMessages { Init(InitGateway), - ConnectionRequest(ConnectionRequest), - AddResource(Resource), + RequestConnection(RequestConnection), + AddResource(ResourceDescription), RemoveResource(RemoveResource), - UpdateResource(Resource), + UpdateResource(ResourceDescription), } // These messages can be sent from a gateway @@ -123,7 +115,6 @@ mod test { }, ipv4_masquerade_enabled: true, ipv6_masquerade_enabled: true, - resources: vec![], }), ); diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs index f980d9e..65e6aad 100644 --- a/libs/tunnel/src/tun_linux.rs +++ b/libs/tunnel/src/tun_linux.rs @@ -23,7 +23,6 @@ const TUNSETIFF: u64 = 0x4004_54ca; const TUN_FILE: &[u8] = b"/dev/net/tun\0"; const RT_SCOPE_LINK: u8 = 253; const RT_PROT_UNSPEC: u8 = 0; -const NETLINK_ERROR_FILE_EXISTS: i32 = -17; #[repr(C)] union IfrIfru { From 462f46b89468a12f4f4cd601d605f490b939ea66 Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:22:39 -0300 Subject: [PATCH 43/54] fix Cargo.toml formatting Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- gateway/Cargo.toml | 2 +- macros/Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 07145d1..e24b817 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -10,4 +10,4 @@ firezone-gateway-connlib = { path = "../libs/gateway" } clap = { version = "4.2", features = ["derive"] } url = { version = "2.3.1", default-features = false } tracing-subscriber = { version = "0.3" } -tracing = { version = "0.1" } \ No newline at end of file +tracing = { version = "0.1" } diff --git a/macros/Cargo.toml b/macros/Cargo.toml index e93d010..5326335 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -10,4 +10,3 @@ proc-macro = true syn = { version = "2.0" } proc-macro2 = { version = "1.0" } quote = { version = "1.0" } - From f080cae137ed79cba166563a518bcc3d3bb3c735 Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:23:12 -0300 Subject: [PATCH 44/54] fix typo Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/common/src/error.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs index 521f6fb..7e0682b 100644 --- a/libs/common/src/error.rs +++ b/libs/common/src/error.rs @@ -40,9 +40,9 @@ pub enum ConnlibError { /// Error while sending through an async channelchannel. #[error("Error sending message through an async channel")] SendChannelError, - /// Error when trying to stablish connection between peers. - #[error("Error while stablishing connection between peers")] - ConnectionStablishError, + /// Error when trying to establish connection between peers. + #[error("Error while establishing connection between peers")] + ConnectionEstablishError, /// Error related to wireguard protocol. #[error("Wireguard error")] WireguardError(WireGuardError), From 5faf14072128578c884cee9e12b80f35c149f5ad Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:23:42 -0300 Subject: [PATCH 45/54] minor refactor Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/common/src/messages/key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/messages/key.rs b/libs/common/src/messages/key.rs index efdae4f..9499ce8 100644 --- a/libs/common/src/messages/key.rs +++ b/libs/common/src/messages/key.rs @@ -40,7 +40,7 @@ impl<'de> Deserialize<'de> for Key { D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(de::Error::custom) + s.parse().map_err(de::Error::custom) } } From 5cd34a5bd15a5bbd05e2eb059ed61f2802890e5c Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:24:02 -0300 Subject: [PATCH 46/54] fix typo Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/tunnel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 534851c..81011cc 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -121,7 +121,7 @@ impl From for PeerConfig { /// Generally, we try to return from the functions here rather than using this callback. #[async_trait] pub trait ControlSignal { - /// Signals to the control plane an intent to initiate a connecti to the given resource. + /// Signals to the control plane an intent to initiate a connection to the given resource. /// /// Used when a packet is found to a resource we have no connection stablished but is within the list of resources available for the client. async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()>; From 9da435711c683edcb32e4299f3e5163543b6a94d Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:24:46 -0300 Subject: [PATCH 47/54] minor refactor Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/tunnel/src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 81011cc..73c059e 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -346,14 +346,12 @@ where continue; } - let decapsulate_result = { - peer.tunnel.lock().decapsulate( - // TODO: See comment above - None, - &src_buf[..size], - &mut dst_buf[..], - ) - }; + let decapsulate_result = peer.tunnel.lock().decapsulate( + // TODO: See comment above + None, + &src_buf[..size], + &mut dst_buf[..], + ); // We found a peer, use it to decapsulate the message+ let mut flush = false; From 6833ca78b520539dc1942bda1b08d13243c91ea5 Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:25:29 -0300 Subject: [PATCH 48/54] fix typo Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/tunnel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 73c059e..77f3358 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -450,7 +450,7 @@ where peer.channel.clone(), ), None => { - // We can buffer requests here but will drop them for now and let the upper layer reialability protocol handle this + // We can buffer requests here but will drop them for now and let the upper layer reliability protocol handle this if let Some(resource) = dev.get_resource(&src[..res]) { // We have awaiting connection to prevent a race condition where // create_peer_connection hasn't added the thing to peer_connections From 2562eef869d1b3e4bdca8834e38bb535df349de3 Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:26:11 -0300 Subject: [PATCH 49/54] fix typo Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/tunnel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 77f3358..da6ab16 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -458,7 +458,7 @@ where let mut awaiting_connection = dev.awaiting_connection.lock(); let id = resource.id(); if !awaiting_connection.contains(&id) { - tracing::trace!("Found new intent to send packets to resource with resource-ip: {dst_addr}, initalizing conection..."); + tracing::trace!("Found new intent to send packets to resource with resource-ip: {dst_addr}, initializing connection..."); awaiting_connection.insert(id); let dev = Arc::clone(&dev); From 46e93e6a5ce8ad3eda2ee3ff5fa42452f5778e42 Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:26:34 -0300 Subject: [PATCH 50/54] minor refactor Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/tunnel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index da6ab16..f3c4267 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -493,7 +493,7 @@ where CB::on_error(&e.into(), Recoverable); } TunnResult::WriteToNetwork(packet) => { - tracing::trace!("writing iface packet to peer: {}", dst_addr); + tracing::trace!("writing iface packet to peer: {dst_addr}"); if let Err(e) = channel.write(&Bytes::copy_from_slice(packet)).await { tracing::error!("Couldn't write packet to channel: {e}"); CB::on_error(&e.into(), Recoverable); From d459a26afffacd0704d3be00ba10b939fe8502ee Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:26:56 -0300 Subject: [PATCH 51/54] fix typo Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/tunnel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index f3c4267..71bcc0a 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -489,7 +489,7 @@ where ); } TunnResult::Err(e) => { - tracing::error!(message = "Encapsulate error for resoruce corresponding to {dst_addr}", error = ?e); + tracing::error!(message = "Encapsulate error for resource corresponding to {dst_addr}", error = ?e); CB::on_error(&e.into(), Recoverable); } TunnResult::WriteToNetwork(packet) => { From 053638678cd736bd36a8d7765acedcc54fc9bf6c Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 12 Jun 2023 15:27:18 -0300 Subject: [PATCH 52/54] fix typo Co-authored-by: Francesca Lovebloom Signed-off-by: Gabi --- libs/tunnel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs index 71bcc0a..1047244 100644 --- a/libs/tunnel/src/lib.rs +++ b/libs/tunnel/src/lib.rs @@ -1,7 +1,7 @@ //! Connlib tunnel implementation. //! //! This is both the wireguard and ICE implementation that should work in tandem. -//! [Tunnel] is the main entry-point for this crate. +//! [Tunnel] is the main entry-point for this crate. use ip_network::IpNetwork; use ip_network_table::IpNetworkTable; use libs_common::{ From 1436e700bb41505dd24bcfd20c4b28f26aea4d53 Mon Sep 17 00:00:00 2001 From: conectado Date: Mon, 12 Jun 2023 15:28:15 -0300 Subject: [PATCH 53/54] add first dockerfiles --- .dockerignore | 176 ++++++++++++++++++++++++++++++++++++++++++++ Dockerfile-gateway | 11 +++ Dockerfile-headless | 11 +++ 3 files changed, 198 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile-gateway create mode 100644 Dockerfile-headless diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5400d93 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,176 @@ +### Rust ### +target/ +# Libraries shouldn't lock their dependencies +Cargo.lock + +### Android ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### Kotlin ### +# Compiled class file +*.class + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +build/ +/*/local.properties +out/ +production/ +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iws + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!clients/android/gradle/wrapper/gradle-wrapper.jar + +### Apple ### +.DS_Store +.build/ +DerivedData/ +xcuserdata/ +*.xcuserstate + +Firezone/Developer.xcconfig diff --git a/Dockerfile-gateway b/Dockerfile-gateway new file mode 100644 index 0000000..0466fc1 --- /dev/null +++ b/Dockerfile-gateway @@ -0,0 +1,11 @@ +FROM rust:1.70-slim as BUILDER +WORKDIR /build/ +COPY . ./ +RUN cargo build -p gateway --release + +# TODO: Change to musl + alpine +FROM debian:bullseye-slim +WORKDIR /app/ +COPY --from=BUILDER /build/target/release/gateway . +ENV PATH "/app:$PATH" +CMD ["gateway"] diff --git a/Dockerfile-headless b/Dockerfile-headless new file mode 100644 index 0000000..bb5e180 --- /dev/null +++ b/Dockerfile-headless @@ -0,0 +1,11 @@ +FROM rust:1.70-slim as BUILDER +WORKDIR /build/ +COPY . ./ +RUN cargo build -p headless --release + +# TODO: Change to musl + alpine +FROM debian:bullseye-slim +WORKDIR /app/ +COPY --from=BUILDER /build/target/release/headless . +ENV PATH "/app:$PATH" +CMD ["headless"] From 6d3e2b90e5db1b2cc2899e82c0967d039ae18661 Mon Sep 17 00:00:00 2001 From: conectado Date: Mon, 12 Jun 2023 16:07:17 -0300 Subject: [PATCH 54/54] apply code review suggestions --- clients/apple/src/lib.rs | 1 - libs/tunnel/src/control_protocol.rs | 2 +- libs/tunnel/src/index.rs | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/apple/src/lib.rs b/clients/apple/src/lib.rs index 9c6ec4b..c539b79 100644 --- a/clients/apple/src/lib.rs +++ b/clients/apple/src/lib.rs @@ -9,7 +9,6 @@ use firezone_client_connlib::{ #[swift_bridge::bridge] mod ffi { - // TODO: Allegedly not FFI safe, but works #[swift_bridge(swift_repr = "struct")] struct ResourceList { resources: String, diff --git a/libs/tunnel/src/control_protocol.rs b/libs/tunnel/src/control_protocol.rs index 0459b38..6cd7b55 100644 --- a/libs/tunnel/src/control_protocol.rs +++ b/libs/tunnel/src/control_protocol.rs @@ -301,7 +301,7 @@ where let local_desc = peer_connection .local_description() .await - .ok_or(Error::ConnectionStablishError)?; + .ok_or(Error::ConnectionEstablishError)?; Ok(local_desc) } diff --git a/libs/tunnel/src/index.rs b/libs/tunnel/src/index.rs index ca67457..f58fcb9 100644 --- a/libs/tunnel/src/index.rs +++ b/libs/tunnel/src/index.rs @@ -31,6 +31,7 @@ impl IndexLfsr { // 24-bit polynomial for randomness. This is arbitrarily chosen to // inject bitflips into the value. const LFSR_POLY: u32 = 0xd80000; // 24-bit polynomial + debug_assert_ne!(self.lfsr, 0); let value = self.lfsr - 1; // lfsr will never have value of 0 self.lfsr = (self.lfsr >> 1) ^ ((0u32.wrapping_sub(self.lfsr & 1u32)) & LFSR_POLY); assert!(self.lfsr != self.initial, "Too many peers created");