Pull request 80: Add rules config support for endpoint connection filtering

Squashed commit of the following:

commit 7b8cf69c390778ea6bd4431fefb047ffa9a3002d
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Mon Sep 1 13:27:26 2025 +0300

    Refactoring

commit 077096b6c81109479229dc7132e254ec5d10905c
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Mon Sep 1 11:44:53 2025 +0300

    Apply filtering rules

commit 1151ef7199853e92dbe62370f94116a395168d16
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Mon Sep 1 11:04:45 2025 +0300

    Add missed cargo file

commit 509a9fe5eddd73fd49b9160e320fd48d5fba4574
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Mon Sep 1 11:04:00 2025 +0300

    Fix test

commit 9d03678c23e3053e6d4685fd060bce51f1335c79
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Mon Sep 1 11:03:34 2025 +0300

    Add rules config

commit baa6c918efa3b401d9688df44c85303038256db0
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Mon Sep 1 08:28:30 2025 +0300

    Remove check tls client random from authenticator

commit cafc71d4b95b05f4f75c5a335e962e510d1b4edc
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Wed Aug 27 20:24:53 2025 +0300

    Refactor

commit 1e950d707c63622de1747e1c79befaf700cfb8f7
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Wed Aug 27 20:14:14 2025 +0300

    Rename fields and validate client_random earlier

commit efdcd2bb193641a5914c82522cdc2376100cd6a6
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Tue Aug 26 09:31:23 2025 +0300

    Add missing field value

commit 23d72ba188959d198bbb5b7cb84fb074eef45342
Author: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
Date:   Tue Aug 26 09:01:10 2025 +0300

    Add rules config support for endpoint connection filtering
    
    Now we have rules.toml configuration that defines filter rules for incoming connections.
    Each rule can specify cidr and/or client_random_prefix and action (allow/deny).
    Both cidr and client_random_prefix are optional - if specified, both must match for the rule to apply.
    If only one is specified, only that condition needs to match.
    If no rules match, the connection is allowed by default. This behavior can be changed by the empty rule with deny action:
    [[rule]]
    action = "deny"
    
    Resolves: AG-42959
    Signed-off-by: Alexey Zhavoronkov <a.zhavoronkov@adguard.com>
This commit is contained in:
Aleksei Zhavoronkov
2025-09-02 18:35:12 +03:00
parent 5dda80e05c
commit a5665277ff
14 changed files with 1081 additions and 54 deletions

463
Cargo.lock generated
View File

@@ -148,9 +148,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.1.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
@@ -160,7 +160,7 @@ checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
dependencies = [
"async-trait",
"axum-core",
"bitflags",
"bitflags 1.3.2",
"bytes",
"futures-util",
"http",
@@ -224,12 +224,62 @@ version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags 2.9.3",
"cexpr",
"clang-sys",
"itertools",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.43",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
[[package]]
name = "boring"
version = "4.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f4ea552f8764e7235bb0b6aaec33b891e5b178c77d8c96cfad6c10f057c64a6"
dependencies = [
"bitflags 2.9.3",
"boring-sys",
"foreign-types",
"libc",
"openssl-macros",
]
[[package]]
name = "boring-sys"
version = "4.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b753c2916f46e25e08abd2cd52b35223a65b7e8a1696ee33b45e20927114696f"
dependencies = [
"autocfg",
"bindgen",
"cmake",
"fs_extra",
"fslock",
]
[[package]]
name = "bumpalo"
version = "3.13.0"
@@ -254,6 +304,15 @@ version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -272,6 +331,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.3.8"
@@ -289,9 +359,9 @@ checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717"
dependencies = [
"anstream",
"anstyle",
"bitflags",
"bitflags 1.3.2",
"clap_lex",
"strsim",
"strsim 0.10.0",
]
[[package]]
@@ -400,9 +470,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.13.4"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
@@ -410,27 +480,27 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.13.4"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 1.0.109",
"strsim 0.11.1",
"syn 2.0.43",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 1.0.109",
"syn 2.0.43",
]
[[package]]
@@ -439,6 +509,12 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "debug_panic"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9377eb110cece2e9431deb8d7d2ec8c116510b896741f9f2bf02b352147aa2a6"
[[package]]
name = "debugid"
version = "0.8.0"
@@ -529,6 +605,18 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum_dispatch"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.43",
]
[[package]]
name = "erased-serde"
version = "0.3.25"
@@ -584,6 +672,33 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
]
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
@@ -593,6 +708,22 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fslock"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "futures"
version = "0.3.28"
@@ -699,6 +830,12 @@ version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "h2"
version = "0.3.20"
@@ -916,6 +1053,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "intrusive-collections"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86"
dependencies = [
"memoffset",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
@@ -929,9 +1075,9 @@ dependencies = [
[[package]]
name = "ipnet"
version = "2.7.2"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "is-terminal"
@@ -981,6 +1127,16 @@ version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
]
[[package]]
name = "libm"
version = "0.2.7"
@@ -1011,9 +1167,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.19"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "macros"
@@ -1051,6 +1207,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -1102,6 +1267,28 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom-derive"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ff943d68b88d0b87a6e0d58615e8fa07f9fd5a1319fa0a72efc1f62275c79a7"
dependencies = [
"nom",
"nom-derive-impl",
"rustversion",
]
[[package]]
name = "nom-derive-impl"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b9a93a84b0d3ec3e70e02d332dc33ac6dfac9cde63e17fcb77172dededa62"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
@@ -1148,6 +1335,28 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a"
dependencies = [
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.43",
]
[[package]]
name = "object"
version = "0.30.4"
@@ -1159,9 +1368,9 @@ dependencies = [
[[package]]
name = "octets"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a74f2cda724d43a0a63140af89836d4e7db6138ef67c9f96d3a0f0150d05000"
checksum = "109983a091271ee8916076731ba5fdc9ee22fea871bc7c6ceab9bfd423eb1d99"
[[package]]
name = "oid-registry"
@@ -1178,6 +1387,17 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
]
[[package]]
name = "os_info"
version = "3.7.0"
@@ -1227,6 +1447,44 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.1.0"
@@ -1271,6 +1529,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -1286,7 +1554,7 @@ version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"byteorder",
"hex",
"lazy_static",
@@ -1350,12 +1618,11 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
[[package]]
name = "qlog"
version = "0.9.0"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321df7a3199d152be256a416096136191e88b7716f1e2e4c8c05b9f77ffb648b"
checksum = "0f15b83c59e6b945f2261c95a1dd9faf239187f32ff0a96af1d1d28c4557f919"
dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_with",
"smallvec",
@@ -1363,21 +1630,25 @@ dependencies = [
[[package]]
name = "quiche"
version = "0.17.2"
version = "0.24.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9e4fa8718d45fd25dd89c196e128d6c3527b5b1735db47eb4bdb9ba3e4cc1c"
checksum = "88fa45f0d2e3fc7ca8b4306408f8b15df9a3b5ac70197f84dfc6ebf58fb7e26a"
dependencies = [
"boring",
"cmake",
"lazy_static",
"debug_panic",
"either",
"enum_dispatch",
"foreign-types-shared",
"intrusive-collections",
"libc",
"libm",
"log",
"octets",
"qlog",
"ring",
"slab",
"smallvec",
"winapi",
"windows-sys 0.59.0",
]
[[package]]
@@ -1437,7 +1708,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
@@ -1532,6 +1803,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.0"
@@ -1556,7 +1833,7 @@ version = "0.36.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14e4d67015953998ad0eb82887a0eb0129e18a7e2f3b7b0f6c422fddcd503d62"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
@@ -1570,7 +1847,7 @@ version = "0.37.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
@@ -1795,24 +2072,25 @@ dependencies = [
[[package]]
name = "serde_with"
version = "1.14.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [
"serde",
"serde_derive",
"serde_with_macros",
]
[[package]]
name = "serde_with_macros"
version = "1.5.2"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.43",
]
[[package]]
@@ -1830,6 +2108,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@@ -1839,6 +2123,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.8"
@@ -1879,6 +2169,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "1.0.109"
@@ -2009,6 +2305,20 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tls-parser"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22c36249c6082584b1f224e70f6bdadf5102197be6cfa92b353efe605d9ac741"
dependencies = [
"nom",
"nom-derive",
"num_enum",
"phf",
"phf_codegen",
"rusticata-macros",
]
[[package]]
name = "tokio"
version = "1.28.2"
@@ -2349,6 +2659,8 @@ dependencies = [
"chrono",
"clap",
"dialoguer",
"hex",
"ipnet",
"lazy_static",
"once_cell",
"rcgen",
@@ -2365,15 +2677,18 @@ version = "0.1.0"
dependencies = [
"async-trait",
"base64 0.21.2",
"boring",
"bytes",
"cc",
"chrono",
"dynfmt",
"futures",
"h2",
"hex",
"http",
"httparse",
"hyper",
"ipnet",
"lazy_static",
"libc",
"log",
@@ -2386,6 +2701,7 @@ dependencies = [
"rustls-pemfile",
"serde",
"smallvec",
"tls-parser",
"tokio",
"tokio-rustls",
"toml_edit",
@@ -2559,6 +2875,15 @@ dependencies = [
"windows-targets 0.48.0",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -2589,6 +2914,22 @@ dependencies = [
"windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@@ -2601,6 +2942,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -2613,6 +2960,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -2625,6 +2978,18 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -2637,6 +3002,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -2649,6 +3020,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -2661,6 +3038,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -2673,6 +3056,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.4.7"

View File

@@ -15,20 +15,23 @@ cc = "1.0.79"
[dependencies]
async-trait = "0.1.68"
base64 = "0.21.2"
tls-parser = "0.12.2"
bytes = "1.4.0"
chrono = { version = "0.4.26", default-features = false, features = ["clock"] }
dynfmt = { version = "0.1.5", features = ["curly"], default-features = false }
futures = "0.3.28"
h2 = "0.3.20"
hex = "0.4.3"
http = "0.2.9"
httparse = "1.8.0"
ipnet = "2.9"
lazy_static = "1.4.0"
libc = "0.2.147"
log = "0.4.19"
macros = { version = "0.1.0", path = "../macros", optional = true }
once_cell = "1.18.0"
prometheus = { version = "0.13.3", features = ["process"] }
quiche = { version = "0.17.2", features = ["qlog"] }
quiche = { version = "0.24.5", features = ["qlog", "boringssl-boring-crate"] }
ring = "0.16.20"
rustls = { version = "0.21.2", features = ["logging"] }
rustls-pemfile = "1.0.2"
@@ -37,6 +40,7 @@ smallvec = "1.10.0"
tokio = { version = "1.28.2", features = ["net", "rt", "sync", "time", "macros", "rt-multi-thread"] }
tokio-rustls = "0.24.1"
toml_edit = "0.19.10"
boring = "4"
[dev-dependencies]
hyper = { version = "0.14.26", features = ["http1", "http2", "client", "server", "runtime", "stream"] }

View File

@@ -5,7 +5,8 @@ use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, UdpSocket};
use crate::direct_forwarder::DirectForwarder;
use crate::{authentication, http_ping_handler, http_speedtest_handler, log_id, log_utils, metrics, net_utils, reverse_proxy, settings, tls_demultiplexer, tunnel};
use crate::{authentication, http_ping_handler, http_speedtest_handler, log_id, log_utils, metrics, net_utils, reverse_proxy, rules, settings, tls_demultiplexer, tunnel};
use crate::net_utils::PeerAddr;
use crate::tls_demultiplexer::TlsDemux;
use crate::forwarder::Forwarder;
use crate::http1_codec::Http1Codec;
@@ -158,7 +159,7 @@ impl Core {
let client_id = log_utils::IdChain::from(log_utils::IdItem::new(
log_utils::CLIENT_ID_FMT, self.context.next_client_id.fetch_add(1, Ordering::Relaxed),
));
let stream = match tcp_listener.accept().await
let (stream, client_addr) = match tcp_listener.accept().await
.and_then(|(s, a)| {
s.set_nodelay(true)?;
Ok((s, a))
@@ -166,7 +167,7 @@ impl Core {
{
Ok((stream, addr)) => if has_tcp_based_codec {
log_id!(debug, client_id, "New TCP client: {}", addr);
stream
(stream, addr)
} else {
continue; // accept just for pings
}
@@ -185,9 +186,9 @@ impl Core {
.await
.unwrap_or_else(|_| Err(io::Error::from(ErrorKind::TimedOut)))
{
Ok(stream) =>
Ok(acceptor) =>
if let Err((client_id, message)) = Core::on_new_tls_connection(
context.clone(), stream, client_id,
context.clone(), acceptor, client_addr.ip(), client_id,
).await {
log_id!(debug, client_id, "{}", message);
},
@@ -240,12 +241,19 @@ impl Core {
async fn on_new_tls_connection(
context: Arc<Context>,
acceptor: TlsAcceptor,
client_ip: std::net::IpAddr,
client_id: log_utils::IdChain<u64>,
) -> Result<(), (log_utils::IdChain<u64>, String)> {
let sni = match acceptor.sni() {
Some(s) => s,
None => return Err((client_id, "Drop TLS connection due to absence of SNI".to_string())),
};
// Apply connection filtering rules
if let Err(deny_reason) = Self::evaluate_connection_rules(
&context, Some(client_ip), acceptor.client_random().as_deref(), &client_id
) {
return Err((client_id, deny_reason));
}
let core_settings = context.settings.clone();
let tls_connection_meta =
@@ -336,6 +344,17 @@ impl Core {
socket: QuicSocket,
client_id: log_utils::IdChain<u64>,
) {
// Apply connection filtering rules
let client_ip = socket.peer_addr().ok().map(|addr| addr.ip());
let client_random = Some(socket.client_random());
if let Err(deny_reason) = Self::evaluate_connection_rules(
&context, client_ip, client_random.as_deref(), &client_id
) {
log_id!(debug, client_id, "{}", deny_reason);
return; // Drop the connection
}
let tls_connection_meta = socket.tls_connection_meta();
log_id!(debug, client_id, "Connection meta: {:?}", tls_connection_meta);
@@ -383,6 +402,32 @@ impl Core {
}
}
/// Helper function to evaluate connection filtering rules
fn evaluate_connection_rules(
context: &Arc<Context>,
client_ip: Option<std::net::IpAddr>,
client_random: Option<&[u8]>,
log_id: &log_utils::IdChain<u64>,
) -> Result<(), String> {
if let Some(rules_engine) = &context.settings.rules_engine {
if let Some(ip) = client_ip {
let rule_result = rules_engine.evaluate(&ip, client_random);
match rule_result {
rules::RuleEvaluation::Deny => {
log_id!(debug, log_id, "Connection denied by filtering rules for IP: {}", ip);
return Err("Connection denied by filtering rules".to_string());
}
rules::RuleEvaluation::Allow => {
log_id!(debug, log_id, "Connection allowed by filtering rules");
}
}
} else {
log_id!(warn, log_id, "Could not extract client IP for rules evaluation");
}
}
Ok(())
}
async fn on_tunnel_request(
context: Arc<Context>,
protocol: tls_demultiplexer::Protocol,

View File

@@ -11,6 +11,7 @@ pub mod shutdown;
pub mod net_utils;
pub mod utils;
pub mod client_config;
pub mod rules;
mod direct_forwarder;
mod downstream;

View File

@@ -5,6 +5,7 @@ use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use boring::ssl::{SelectCertError, SslContextBuilder, SslMethod, SslRef};
use bytes::{Buf, Bytes, BytesMut};
use http::header::InvalidHeaderName;
use lazy_static::lazy_static;
@@ -23,6 +24,7 @@ use crate::utils::Either;
const TOKEN_PREFIX_SIZE: usize = 16;
const MUX_ID_FMT: &str = "QMUX={}";
const SOCKET_ID_FMT: &str = "QSOCK={}";
const QUIC_CONNECTION_CLOSE_CODE: u64 = 0x42;
type QuicConnection = quiche::Connection;
@@ -56,6 +58,8 @@ pub(crate) struct QuicSocket {
waiting_writable_streams: std::sync::Mutex<HashSet<u64>>,
id: log_utils::IdChain<u64>,
tls_connection_meta: tls_demultiplexer::ConnectionMeta,
/// TLS client_random extracted from QUIC handshake
client_random: Vec<u8>,
}
pub(crate) enum QuicSocketEvent {
@@ -453,6 +457,15 @@ impl QuicMultiplexer {
Arc::new(std::sync::Mutex::new(h3_conn))
};
// Extract client_random from QUIC after handshake is complete
let extracted_client_random = {
let mut quic = quic_conn.lock().unwrap();
let ssl: &mut SslRef = quic.as_mut();
let mut client_random = [0u8; 32];
ssl.client_random(&mut client_random);
client_random.to_vec()
};
let (tx, rx) = mpsc::channel(1);
self.connections.insert(conn_id.clone().into_owned(), Connection::Established(EstablishedConnection {
socket_tx: tx,
@@ -471,6 +484,7 @@ impl QuicMultiplexer {
SOCKET_ID_FMT, self.next_socket_id.fetch_add(1, Ordering::Relaxed),
)),
tls_connection_meta: conn.tls_connection_meta,
client_random: extracted_client_random,
})
}
@@ -634,6 +648,10 @@ impl QuicSocket {
&self.tls_connection_meta
}
pub fn client_random(&self) -> Vec<u8> {
self.client_random.clone()
}
pub fn send_response(&self, stream_id: u64, response: ResponseHeaders, fin: bool) -> io::Result<()> {
let response: Vec<_> =
std::iter::once(h3::HeaderRef::new(b":status", response.status.as_str().as_bytes()))
@@ -814,9 +832,6 @@ impl QuicSocket {
log_id!(trace, self.id, "Stream reset by client: id={}, err={}", stream_id, err);
Ok(Some(QuicSocketEvent::Close(stream_id)))
}
Ok((_, h3::Event::Datagram)) => Err(io::Error::new(
ErrorKind::Other, "Received unexpected datagram frame",
)),
Ok((_, h3::Event::PriorityUpdate)) => Ok(None),
Ok((_, h3::Event::GoAway)) => Err(io::Error::new(
ErrorKind::UnexpectedEof, "Received GOAWAY",
@@ -937,9 +952,11 @@ fn quic_recv(
fn make_quic_conn_config(
core_settings: &Settings, cert_chain_file: &str, priv_key_file: &str,
) -> io::Result<quiche::Config> {
let ctx = SslContextBuilder::new(SslMethod::tls())?;
let quic_settings = core_settings.listen_protocols.quic.as_ref().unwrap();
let mut cfg = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap();
let mut cfg = quiche::Config::with_boring_ssl_ctx_builder(quiche::PROTOCOL_VERSION, ctx).unwrap();
cfg.load_cert_chain_from_pem_file(cert_chain_file).unwrap();
cfg.load_priv_key_from_pem_file(priv_key_file).unwrap();
cfg.set_application_protos(h3::APPLICATION_PROTOCOL).unwrap();

214
lib/src/rules.rs Normal file
View File

@@ -0,0 +1,214 @@
use std::net::IpAddr;
use std::path::Path;
use serde::{Deserialize, Serialize};
use ipnet::IpNet;
/// Action to take when a rule matches
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RuleAction {
Allow,
Deny,
}
/// Individual filter rule
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
/// CIDR range to match against client IP
#[serde(default)]
pub cidr: Option<String>,
/// Client random prefix to match (hex-encoded)
#[serde(default)]
pub client_random_prefix: Option<String>,
/// Action to take when this rule matches
pub action: RuleAction,
}
/// Rules configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RulesConfig {
/// List of filter rules
#[serde(default)]
pub rule: Vec<Rule>,
}
/// Rule evaluation engine
pub struct RulesEngine {
rules: RulesConfig,
}
/// Result of rule evaluation
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RuleEvaluation {
Allow,
Deny,
}
impl Rule {
/// Check if this rule matches the given connection parameters
pub fn matches(&self, client_ip: &IpAddr, client_random: Option<&[u8]>) -> bool {
let mut matches = true;
// Check CIDR match if specified
if let Some(cidr_str) = &self.cidr {
if let Ok(cidr) = cidr_str.parse::<IpNet>() {
matches &= cidr.contains(client_ip);
} else {
// Invalid CIDR, rule doesn't match
return false;
}
}
// Check client_random prefix if specified
if let Some(prefix_str) = &self.client_random_prefix {
if let Some(client_random_data) = client_random {
if let Ok(prefix_bytes) = hex::decode(prefix_str) {
matches &= client_random_data.starts_with(&prefix_bytes);
} else {
// Invalid hex prefix, rule doesn't match
return false;
}
} else {
// No client_random provided but rule requires it, doesn't match
matches = false;
}
}
matches
}
}
impl RulesEngine {
/// Create a new rules engine from rules config
pub fn from_config(rules: RulesConfig) -> Self {
Self { rules }
}
/// Create a default rules engine that allows all connections
pub fn default_allow() -> Self {
Self {
rules: RulesConfig { rule: vec![] }
}
}
/// Evaluate connection against all rules
/// Returns the action from the first matching rule, or Allow if no rules match
pub fn evaluate(&self, client_ip: &IpAddr, client_random: Option<&[u8]>) -> RuleEvaluation {
for rule in &self.rules.rule {
if rule.matches(client_ip, client_random) {
return match rule.action {
RuleAction::Allow => RuleEvaluation::Allow,
RuleAction::Deny => RuleEvaluation::Deny,
};
}
}
// Default action if no rules match: allow
RuleEvaluation::Allow
}
/// Get a reference to the rules configuration
pub fn config(&self) -> &RulesConfig {
&self.rules
}
}
impl Default for RulesConfig {
fn default() -> Self {
Self { rule: vec![] }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_cidr_rule_matching() {
let rule = Rule {
cidr: Some("192.168.1.0/24".to_string()),
client_random_prefix: None,
action: RuleAction::Allow,
};
let ip_match = IpAddr::from_str("192.168.1.100").unwrap();
let ip_no_match = IpAddr::from_str("10.0.0.1").unwrap();
assert!(rule.matches(&ip_match, None));
assert!(!rule.matches(&ip_no_match, None));
}
#[test]
fn test_client_random_prefix_matching() {
let rule = Rule {
cidr: None,
client_random_prefix: Some("aabbcc".to_string()),
action: RuleAction::Deny,
};
let client_random_match = hex::decode("aabbccddee").unwrap();
let client_random_no_match = hex::decode("112233").unwrap();
let ip = IpAddr::from_str("127.0.0.1").unwrap();
assert!(rule.matches(&ip, Some(&client_random_match)));
assert!(!rule.matches(&ip, Some(&client_random_no_match)));
assert!(!rule.matches(&ip, None)); // No client random provided
}
#[test]
fn test_combined_rule_matching() {
let rule = Rule {
cidr: Some("10.0.0.0/8".to_string()),
client_random_prefix: Some("ff".to_string()),
action: RuleAction::Allow,
};
let ip_match = IpAddr::from_str("10.1.2.3").unwrap();
let ip_no_match = IpAddr::from_str("192.168.1.1").unwrap();
let client_random_match = hex::decode("ff00112233").unwrap();
let client_random_no_match = hex::decode("0011223344").unwrap();
// Both must match
assert!(rule.matches(&ip_match, Some(&client_random_match)));
assert!(!rule.matches(&ip_match, Some(&client_random_no_match)));
assert!(!rule.matches(&ip_no_match, Some(&client_random_match)));
assert!(!rule.matches(&ip_no_match, Some(&client_random_no_match)));
}
#[test]
fn test_rules_engine_evaluation() {
let rules = RulesConfig {
rule: vec![
Rule {
cidr: Some("192.168.1.0/24".to_string()),
client_random_prefix: None,
action: RuleAction::Deny,
},
Rule {
cidr: Some("10.0.0.0/8".to_string()),
client_random_prefix: None,
action: RuleAction::Allow,
},
Rule {
cidr: None,
client_random_prefix: None,
action: RuleAction::Deny, // Catch-all deny
},
],
};
let engine = RulesEngine::from_config(rules);
let ip_deny = IpAddr::from_str("192.168.1.100").unwrap();
let ip_allow = IpAddr::from_str("10.1.2.3").unwrap();
let ip_default = IpAddr::from_str("172.16.1.1").unwrap();
assert_eq!(engine.evaluate(&ip_deny, None), RuleEvaluation::Deny);
assert_eq!(engine.evaluate(&ip_allow, None), RuleEvaluation::Allow);
assert_eq!(engine.evaluate(&ip_default, None), RuleEvaluation::Deny); // Default deny
}
}

View File

@@ -11,7 +11,7 @@ use macros::{Getter, RuntimeDoc};
use serde::{Deserialize, Serialize};
use toml_edit::{Document, Item};
use authentication::registry_based::Client;
use crate::{authentication, utils};
use crate::{authentication, utils, rules};
pub type Socks5BuilderResult<T> = Result<T, Socks5Error>;
@@ -28,6 +28,8 @@ pub enum ValidationError {
ReverseProxy(String),
/// Invalid [`Settings.listen_protocols`]
ListenProtocols(String),
/// Invalid rules file
RulesFile(String),
}
impl Debug for ValidationError {
@@ -39,6 +41,7 @@ impl Debug for ValidationError {
Self::SpeedTlsHostInfo(x) => write!(f, "Invalid speedtest TLS hosts: {}", x),
Self::ReverseProxy(x) => write!(f, "Invalid reverse proxy settings: {}", x),
Self::ListenProtocols(x) => write!(f, "Invalid listen protocols settings: {}", x),
Self::RulesFile(x) => write!(f, "Invalid rules file: {}", x),
}
}
}
@@ -142,6 +145,13 @@ pub struct Settings {
pub(crate) icmp: Option<IcmpSettings>,
/// The metrics gathering request handler settings
pub(crate) metrics: Option<MetricsSettings>,
/// Path to the rules file for connection filtering.
/// If not specified or file doesn't exist, all connections are allowed by default.
#[serde(default)]
#[serde(skip_serializing)]
#[serde(rename(deserialize = "rules_file"))]
#[serde(deserialize_with = "deserialize_rules")]
pub(crate) rules_engine: Option<rules::RulesEngine>,
/// Whether an instance was built through a [`SettingsBuilder`].
/// This flag is a workaround for absence of the ability to validate
@@ -496,6 +506,7 @@ impl Default for Settings {
reverse_proxy: None,
icmp: None,
metrics: Default::default(),
rules_engine: Some(rules::RulesEngine::default_allow()),
built: false,
}
}
@@ -732,6 +743,7 @@ impl SettingsBuilder {
reverse_proxy: None,
icmp: None,
metrics: Default::default(),
rules_engine: Some(rules::RulesEngine::default_allow()),
built: true,
},
}
@@ -839,6 +851,12 @@ impl SettingsBuilder {
self.settings.metrics = Some(x);
self
}
/// Set the rules engine for connection filtering
pub fn rules_engine(mut self, x: rules::RulesEngine) -> Self {
self.settings.rules_engine = Some(x);
self
}
}
impl TlsSettingsBuilder {
@@ -1311,6 +1329,75 @@ fn deserialize_clients<'de, D>(deserializer: D) -> Result<Vec<Client>, D::Error>
Ok(res)
}
fn deserialize_rules<'de, D>(deserializer: D) -> Result<Option<rules::RulesEngine>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let path = match deserialize_file_path(deserializer) {
Ok(path) => path,
Err(_) => {
// No rules file specified, default to allow all
return Ok(Some(rules::RulesEngine::default_allow()));
}
};
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(e) => {
// Log warning but don't fail - default to allow all
eprintln!("Warning: Could not read rules file '{}': {}. Defaulting to allow all connections.", path, e);
return Ok(Some(rules::RulesEngine::default_allow()));
}
};
let rules_doc: Document = match content.parse() {
Ok(doc) => doc,
Err(e) => {
eprintln!("Warning: Could not parse rules file '{}': {}. Defaulting to allow all connections.", path, e);
return Ok(Some(rules::RulesEngine::default_allow()));
}
};
let rules_config = match rules_doc.get("rule").and_then(Item::as_array_of_tables) {
Some(rules_array) => {
let rules: Vec<rules::Rule> = rules_array
.iter()
.filter_map(|rule_table| {
let cidr = rule_table.get("cidr")
.and_then(Item::as_str)
.map(|s| s.to_string());
let client_random_prefix = rule_table.get("client_random_prefix")
.and_then(Item::as_str)
.map(|s| s.to_string());
let action = rule_table.get("action")
.and_then(Item::as_str)
.and_then(|s| match s {
"allow" => Some(rules::RuleAction::Allow),
"deny" => Some(rules::RuleAction::Deny),
_ => None,
})?;
Some(rules::Rule {
cidr,
client_random_prefix,
action,
})
})
.collect();
rules::RulesConfig { rule: rules }
}
None => {
// No rules array found, create empty config
rules::RulesConfig { rule: vec![] }
}
};
Ok(Some(rules::RulesEngine::from_config(rules_config)))
}
fn demangle_toml_string(x: String) -> String {
x.replace('"', "")
.trim()

View File

@@ -5,13 +5,14 @@ use rustls::{Certificate, PrivateKey, ServerConfig};
use tokio::net::TcpStream;
use tokio_rustls::{LazyConfigAcceptor, StartHandshake};
use tokio_rustls::server::TlsStream;
use tls_parser::{parse_tls_plaintext, TlsMessage};
use crate::{log_utils, tls_demultiplexer};
pub(crate) struct TlsListener {}
pub(crate) struct TlsAcceptor {
inner: StartHandshake<TcpStream>,
client_random: Option<Vec<u8>>,
}
impl TlsListener {
@@ -20,12 +21,47 @@ impl TlsListener {
}
pub async fn listen(&self, stream: TcpStream) -> io::Result<TlsAcceptor> {
// Peek at the first 1024 bytes to parse Client Hello
let mut buffer = vec![0u8; 1024];
let bytes_peeked = stream.peek(&mut buffer).await?;
// Extract client_random from the peeked data
let client_random = Self::extract_client_random(&buffer[..bytes_peeked]);
// Now let rustls handle the stream normally
LazyConfigAcceptor::new(rustls::server::Acceptor::default(), stream)
.await
.map(|hs| TlsAcceptor {
inner: hs,
client_random,
})
}
fn extract_client_random(data: &[u8]) -> Option<Vec<u8>> {
// Parse TLS plaintext record
match parse_tls_plaintext(data) {
Ok((_, plaintext)) => {
// Look for handshake messages
for message in &plaintext.msg {
if let TlsMessage::Handshake(handshake) = message {
// Check if this is a ClientHello handshake
if matches!(handshake, tls_parser::TlsMessageHandshake::ClientHello(..)) {
// Extract the ClientHello data
if let tls_parser::TlsMessageHandshake::ClientHello(client_hello) = handshake {
if client_hello.random.len() >= 32 {
let client_random = client_hello.random[..32].to_vec();
return Some(client_random);
}
}
}
}
}
}
Err(e) => log::debug!("Failed to parse TLS plaintext: {:?}", e),
}
None
}
}
impl TlsAcceptor {
@@ -40,6 +76,10 @@ impl TlsAcceptor {
.unwrap_or_default()
}
pub fn client_random(&self) -> Option<Vec<u8>> {
self.client_random.clone()
}
pub async fn accept(
self,
protocol: tls_demultiplexer::Protocol,
@@ -62,4 +102,4 @@ impl TlsAcceptor {
self.inner.into_stream(tls_config).await
}
}
}

View File

@@ -12,6 +12,8 @@ path = "setup_wizard/main.rs"
chrono = { version = "0.4.26", default-features = false, features = ["clock"] }
clap = "4.3.8"
dialoguer = "0.10.4"
hex = "0.4"
ipnet = "2.9"
lazy_static = "1.4.0"
once_cell = "1.18.0"
rcgen = "0.10.0"

View File

@@ -4,8 +4,8 @@ use vpn_libs_endpoint::settings::{ForwardProtocolSettings, Http1Settings, Http2S
use vpn_libs_endpoint::utils::{IterJoin, ToTomlComment};
use crate::template_settings;
pub fn compose_document(settings: &Settings, credentials_path: &str) -> String {
once(compose_main_table(settings, credentials_path))
pub fn compose_document(settings: &Settings, credentials_path: &str, rules_path: &str) -> String {
once(compose_main_table(settings, credentials_path, rules_path))
.chain(once(compose_forward_protocol_table(settings.get_forward_protocol())))
.chain(once(compose_listener_protocol_table(settings.get_listen_protocols())))
.chain(once(compose_icmp_table(settings.get_icmp().as_ref())))
@@ -13,11 +13,12 @@ pub fn compose_document(settings: &Settings, credentials_path: &str) -> String {
.join("\n")
}
fn compose_main_table(settings: &Settings, credentials_path: &str) -> String {
fn compose_main_table(settings: &Settings, credentials_path: &str, rules_path: &str) -> String {
let mut doc: Document = template_settings::MAIN_TABLE.parse().unwrap();
doc["listen_address"] = value(settings.get_listen_address().to_string());
doc["credentials_file"] = value(credentials_path);
doc["rules_file"] = value(rules_path);
doc["ipv6_available"] = value(*settings.get_ipv6_available());
doc["allow_private_network_connections"] = value(*settings.get_allow_private_network_connections());
doc["tls_handshake_timeout_secs"] = value(settings.get_tls_handshake_timeout().as_secs() as i64);

View File

@@ -5,10 +5,12 @@ use crate::Mode;
use crate::user_interaction::{ask_for_agreement, ask_for_input, ask_for_password, checked_overwrite, select_variant};
pub const DEFAULT_CREDENTIALS_PATH: &str = "credentials.toml";
pub const DEFAULT_RULES_PATH: &str = "rules.toml";
pub struct Built {
pub settings: Settings,
pub credentials_path: String,
pub rules_path: String,
}
pub fn build() -> Built {
@@ -30,6 +32,7 @@ pub fn build() -> Built {
})
.build().expect("Couldn't build the library settings"),
credentials_path: build_authenticator(),
rules_path: build_rules(),
}
}
@@ -57,6 +60,31 @@ fn build_authenticator() -> String {
}
}
fn build_rules() -> String {
if crate::get_mode() != Mode::NonInteractive
&& check_file_exists(".", DEFAULT_RULES_PATH)
&& ask_for_agreement(&format!("Reuse the existing rules file: {DEFAULT_RULES_PATH}?"))
{
DEFAULT_RULES_PATH.into()
} else {
let path = ask_for_input::<String>(
"Path to the rules file",
Some(DEFAULT_RULES_PATH.into()),
);
if checked_overwrite(&path, "Overwrite the existing rules file?") {
println!("Let's create connection filtering rules");
let rules_config = crate::rules_settings::build();
let rules_content = generate_rules_toml_content(&rules_config);
fs::write(&path, rules_content)
.expect("Couldn't write the rules into a file");
println!("The rules configuration is written to file: {}", path);
}
path
}
}
fn build_user_list() -> Vec<(String, String)> {
if let Some(x) = crate::get_predefined_params().credentials.clone() {
return vec![x];
@@ -96,6 +124,33 @@ fn compose_credentials_content(clients: impl Iterator<Item=(String, String)>) ->
doc.to_string()
}
fn generate_rules_toml_content(rules_config: &vpn_libs_endpoint::rules::RulesConfig) -> String {
let mut content = String::new();
// Add header comments explaining the format
content.push_str("# Rules configuration for VPN endpoint connection filtering\n");
content.push_str("# \n");
content.push_str("# This file defines filter rules for incoming connections.\n");
content.push_str("# Rules are evaluated in order, and the first matching rule's action is applied.\n");
content.push_str("# If no rules match, the connection is allowed by default.\n");
content.push_str("#\n");
content.push_str("# Each rule can specify:\n");
content.push_str("# - cidr: IP address range in CIDR notation\n");
content.push_str("# - client_random_prefix: Hex-encoded prefix of TLS client random data\n");
content.push_str("# - action: \"allow\" or \"deny\"\n");
content.push_str("#\n");
content.push_str("# Both cidr and client_random_prefix are optional - if specified, both must match for the rule to apply.\n");
content.push_str("# If only one is specified, only that condition needs to match.\n\n");
// Serialize the actual rules (usually empty)
if !rules_config.rule.is_empty() {
content.push_str(&toml::ser::to_string(rules_config).unwrap());
content.push_str("\n");
}
content
}
fn check_file_exists(path: &str, name: &str) -> bool {
match fs::read_dir(path) {
Ok(x) => x.filter_map(Result::ok)

View File

@@ -5,6 +5,7 @@ use crate::user_interaction::{ask_for_agreement, ask_for_input, checked_overwrit
mod composer;
mod library_settings;
mod rules_settings;
mod template_settings;
mod tls_hosts_settings;
mod user_interaction;
@@ -134,7 +135,7 @@ Required in non-interactive mode."#),
Some("vpn.toml".into()),
));
if checked_overwrite(&path, "Overwrite the existing library settings file?") {
let doc = composer::compose_document(&built.settings, &built.credentials_path);
let doc = composer::compose_document(&built.settings, &built.credentials_path, &built.rules_path);
fs::write(&path, doc)
.expect("Couldn't write the library settings to a file");
}

View File

@@ -0,0 +1,152 @@
use vpn_libs_endpoint::rules::{Rule, RuleAction, RulesConfig};
use crate::user_interaction::{ask_for_agreement, ask_for_input};
use crate::get_mode;
pub fn build() -> RulesConfig {
match get_mode() {
crate::Mode::NonInteractive => build_non_interactive(),
crate::Mode::Interactive => build_interactive(),
}
}
fn build_non_interactive() -> RulesConfig {
// In non-interactive mode, generate empty rules
// The actual examples will be in the serialized TOML comments
RulesConfig { rule: vec![] }
}
fn build_interactive() -> RulesConfig {
println!("Setting up connection filtering rules...");
let mut rules = Vec::new();
// Ask if user wants to configure rules
if !ask_for_agreement("Do you want to configure connection filtering rules? (if not, all connections will be allowed)") {
println!("Skipping rules configuration - all connections will be allowed.");
return RulesConfig { rule: vec![] };
}
println!();
println!("You can configure rules to allow/deny connections based on:");
println!(" - Client IP address (CIDR notation, e.g., 192.168.1.0/24)");
println!(" - TLS client random prefix (hex-encoded, e.g., aabbcc)");
println!(" - Both conditions together");
println!();
add_custom_rules(&mut rules);
RulesConfig { rule: rules }
}
fn add_custom_rules(rules: &mut Vec<Rule>) {
println!();
while ask_for_agreement("Add a custom rule?") {
let rule_type = ask_for_input::<String>(
"Rule type (1=IP range, 2=client random prefix, 3=both)",
Some("1".to_string()),
);
match rule_type.as_str() {
"1" => add_ip_rule(rules),
"2" => add_client_random_rule(rules),
"3" => add_combined_rule(rules),
_ => {
println!("Invalid choice. Skipping rule.");
continue;
}
}
println!();
}
}
fn add_ip_rule(rules: &mut Vec<Rule>) {
let cidr = ask_for_input::<String>(
"Enter IP range in CIDR notation (e.g., 203.0.113.0/24)",
None,
);
// Validate CIDR format
if let Err(_) = cidr.parse::<ipnet::IpNet>() {
println!("Invalid CIDR format. Skipping rule.");
return;
}
let action = ask_for_rule_action();
rules.push(Rule {
cidr: Some(cidr),
client_random_prefix: None,
action,
});
println!("Rule added successfully.");
}
fn add_client_random_rule(rules: &mut Vec<Rule>) {
let prefix = ask_for_input::<String>(
"Enter client random prefix (hex, e.g., aabbcc)",
None,
);
// Validate hex format
if let Err(_) = hex::decode(&prefix) {
println!("Invalid hex format. Skipping rule.");
return;
}
let action = ask_for_rule_action();
rules.push(Rule {
cidr: None,
client_random_prefix: Some(prefix),
action,
});
println!("Rule added successfully.");
}
fn add_combined_rule(rules: &mut Vec<Rule>) {
let cidr = ask_for_input::<String>(
"Enter IP range in CIDR notation (e.g., 172.16.0.0/12)",
None,
);
// Validate CIDR format
if let Err(_) = cidr.parse::<ipnet::IpNet>() {
println!("Invalid CIDR format. Skipping rule.");
return;
}
let prefix = ask_for_input::<String>(
"Enter client random prefix (hex, e.g., 001122)",
None,
);
// Validate hex format
if let Err(_) = hex::decode(&prefix) {
println!("Invalid hex format. Skipping rule.");
return;
}
let action = ask_for_rule_action();
rules.push(Rule {
cidr: Some(cidr),
client_random_prefix: Some(prefix),
action,
});
println!("Rule added successfully.");
}
fn ask_for_rule_action() -> RuleAction {
let action_str = ask_for_input::<String>(
"Action (allow/deny)",
Some("allow".to_string()),
);
match action_str.to_lowercase().as_str() {
"deny" => RuleAction::Deny,
_ => RuleAction::Allow,
}
}

View File

@@ -18,6 +18,24 @@ listen_address = ""
# ```
credentials_file = "{}"
# The path to a TOML file for connection filtering rules in the following format:
#
# ```
# [[rule]]
# cidr = "192.168.0.0/16"
# action = "allow"
#
# [[rule]]
# client_random_prefix = "aabbcc"
# action = "deny"
#
# [[rule]]
# action = "deny"
#
# If no rules in this file, all connections are allowed by default.
# ```
rules_file = "{}"
{}
ipv6_available = {}
@@ -41,6 +59,7 @@ udp_connections_timeout_secs = {}
"#,
Settings::doc_listen_address().to_toml_comment(),
crate::library_settings::DEFAULT_CREDENTIALS_PATH,
crate::library_settings::DEFAULT_RULES_PATH,
Settings::doc_ipv6_available().to_toml_comment(),
Settings::default_ipv6_available(),
Settings::doc_allow_private_network_connections().to_toml_comment(),