├── .github ├── dependabot.yaml └── workflows │ ├── build_container.yaml │ ├── ci.yaml │ └── daily_change_check.yaml ├── .gitignore ├── .mailmap ├── Cargo.lock ├── Cargo.toml ├── Containerfile ├── LICENSE ├── README.md ├── src ├── config.rs ├── context.rs ├── http.rs ├── logger.rs ├── main.rs ├── proxy_protocol.rs ├── reader.rs ├── tcp │ ├── http.rs │ ├── mod.rs │ ├── tcp.rs │ └── tls.rs ├── tls.rs └── zc.rs └── test_data ├── example.net.crt └── example.net.key /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | -------------------------------------------------------------------------------- /.github/workflows/build_container.yaml: -------------------------------------------------------------------------------- 1 | name: Build and push a container image 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | build-and-push-image: 7 | runs-on: ubuntu-latest 8 | 9 | permissions: 10 | packages: write 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Build image 16 | id: build_image 17 | uses: redhat-actions/buildah-build@v2 18 | with: 19 | containerfiles: Containerfile 20 | image: sniproxy 21 | tags: "latest" 22 | 23 | - name: Push image 24 | uses: redhat-actions/push-to-registry@v2 25 | with: 26 | image: ${{ steps.build_image.outputs.image }} 27 | tags: ${{ steps.build_image.outputs.tags }} 28 | registry: ghcr.io/${{ github.repository_owner }} 29 | username: ${{ github.actor }} 30 | password: ${{ github.token }} 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install the Rust toolchain 13 | run: | 14 | rustup override set nightly 15 | rustup update nightly 16 | rustup component add rustfmt clippy 17 | 18 | - name: Check coding style 19 | run: cargo fmt --check 20 | 21 | - name: Run linter 22 | run: cargo clippy -- -D warnings 23 | 24 | - name: Unit tests 25 | run: cargo test --verbose 26 | -------------------------------------------------------------------------------- /.github/workflows/daily_change_check.yaml: -------------------------------------------------------------------------------- 1 | name: Daily check for new commits in main 2 | 3 | on: 4 | # Every day at 01:42 AM UTC. 5 | schedule: 6 | - cron: "42 01 * * *" 7 | 8 | jobs: 9 | daily-change-check: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Check for new commits in main and trigger actions 16 | run: | 17 | if [ -z "$(git log -n1 --since=yesterday --oneline)" ]; then 18 | exit 0 19 | fi 20 | gh workflow run build_container.yaml 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | sniproxy.yaml 3 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Antoine Tenart 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys 0.59.0", 77 | ] 78 | 79 | [[package]] 80 | name = "anyhow" 81 | version = "1.0.98" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 84 | 85 | [[package]] 86 | name = "backtrace" 87 | version = "0.3.75" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 90 | dependencies = [ 91 | "addr2line", 92 | "cfg-if", 93 | "libc", 94 | "miniz_oxide", 95 | "object", 96 | "rustc-demangle", 97 | "windows-targets", 98 | ] 99 | 100 | [[package]] 101 | name = "bytes" 102 | version = "1.10.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 105 | 106 | [[package]] 107 | name = "cfg-if" 108 | version = "1.0.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 111 | 112 | [[package]] 113 | name = "clap" 114 | version = "4.5.39" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 117 | dependencies = [ 118 | "clap_builder", 119 | "clap_derive", 120 | ] 121 | 122 | [[package]] 123 | name = "clap_builder" 124 | version = "4.5.39" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 127 | dependencies = [ 128 | "anstream", 129 | "anstyle", 130 | "clap_lex", 131 | "strsim", 132 | ] 133 | 134 | [[package]] 135 | name = "clap_derive" 136 | version = "4.5.32" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 139 | dependencies = [ 140 | "heck", 141 | "proc-macro2", 142 | "quote", 143 | "syn", 144 | ] 145 | 146 | [[package]] 147 | name = "clap_lex" 148 | version = "0.7.4" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 151 | 152 | [[package]] 153 | name = "colorchoice" 154 | version = "1.0.3" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 157 | 158 | [[package]] 159 | name = "deranged" 160 | version = "0.4.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 163 | dependencies = [ 164 | "powerfmt", 165 | ] 166 | 167 | [[package]] 168 | name = "equivalent" 169 | version = "1.0.2" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 172 | 173 | [[package]] 174 | name = "gimli" 175 | version = "0.31.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 178 | 179 | [[package]] 180 | name = "hashbrown" 181 | version = "0.15.3" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 184 | 185 | [[package]] 186 | name = "heck" 187 | version = "0.5.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 190 | 191 | [[package]] 192 | name = "indexmap" 193 | version = "2.9.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 196 | dependencies = [ 197 | "equivalent", 198 | "hashbrown", 199 | ] 200 | 201 | [[package]] 202 | name = "ipnet" 203 | version = "2.11.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 206 | dependencies = [ 207 | "serde", 208 | ] 209 | 210 | [[package]] 211 | name = "is_terminal_polyfill" 212 | version = "1.70.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 215 | 216 | [[package]] 217 | name = "itoa" 218 | version = "1.0.15" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 221 | 222 | [[package]] 223 | name = "libc" 224 | version = "0.2.172" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 227 | 228 | [[package]] 229 | name = "log" 230 | version = "0.4.27" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 233 | 234 | [[package]] 235 | name = "memchr" 236 | version = "2.7.4" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 239 | 240 | [[package]] 241 | name = "miniz_oxide" 242 | version = "0.8.8" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 245 | dependencies = [ 246 | "adler2", 247 | ] 248 | 249 | [[package]] 250 | name = "mio" 251 | version = "1.0.3" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 254 | dependencies = [ 255 | "libc", 256 | "wasi", 257 | "windows-sys 0.52.0", 258 | ] 259 | 260 | [[package]] 261 | name = "num-conv" 262 | version = "0.1.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 265 | 266 | [[package]] 267 | name = "object" 268 | version = "0.36.7" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 271 | dependencies = [ 272 | "memchr", 273 | ] 274 | 275 | [[package]] 276 | name = "once_cell" 277 | version = "1.21.3" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 280 | 281 | [[package]] 282 | name = "pin-project-lite" 283 | version = "0.2.16" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 286 | 287 | [[package]] 288 | name = "powerfmt" 289 | version = "0.2.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 292 | 293 | [[package]] 294 | name = "proc-macro2" 295 | version = "1.0.95" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 298 | dependencies = [ 299 | "unicode-ident", 300 | ] 301 | 302 | [[package]] 303 | name = "quote" 304 | version = "1.0.40" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 307 | dependencies = [ 308 | "proc-macro2", 309 | ] 310 | 311 | [[package]] 312 | name = "regex" 313 | version = "1.11.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 316 | dependencies = [ 317 | "aho-corasick", 318 | "memchr", 319 | "regex-automata", 320 | "regex-syntax", 321 | ] 322 | 323 | [[package]] 324 | name = "regex-automata" 325 | version = "0.4.9" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 328 | dependencies = [ 329 | "aho-corasick", 330 | "memchr", 331 | "regex-syntax", 332 | ] 333 | 334 | [[package]] 335 | name = "regex-syntax" 336 | version = "0.8.5" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 339 | 340 | [[package]] 341 | name = "rustc-demangle" 342 | version = "0.1.24" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 345 | 346 | [[package]] 347 | name = "ryu" 348 | version = "1.0.20" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 351 | 352 | [[package]] 353 | name = "serde" 354 | version = "1.0.219" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 357 | dependencies = [ 358 | "serde_derive", 359 | ] 360 | 361 | [[package]] 362 | name = "serde_derive" 363 | version = "1.0.219" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 366 | dependencies = [ 367 | "proc-macro2", 368 | "quote", 369 | "syn", 370 | ] 371 | 372 | [[package]] 373 | name = "serde_yaml" 374 | version = "0.9.34+deprecated" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 377 | dependencies = [ 378 | "indexmap", 379 | "itoa", 380 | "ryu", 381 | "serde", 382 | "unsafe-libyaml", 383 | ] 384 | 385 | [[package]] 386 | name = "sniproxy" 387 | version = "0.1.0" 388 | dependencies = [ 389 | "anyhow", 390 | "clap", 391 | "ipnet", 392 | "libc", 393 | "log", 394 | "once_cell", 395 | "regex", 396 | "serde", 397 | "serde_yaml", 398 | "socket2", 399 | "termcolor", 400 | "thiserror", 401 | "time", 402 | "tokio", 403 | ] 404 | 405 | [[package]] 406 | name = "socket2" 407 | version = "0.5.10" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 410 | dependencies = [ 411 | "libc", 412 | "windows-sys 0.52.0", 413 | ] 414 | 415 | [[package]] 416 | name = "strsim" 417 | version = "0.11.1" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 420 | 421 | [[package]] 422 | name = "syn" 423 | version = "2.0.101" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 426 | dependencies = [ 427 | "proc-macro2", 428 | "quote", 429 | "unicode-ident", 430 | ] 431 | 432 | [[package]] 433 | name = "termcolor" 434 | version = "1.4.1" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 437 | dependencies = [ 438 | "winapi-util", 439 | ] 440 | 441 | [[package]] 442 | name = "thiserror" 443 | version = "2.0.12" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 446 | dependencies = [ 447 | "thiserror-impl", 448 | ] 449 | 450 | [[package]] 451 | name = "thiserror-impl" 452 | version = "2.0.12" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 455 | dependencies = [ 456 | "proc-macro2", 457 | "quote", 458 | "syn", 459 | ] 460 | 461 | [[package]] 462 | name = "time" 463 | version = "0.3.41" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 466 | dependencies = [ 467 | "deranged", 468 | "itoa", 469 | "num-conv", 470 | "powerfmt", 471 | "serde", 472 | "time-core", 473 | "time-macros", 474 | ] 475 | 476 | [[package]] 477 | name = "time-core" 478 | version = "0.1.4" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 481 | 482 | [[package]] 483 | name = "time-macros" 484 | version = "0.2.22" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 487 | dependencies = [ 488 | "num-conv", 489 | "time-core", 490 | ] 491 | 492 | [[package]] 493 | name = "tokio" 494 | version = "1.45.1" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 497 | dependencies = [ 498 | "backtrace", 499 | "bytes", 500 | "libc", 501 | "mio", 502 | "pin-project-lite", 503 | "socket2", 504 | "tokio-macros", 505 | "windows-sys 0.52.0", 506 | ] 507 | 508 | [[package]] 509 | name = "tokio-macros" 510 | version = "2.5.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 513 | dependencies = [ 514 | "proc-macro2", 515 | "quote", 516 | "syn", 517 | ] 518 | 519 | [[package]] 520 | name = "unicode-ident" 521 | version = "1.0.18" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 524 | 525 | [[package]] 526 | name = "unsafe-libyaml" 527 | version = "0.2.11" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 530 | 531 | [[package]] 532 | name = "utf8parse" 533 | version = "0.2.2" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 536 | 537 | [[package]] 538 | name = "wasi" 539 | version = "0.11.0+wasi-snapshot-preview1" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 542 | 543 | [[package]] 544 | name = "winapi-util" 545 | version = "0.1.9" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 548 | dependencies = [ 549 | "windows-sys 0.59.0", 550 | ] 551 | 552 | [[package]] 553 | name = "windows-sys" 554 | version = "0.52.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 557 | dependencies = [ 558 | "windows-targets", 559 | ] 560 | 561 | [[package]] 562 | name = "windows-sys" 563 | version = "0.59.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 566 | dependencies = [ 567 | "windows-targets", 568 | ] 569 | 570 | [[package]] 571 | name = "windows-targets" 572 | version = "0.52.6" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 575 | dependencies = [ 576 | "windows_aarch64_gnullvm", 577 | "windows_aarch64_msvc", 578 | "windows_i686_gnu", 579 | "windows_i686_gnullvm", 580 | "windows_i686_msvc", 581 | "windows_x86_64_gnu", 582 | "windows_x86_64_gnullvm", 583 | "windows_x86_64_msvc", 584 | ] 585 | 586 | [[package]] 587 | name = "windows_aarch64_gnullvm" 588 | version = "0.52.6" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 591 | 592 | [[package]] 593 | name = "windows_aarch64_msvc" 594 | version = "0.52.6" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 597 | 598 | [[package]] 599 | name = "windows_i686_gnu" 600 | version = "0.52.6" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 603 | 604 | [[package]] 605 | name = "windows_i686_gnullvm" 606 | version = "0.52.6" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 609 | 610 | [[package]] 611 | name = "windows_i686_msvc" 612 | version = "0.52.6" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 615 | 616 | [[package]] 617 | name = "windows_x86_64_gnu" 618 | version = "0.52.6" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 621 | 622 | [[package]] 623 | name = "windows_x86_64_gnullvm" 624 | version = "0.52.6" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 627 | 628 | [[package]] 629 | name = "windows_x86_64_msvc" 630 | version = "0.52.6" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 633 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sniproxy" 3 | license = "GPL-2.0+" 4 | authors = ["Antoine Tenart "] 5 | version = "0.1.0" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | anyhow = "1.0" 10 | clap = { version = "4.5", features = ["derive"] } 11 | ipnet = { version = "2.11", features = ["serde"] } 12 | libc = "0.2" 13 | log = { version = "0.4", features = ["std"] } 14 | once_cell = "1.21" 15 | regex = "1.10" 16 | serde = "1.0" 17 | serde_yaml = "0.9" 18 | socket2 = "0.5" 19 | termcolor = "1.3" 20 | thiserror = "2.0" 21 | time = { version = "0.3", features = ["formatting", "macros"] } 22 | tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread"] } 23 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/rustlang/rust:nightly-alpine as builder 2 | RUN apk add musl-dev 3 | WORKDIR /usr/src/sniproxy 4 | COPY Cargo.* . 5 | COPY src/ src 6 | RUN cargo install --path . 7 | 8 | FROM alpine:latest 9 | RUN apk --no-cache upgrade 10 | WORKDIR / 11 | COPY --from=builder /usr/local/cargo/bin/sniproxy . 12 | EXPOSE 80/tcp 443/tcp 13 | ENTRYPOINT ["/sniproxy"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SNIProxy 2 | 3 | _SNIProxy_ is a TLS proxy, based on the TLS 4 | [Server Name Indication (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication). 5 | The SNI is contained in TLS handshakes and _SNIProxy_ uses it to route 6 | connections to backends. _SNIProxy_ does not need the TLS encryption keys and 7 | cannot decrypt the TLS traffic that goes through. 8 | 9 | _SNIProxy_ is meant to be simple to use and configure, with sane defaults and 10 | few parameters. 11 | 12 | A first version was written in [Go](https://go.dev) and can be found in the 13 | [archive/go](https://github.com/atenart/sniproxy/tree/archive/go) branch. It was 14 | latter rewritten in [Rust](https://www.rust-lang.org). 15 | 16 | ## Container image 17 | 18 | ```shell 19 | $ podman run --name sniproxy -p 80:80/tcp -p 443:443/tcp \ 20 | -v $(pwd)/sniproxy.yaml:/sniproxy.yaml:ro \ 21 | ghcr.io/atenart/sniproxy:latest 22 | ``` 23 | 24 | The above works with Docker too, just replace `podman` with `docker`. 25 | 26 | _SNIProxy_ handles HTTP connections and redirects those to their HTTPS 27 | counterparts. If this is not needed, the above `-p 80:80/tcp` can be omitted. 28 | 29 | ## Parameters 30 | 31 | The log level can be controlled using the `--log-level` CLI parameter. By 32 | default `INFO` and above levels are reported. The configuration file can be 33 | selected by the `--config` CLI parameter, and defaults to `sniproxy.yaml`. 34 | 35 | See `sniproxy --help` for a list of available parameters. 36 | 37 | ## Configuration file 38 | 39 | _SNIProxy_'s configuration file is written in the 40 | [YAML](https://en.wikipedia.org/wiki/YAML) format. 41 | 42 | ```text 43 | --- 44 | bind_https: 45 | bind_http: 46 | routes: 47 | - domains: 48 | - 49 | - 50 | backend: 51 | address: 52 | proxy_protocol: 53 | alpn_challenge_backend: 54 | address: 55 | proxy_protocol: 56 | alpn_challenge_bypass_acl: 57 | denied_ranges: 58 | - 59 | - 60 | allowed_ranges: 61 | - 62 | - domains: 63 | ... 64 | ``` 65 | 66 | A configuration for a single route can be as simple as: 67 | 68 | ```yaml 69 | --- 70 | routes: 71 | - domains: 72 | - "example.net" 73 | backend: 74 | address: "1.2.3.4:443" 75 | ``` 76 | 77 | Domain names can be a regular expression: 78 | 79 | ```yaml 80 | --- 81 | routes: 82 | - domains: 83 | # Matches example.net and all its subdomains. 84 | - "example.net" 85 | - "*.example.net" 86 | backend: 87 | address: "1.2.3.4:443" 88 | ``` 89 | 90 | ### Optional parameters 91 | 92 | _SNIProxy_ has a built-in ACL logic and can block and allow connections based on 93 | the client IP address. When at least one range is explicitly allowed, all other 94 | ranges are automatically denied (0.0.0.0/0 & ::/0). When an address can be found 95 | in two ranges, the most specific wins. If the exact same range is both allowed 96 | and denied, the deny rule wins. 97 | 98 | ```yaml 99 | --- 100 | routes: 101 | - domains: 102 | - "example.net" 103 | backend: 104 | address: "1.2.3.4:8080" 105 | denied_ranges: 106 | - "10.0.0.42/32" 107 | - domains: 108 | - "foo.example.com" 109 | backend: 110 | address: "5.6.7.8:443" 111 | denied_ranges: 112 | - "10.0.0.42/32" 113 | - "10.0.0.43/32" 114 | - "192.168.0.0/24" 115 | allowed_ranges: 116 | - "192.168.0.42/32" 117 | ``` 118 | 119 | _SNIProxy_ can use a different backend for ALPN requests: 120 | 121 | ```yaml 122 | --- 123 | routes: 124 | - domains: 125 | - "example.net" 126 | backend: 127 | address: "[1111::1]:8080" 128 | alpn_challenge_backend: 129 | address: "alpn-backend:8080" 130 | ``` 131 | 132 | The ACL rules can be bypassed for ALPN challenge requests: 133 | 134 | ```yaml 135 | --- 136 | routes: 137 | - domains: 138 | - "example.net" 139 | backend: 140 | address: "[1111::1]:8080" 141 | alpn_challenge_backend: 142 | address: "alpn-backend:8080" 143 | alpn_challenge_bypass_acl: true 144 | allowed_ranges: 145 | - "192.168.0.0/24" 146 | ``` 147 | 148 | [HAProxy PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) 149 | v1 and v2 are supported for backend connections: 150 | 151 | ```yaml 152 | --- 153 | routes: 154 | - domains: 155 | - "example.net" 156 | backend: 157 | address: "[1111::1]:8080" 158 | proxy_protocol: 2 159 | alpn_challenge_backend: 160 | address: "alpn-backend:8080" 161 | proxy_protocol: 1 162 | ``` 163 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp, fmt, fs, 3 | net::{IpAddr, SocketAddr, ToSocketAddrs}, 4 | path::PathBuf, 5 | }; 6 | 7 | use anyhow::{anyhow, bail, Result}; 8 | use ipnet::IpNet; 9 | use regex::RegexSet; 10 | use serde::{de, Deserialize}; 11 | use thiserror::Error; 12 | 13 | #[derive(Error, Debug)] 14 | pub(crate) enum Error { 15 | #[error("hostname not found")] 16 | HostnameNotFound, 17 | #[error("no backend")] 18 | NoBackend, 19 | #[error("access denied")] 20 | AccessDenied, 21 | } 22 | 23 | /// Main (internal) configuration. 24 | #[derive(Debug, Deserialize)] 25 | pub(crate) struct Config { 26 | /// TCP address & port to bind to for the TLS SNI proxy. Defaults to 27 | /// `[::]:443`. 28 | #[serde(default = "default_bind_https")] 29 | pub(crate) bind_https: SocketAddr, 30 | /// TCP address & port to bind to for the HTTP to HTTPS redirection. 31 | /// Defaults to `[::]:80`. 32 | #[serde(default = "default_bind_http")] 33 | pub(crate) bind_http: SocketAddr, 34 | /// List of routes. 35 | routes: Vec, 36 | } 37 | 38 | impl Config { 39 | /// Parses a file in YAML formatted str and converts it to a `Config` 40 | /// representation. 41 | pub(crate) fn from_str(input: &str) -> Result { 42 | let config: Self = serde_yaml::from_str(input)?; 43 | 44 | // Sanity check the configuration: 45 | for (i, route) in config.routes.iter().enumerate() { 46 | // Routes must have at least one of the backend types. 47 | if route.backend.is_none() && route.alpn_challenge_backend.is_none() { 48 | bail!("Route {i} has neither a backend nor an alpn_challenge_backend"); 49 | } 50 | 51 | let check_address = |address: &str| { 52 | // First check if the address is a valid ip:port one. 53 | if address.parse::().is_ok() { 54 | return Ok(()); 55 | } 56 | 57 | // If not, it should be an hostname:port one. Split once 58 | // starting from the left to catch invalid definitions (port 59 | // validation will fail if more than a single ':' is used). 60 | match address.split_once(':') { 61 | Some((addr, port)) => { 62 | if addr.is_empty() { 63 | bail!("{address} requires an address/hostname part"); 64 | } 65 | 66 | // Backend addresses must have an explicit & valid port. 67 | if let Err(e) = port.parse::() { 68 | bail!("{address} has an invalid port ({e})") 69 | } 70 | } 71 | None => bail!("No port found in {address}"), 72 | } 73 | Ok(()) 74 | }; 75 | if let Some(backend) = &route.backend { 76 | check_address(&backend.address)?; 77 | } 78 | if let Some(backend) = &route.alpn_challenge_backend { 79 | check_address(&backend.address)?; 80 | } 81 | } 82 | 83 | Ok(config) 84 | } 85 | 86 | /// Parses a file in YAML file and converts it to a `Config` representation. 87 | pub(crate) fn from_file(path: PathBuf) -> Result { 88 | Self::from_str(&fs::read_to_string(&path).map_err(|e| { 89 | anyhow!( 90 | "Could not read configuration file '{}': {e}", 91 | path.display() 92 | ) 93 | })?) 94 | } 95 | 96 | /// Returns a reference to a route matching the input domain, if any. 97 | pub(crate) fn get_route(&self, domain: &str) -> Option<&Route> { 98 | self.routes.iter().find(|r| r.domains.is_match(domain)) 99 | } 100 | 101 | /// Returns a reference to a backend matching the input domain, if any. 102 | pub(crate) fn get_backend( 103 | &self, 104 | hostname: &str, 105 | peer: &SocketAddr, 106 | is_challenge: bool, 107 | ) -> Result<&Backend> { 108 | // Get the corresponding route. 109 | let route = match self.get_route(hostname) { 110 | Some(route) => route, 111 | None => bail!(Error::HostnameNotFound), 112 | }; 113 | 114 | // Check ACLs (or opt-in bypass for ALPN challenges). 115 | if !is_challenge || !route.alpn_challenge_bypass_acl { 116 | // Check ACLs. 117 | if !route.is_allowed(peer) { 118 | bail!(Error::AccessDenied); 119 | } 120 | } 121 | 122 | // Get the right backend. 123 | let backend = match is_challenge { 124 | false => route.backend.as_ref(), 125 | true => route 126 | .alpn_challenge_backend 127 | .as_ref() 128 | .or(route.backend.as_ref()), 129 | } 130 | .ok_or_else(|| anyhow!(Error::NoBackend))?; 131 | 132 | Ok(backend) 133 | } 134 | 135 | /// Do we need an HTTP server. 136 | pub(crate) fn need_http(&self) -> bool { 137 | self.routes.iter().any(|r| r.http_redirect) 138 | } 139 | } 140 | 141 | /// Represents a single route between an SNI and a backend. 142 | #[derive(Debug, Deserialize)] 143 | pub(crate) struct Route { 144 | /// List of valid domains for this route (regexp). 145 | #[serde(deserialize_with = "deserialize_regex")] 146 | domains: RegexSet, 147 | /// Backend to proxy the connection to when the route is used. 148 | pub(crate) backend: Option, 149 | /// Backend to use if the request is an ALPN challenge. 150 | pub(crate) alpn_challenge_backend: Option, 151 | /// Bypass ACLs for ALPN challenges, if an ALPN challenge backend is used. 152 | #[serde(default)] 153 | pub(crate) alpn_challenge_bypass_acl: bool, 154 | /// Allow and deny ACLs, containing a list of IP ranges to allow or deny for 155 | /// this route. If 'allow' is used, all non-matching addresses are denied. 156 | /// A 'deny' rule wins over an 'allow' one and the most specific subnet 157 | /// takes precedence. 158 | /// 159 | /// Denied IP ranges. If a request matches, it'll be denied. 160 | /// 161 | /// For how allowed ranges compare to denied ones, see `allowed_ranges`. 162 | #[serde(default)] 163 | denied_ranges: Vec, 164 | /// Allowed IP ranges. When not used, non-denied requests will pass through, 165 | /// otherwise when at least one IP range is in the allowed ranges, all 166 | /// non-matching requests are denied. 167 | /// 168 | /// If an IP matches both a denied and an allowed IP range, the most 169 | /// specific range wins. Otherwise, the denied range wins over the allowed 170 | /// one. 171 | #[serde(default)] 172 | allowed_ranges: Vec, 173 | /// Redirect HTTP requests comming to `bind_http` and `bind_https` to their 174 | /// HTTPS counterparts (using the request Host header). 175 | #[serde(default = "default_true")] 176 | pub(crate) http_redirect: bool, 177 | } 178 | 179 | impl Route { 180 | /// Checks if a client IP address is allowed by the route ACLs. 181 | pub(crate) fn is_allowed(&self, addr: &SocketAddr) -> bool { 182 | if self.denied_ranges.is_empty() && self.allowed_ranges.is_empty() { 183 | return true; 184 | } 185 | 186 | // Get the client IP address, v4 or v6. 187 | let client_ip = addr.ip(); 188 | 189 | // Look for the minimum CIDR allowing our request, or None if not found. 190 | let mut cidr = None; 191 | self.allowed_ranges.iter().for_each(|r| { 192 | if Self::contains(r, &client_ip) { 193 | cidr = match cidr { 194 | Some(cidr) => Some(cmp::max(cidr, r.prefix_len())), 195 | None => Some(r.prefix_len()), 196 | }; 197 | } 198 | }); 199 | 200 | // If we didn't match an allowed range, while having no denied ranges 201 | // defined; deny the request. 202 | if cidr.is_none() && !self.allowed_ranges.is_empty() && self.denied_ranges.is_empty() { 203 | return false; 204 | } 205 | 206 | // Looks for denied range matches with a CIDR <= of what we found in the 207 | // allowed ranges (or 0 if no allowed range was defined). 208 | let cidr = cidr.unwrap_or(0); 209 | for r in self.denied_ranges.iter() { 210 | if Self::contains(r, &client_ip) && r.prefix_len() >= cidr { 211 | return false; 212 | } 213 | } 214 | 215 | true 216 | } 217 | 218 | /// Check if a subnet contains an IP address, including IPv4-mapped IPv6 219 | /// addresses in IPv4 subnets. 220 | fn contains(net: &IpNet, ip: &IpAddr) -> bool { 221 | if let (IpNet::V4(v4net), IpAddr::V6(v6)) = (net, ip) { 222 | if let Some(ref v4) = v6.to_ipv4_mapped() { 223 | return v4net.contains(v4); 224 | } 225 | } 226 | 227 | net.contains(ip) 228 | } 229 | } 230 | 231 | /// Represents a backend (host and its specific options). 232 | #[derive(Debug, Deserialize)] 233 | pub(crate) struct Backend { 234 | /// Backend address in the : form; can be either an IP 235 | /// address or an hostname. 236 | pub(crate) address: String, 237 | /// HAProxy PROXY protocol. Disable: None, v1: Some(1), v2: Some(2). 238 | pub(crate) proxy_protocol: Option, 239 | } 240 | 241 | impl Backend { 242 | pub(crate) fn to_socket_addr(&self) -> Result { 243 | self.address 244 | .to_socket_addrs()? 245 | .next() 246 | .ok_or_else(|| anyhow!("Could not convert {} to an IP:port pair", self.address)) 247 | } 248 | } 249 | 250 | /// Deserialize a set of custom regex expressions from a sequence of strings to 251 | /// a `RegexSet`. 252 | fn deserialize_regex<'a, D>(deserializer: D) -> Result 253 | where 254 | D: de::Deserializer<'a>, 255 | { 256 | struct RegexVisitor; 257 | 258 | impl<'a> de::Visitor<'a> for RegexVisitor { 259 | type Value = RegexSet; 260 | 261 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 262 | formatter.write_str("a string containing a regex") 263 | } 264 | 265 | fn visit_seq(self, mut seq: S) -> Result 266 | where 267 | S: de::SeqAccess<'a>, 268 | { 269 | let mut patterns = Vec::new(); 270 | 271 | while let Some(val) = seq.next_element::<&str>()? { 272 | // The following has to be done in the right order! 273 | let val = val.replace('.', r"\."); 274 | let val = val.replace('*', ".*"); 275 | 276 | patterns.push(format!("^{val}$")); 277 | } 278 | 279 | RegexSet::new(&patterns).map_err(de::Error::custom) 280 | } 281 | } 282 | 283 | deserializer.deserialize_seq(RegexVisitor) 284 | } 285 | 286 | // Default values. 287 | fn default_bind_https() -> SocketAddr { 288 | "[::]:443".parse().unwrap() 289 | } 290 | fn default_bind_http() -> SocketAddr { 291 | "[::]:80".parse().unwrap() 292 | } 293 | fn default_true() -> bool { 294 | true 295 | } 296 | 297 | #[cfg(test)] 298 | mod tests { 299 | use super::*; 300 | 301 | #[test] 302 | fn invalid_configs() { 303 | // At least one route should be defined. 304 | assert!(Config::from_str("").is_err()); 305 | 306 | // Config with a backend but no domain. 307 | assert!(Config::from_str( 308 | " 309 | routes: 310 | - backend: 311 | address: 127.0.0.1:443 312 | " 313 | ) 314 | .is_err()); 315 | 316 | // Configs with a malformed backend. 317 | assert!(Config::from_str( 318 | " 319 | routes: 320 | - domains: 321 | - example.net 322 | backend: 323 | address: 127.0.0.1 324 | " 325 | ) 326 | .is_err()); 327 | assert!(Config::from_str( 328 | " 329 | routes: 330 | - domains: 331 | - example.net 332 | backend: 333 | address: :443 334 | " 335 | ) 336 | .is_err()); 337 | assert!(Config::from_str( 338 | " 339 | routes: 340 | - domains: 341 | - example.net 342 | backend: 343 | address: foobar 344 | " 345 | ) 346 | .is_err()); 347 | assert!(Config::from_str( 348 | " 349 | routes: 350 | - domains: 351 | - example.net 352 | backend: 353 | address: foo:bar:443 354 | " 355 | ) 356 | .is_err()); 357 | 358 | // Config with a route but not backend. 359 | assert!(Config::from_str( 360 | " 361 | routes: 362 | - domains: 363 | - example.net 364 | " 365 | ) 366 | .is_err()); 367 | } 368 | 369 | #[test] 370 | fn simple_config() { 371 | let cfg = Config::from_str( 372 | " 373 | routes: 374 | - domains: 375 | - example.net 376 | backend: 377 | address: 127.0.0.1:443 378 | ", 379 | ) 380 | .unwrap(); 381 | assert_eq!(cfg.bind_https, "[::]:443".parse().unwrap()); 382 | assert_eq!(cfg.bind_http, "[::]:80".parse().unwrap()); 383 | assert_eq!(cfg.need_http(), true); 384 | 385 | // Invalid routes. 386 | assert!(cfg.get_route("").is_none()); 387 | assert!(cfg.get_route("foo.example.net").is_none()); 388 | assert!(cfg.get_route(".*example.*").is_none()); 389 | 390 | // The one valid route. 391 | let route = cfg.get_route("example.net").unwrap(); 392 | 393 | assert_eq!(route.backend.as_ref().unwrap().address, "127.0.0.1:443"); 394 | assert!(route.backend.as_ref().unwrap().proxy_protocol.is_none()); 395 | 396 | assert!(route.alpn_challenge_backend.is_none()); 397 | assert_eq!(route.alpn_challenge_bypass_acl, false); 398 | 399 | assert_eq!(route.is_allowed(&"10.0.42.1:12345".parse().unwrap()), true); 400 | assert_eq!(route.is_allowed(&"[1111::1]:12345".parse().unwrap()), true); 401 | 402 | // Config with an IPv6 backend. 403 | assert!(Config::from_str( 404 | " 405 | routes: 406 | - domains: 407 | - example.net 408 | backend: 409 | address: \"[::1]:443\" 410 | ", 411 | ) 412 | .is_ok()); 413 | 414 | // Config with an alpn challenge backend only. 415 | assert!(Config::from_str( 416 | " 417 | routes: 418 | - domains: 419 | - example.net 420 | alpn_challenge_backend: 421 | address: 127.0.0.1:443 422 | ", 423 | ) 424 | .is_ok()); 425 | 426 | // Config with an hostname as a backend. 427 | assert!(Config::from_str( 428 | " 429 | routes: 430 | - domains: 431 | - example.net 432 | alpn_challenge_backend: 433 | address: foo.example.net:443 434 | ", 435 | ) 436 | .is_ok()); 437 | } 438 | 439 | #[test] 440 | fn full_config() { 441 | let cfg = Config::from_str( 442 | " 443 | bind_https: \"[2222::42]:8433\" 444 | bind_http: 127.0.0.1:8080 445 | routes: 446 | - domains: 447 | - example.net 448 | backend: 449 | address: 127.0.0.1:443 450 | proxy_protocol: 2 451 | alpn_challenge_bypass_acl: true 452 | alpn_challenge_backend: 453 | address: 10.0.42.1:443 454 | proxy_protocol: 1 455 | denied_ranges: 456 | - 10.0.10.0/24 457 | - 10.0.1.1/32 458 | - 10.0.2.42/32 459 | allowed_ranges: 460 | - 10.0.10.128/29 461 | - 10.0.2.42/32 462 | - domains: 463 | - \"*.foo.example.com\" 464 | - foo.example.net 465 | http_redirect: false 466 | backend: 467 | address: \"[1234::42:1]:10443\" 468 | allowed_ranges: 469 | - 10.0.42.0/27 470 | ", 471 | ) 472 | .unwrap(); 473 | assert_eq!(cfg.bind_https, "[2222::42]:8433".parse().unwrap()); 474 | assert_eq!(cfg.bind_http, "127.0.0.1:8080".parse().unwrap()); 475 | assert_eq!(cfg.need_http(), true); 476 | 477 | // Invalid routes. 478 | assert!(cfg.get_route("").is_none()); 479 | assert!(cfg.get_route("foo.example.com").is_none()); 480 | assert!(cfg.get_route("extrafoo.example.com").is_none()); 481 | assert!(cfg.get_route("extrafoo.example.net").is_none()); 482 | 483 | // Test first route. 484 | let route = cfg.get_route("example.net").unwrap(); 485 | assert_eq!(route.backend.as_ref().unwrap().address, "127.0.0.1:443"); 486 | assert_eq!(route.backend.as_ref().unwrap().proxy_protocol, Some(2)); 487 | 488 | let alpn_backend = route.alpn_challenge_backend.as_ref().unwrap(); 489 | assert_eq!(alpn_backend.address, "10.0.42.1:443"); 490 | assert_eq!(alpn_backend.proxy_protocol, Some(1)); 491 | assert_eq!(route.alpn_challenge_bypass_acl, true); 492 | 493 | // First route ACLs. 494 | assert_eq!( 495 | route.is_allowed(&"10.0.10.127:12345".parse().unwrap()), 496 | false 497 | ); 498 | assert_eq!(route.is_allowed(&"10.0.1.1:10001".parse().unwrap()), false); 499 | assert_eq!(route.is_allowed(&"10.0.1.2:10001".parse().unwrap()), true); 500 | assert_eq!( 501 | route.is_allowed(&"[::ffff:10.0.1.1]:10001".parse().unwrap()), 502 | false 503 | ); 504 | assert_eq!( 505 | route.is_allowed(&"[::ffff:10.0.1.2]:10001".parse().unwrap()), 506 | true 507 | ); 508 | assert_eq!( 509 | route.is_allowed(&"10.0.10.132:12345".parse().unwrap()), 510 | true 511 | ); 512 | assert_eq!(route.is_allowed(&"172.16.99.1:1337".parse().unwrap()), true); 513 | assert_eq!( 514 | route.is_allowed(&"[4242::1:2:3:4:5:6]:11337".parse().unwrap()), 515 | true 516 | ); 517 | 518 | // Check deny wins over allow. 519 | assert_eq!(route.is_allowed(&"10.0.2.42:1337".parse().unwrap()), false); 520 | 521 | // We get the same route for the following matches. 522 | let tmp1 = cfg.get_route("0.foo.example.com").unwrap() as *const Route; 523 | let tmp2 = cfg.get_route("subdomain.foo.example.com").unwrap() as *const Route; 524 | let tmp3 = cfg.get_route("foo.example.net").unwrap() as *const Route; 525 | assert!(tmp1 == tmp2 && tmp2 == tmp3); 526 | 527 | // Test the second route. 528 | let route = cfg.get_route("a.b.c.d.foo.example.com").unwrap(); 529 | assert_eq!( 530 | route.backend.as_ref().unwrap().address, 531 | "[1234::42:1]:10443" 532 | ); 533 | assert!(route.backend.as_ref().unwrap().proxy_protocol.is_none()); 534 | assert_eq!(route.http_redirect, false); 535 | assert!(route.alpn_challenge_backend.is_none()); 536 | assert_eq!(route.alpn_challenge_bypass_acl, false); 537 | 538 | // Second route ACLs. 539 | assert_eq!( 540 | route.is_allowed(&"10.0.10.127:12345".parse().unwrap()), 541 | false 542 | ); 543 | assert_eq!( 544 | route.is_allowed(&"[1111::42:128]:8001".parse().unwrap()), 545 | false 546 | ); 547 | assert_eq!(route.is_allowed(&"10.0.42.0:10001".parse().unwrap()), true); 548 | assert_eq!(route.is_allowed(&"10.0.42.31:10001".parse().unwrap()), true); 549 | assert_eq!( 550 | route.is_allowed(&"10.0.42.32:10001".parse().unwrap()), 551 | false 552 | ); 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, future::Future, net::SocketAddr}; 2 | 3 | use anyhow::Result; 4 | use tokio::task::futures::TaskLocalFuture; 5 | 6 | tokio::task_local! { 7 | pub(crate) static REQ_CONTEXT: RefCell; 8 | } 9 | 10 | /// Request context, embedding per-request information in tokio tasks. 11 | pub(crate) struct ReqContext { 12 | /// Local IP & port. 13 | pub(crate) local: SocketAddr, 14 | /// Peer IP & port. 15 | pub(crate) peer: SocketAddr, 16 | /// Hostname requested. Can be None early in the processing. 17 | pub(crate) hostname: Option, 18 | } 19 | 20 | impl ReqContext { 21 | /// Initialize a new request context given local & peer information. 22 | pub(crate) fn from(local: SocketAddr, peer: SocketAddr) -> RefCell { 23 | RefCell::new(Self { 24 | local, 25 | peer, 26 | hostname: None, 27 | }) 28 | } 29 | 30 | /// Set the context hostname. 31 | fn set_hostname(&mut self, hostname: String) { 32 | self.hostname = Some(hostname) 33 | } 34 | } 35 | 36 | /// Run a future with a request context. 37 | pub(crate) fn with_req_context( 38 | context: RefCell, 39 | f: F, 40 | ) -> TaskLocalFuture, F> { 41 | REQ_CONTEXT.scope(context, f) 42 | } 43 | 44 | /// Set the current context hostname. Can fail if not context is defined. 45 | pub(crate) fn set_hostname(hostname: &str) -> Result<()> { 46 | REQ_CONTEXT.try_with(|context| context.borrow_mut().set_hostname(hostname.to_string()))?; 47 | Ok(()) 48 | } 49 | 50 | /// Returns the local address associated with the context. 51 | pub(crate) fn local_addr() -> Result { 52 | Ok(REQ_CONTEXT.try_with(|context| -> SocketAddr { 53 | let context = context.borrow(); 54 | context.local 55 | })?) 56 | } 57 | 58 | /// Returns the peer address associated with the context. 59 | pub(crate) fn peer_addr() -> Result { 60 | Ok(REQ_CONTEXT.try_with(|context| -> SocketAddr { 61 | let context = context.borrow(); 62 | context.peer 63 | })?) 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn context() { 72 | let local = "[1111:1::42]:10443".parse().unwrap(); 73 | let peer = "[1337:42::700]:12345".parse().unwrap(); 74 | 75 | with_req_context(ReqContext::from(local, peer), async { 76 | REQ_CONTEXT.with(|context| { 77 | let context = context.borrow(); 78 | assert_eq!(context.local, "[1111:1::42]:10443".parse().unwrap()); 79 | assert_eq!(context.peer, "[1337:42::700]:12345".parse().unwrap()); 80 | assert_eq!(context.hostname, None); 81 | }); 82 | assert!(set_hostname("example.net").is_ok()); 83 | REQ_CONTEXT.with(|context| { 84 | let context = context.borrow(); 85 | assert_eq!(context.hostname, Some("example.net".to_string())); 86 | }); 87 | }); 88 | 89 | assert!(REQ_CONTEXT.try_with(|_| {}).is_err()); 90 | assert!(set_hostname("example.net").is_err()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | net::SocketAddr, 4 | str, 5 | }; 6 | 7 | use anyhow::{bail, Result}; 8 | use log::info; 9 | 10 | use crate::{config::Config, reader::ReaderBuf}; 11 | 12 | /// Check if the provided buffer looks like an HTTP request. This does not guarantee the request is 13 | /// a genuine one, but should be enough to at least try handling it. 14 | pub(crate) fn is_http(rb: &ReaderBuf) -> bool { 15 | // The buffer comes from previous reads, we might have failed to read up to 5 bytes. No reason 16 | // try try reading it again. In all other cases 5 bytes is the maximal length we can match all 17 | // HTTP methods on. This is a shortcut but as we don't try to be 100% correct here, this is 18 | // fine. 19 | let buf = rb.buf(); 20 | if buf.len() >= 5 { 21 | // From https://developer.mozilla.org/fr/docs/Web/HTTP/Methods 22 | if let Ok( 23 | "GET /" | "HEAD " | "POST " | "PUT /" | "DELET" | "CONNE" | "OPTIO" | "TRACE" | "PATCH", 24 | ) = str::from_utf8(&buf[..5]) 25 | { 26 | return true; 27 | } 28 | } 29 | false 30 | } 31 | 32 | /// Redirect an HTTP request with a 308. 33 | pub(crate) fn try_redirect( 34 | config: &Config, 35 | client: &SocketAddr, 36 | rb: &mut ReaderBuf, 37 | ) -> Result<()> { 38 | let host = match get_host(rb)? { 39 | Some(host) => host, 40 | None => return Ok(()), // Not much we can do, not really an error. 41 | }; 42 | #[cfg(not(test))] 43 | crate::context::set_hostname(host)?; 44 | 45 | match config.get_route(host) { 46 | Some(route) => { 47 | if !route.http_redirect { 48 | bail!("Denied HTTP redirect to HTTPS (disabled)"); 49 | } 50 | 51 | if !route.is_allowed(client) { 52 | bail!("Denied HTTP redirect to HTTPS (ACLs)"); 53 | } 54 | } 55 | None => bail!("Unknown hostname ({host})"), // Not much we can do, not really an error. 56 | } 57 | 58 | info!("Redirecting HTTP request to HTTPS"); 59 | let response = format!( 60 | "HTTP/1.0 308 Unknown\r\nLocation: https://{host}:{}\r\n\r\n", 61 | config.bind_https.port() 62 | ); 63 | Ok(rb.get_mut().write_all(response.as_bytes())?) 64 | } 65 | 66 | /// Try parsing an HTTP request host. 67 | fn get_host(rb: &mut ReaderBuf) -> Result> { 68 | // Try to read the remaining of the HTTP headers. 8KB is the limit size on 69 | // many web servers. 70 | rb.read(8192)?; 71 | let headers = str::from_utf8(rb.buf())?; 72 | 73 | // Skip the request line and loop over the HTTP headers to find the Host 74 | // one and extract its value. 75 | for hdr in headers.split("\r\n").skip(1) { 76 | match hdr.split_once(':') { 77 | Some(("Host", val)) => { 78 | let val = val.trim(); 79 | // Host could be in the : form. 80 | return Ok(Some(match val.split_once(':') { 81 | Some((host, _)) => host, 82 | _ => val, 83 | })); 84 | } 85 | _ => continue, 86 | } 87 | } 88 | 89 | info!("Could not find HTTP Host header in request"); 90 | Ok(None) 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use std::io::Cursor; 96 | 97 | use crate::{config::Config, reader::ReaderBuf as B, tls::tests::RECORD_SNI_ALPN}; 98 | 99 | #[test] 100 | fn is_http() { 101 | let valid: &[&str] = &[ 102 | "GET /", "HEAD ", "POST ", "PUT /", "DELET", "CONNE", "OPTIO", "TRACE", "PATCH", 103 | ]; 104 | for s in valid.iter() { 105 | let mut rb = &mut B::from_bytes(s.as_bytes()); 106 | rb.read(5).unwrap(); 107 | assert!(super::is_http(&mut rb)); 108 | } 109 | 110 | let mut rb = B::from_bytes(&[]); 111 | rb.read(5).unwrap(); 112 | assert_eq!(super::is_http(&mut rb), false); 113 | 114 | let mut rb = B::from_bytes(&[0; 256]); 115 | rb.read(5).unwrap(); 116 | assert_eq!(super::is_http(&mut rb), false); 117 | } 118 | 119 | #[test] 120 | fn get_host() { 121 | assert_eq!(super::get_host(&mut B::from_bytes(&[])).unwrap(), None); 122 | assert_eq!(super::get_host(&mut B::from_bytes(b"GET /")).unwrap(), None); 123 | assert_eq!( 124 | super::get_host(&mut B::from_bytes(b"GET /\r\nHost: example.net\r\n")).unwrap(), 125 | Some("example.net") 126 | ); 127 | assert_eq!( 128 | super::get_host(&mut B::from_bytes( 129 | b"GET /\r\nHost: foo.example.net:8080\r\n" 130 | )) 131 | .unwrap(), 132 | Some("foo.example.net") 133 | ); 134 | assert_eq!( 135 | super::get_host(&mut B::from_bytes( 136 | b"GET /\r\nScheme: https\r\nHost: example.net\r\nFilename: foo\r\n" 137 | )) 138 | .unwrap(), 139 | Some("example.net") 140 | ); 141 | 142 | // Invalid UTF-8 sequence. 143 | assert!(super::get_host(&mut B::from_bytes(RECORD_SNI_ALPN)).is_err()); 144 | } 145 | 146 | #[test] 147 | fn try_redirect() { 148 | let config = Config::from_str( 149 | " 150 | routes: 151 | - domains: 152 | - example.net 153 | backend: 154 | address: 127.0.0.1:8000 155 | - domains: 156 | - denied.example.net 157 | http_redirect: false 158 | backend: 159 | address: 127.0.0.1:8000 160 | - domains: 161 | - acls.example.net 162 | backend: 163 | address: 127.0.0.1:8000 164 | allowed_ranges: 165 | - 10.0.0.0/8 166 | denied_ranges: 167 | - 127.0.0.0/24 168 | ", 169 | ) 170 | .unwrap(); 171 | 172 | let req = "GET /\r\nHost: example.net\r\n".as_bytes(); 173 | let mut rb = B::new(Cursor::new(req.to_vec())); 174 | assert!(super::try_redirect(&config, &"127.0.0.1:10000".parse().unwrap(), &mut rb).is_ok()); 175 | 176 | let buf = rb.into_inner().into_inner(); 177 | assert_eq!( 178 | &buf[req.len()..], 179 | b"HTTP/1.0 308 Unknown\r\nLocation: https://example.net:443\r\n\r\n" 180 | ); 181 | 182 | let req = "GET /\r\n".as_bytes(); 183 | let mut rb = B::new(Cursor::new(req.to_vec())); 184 | assert!(super::try_redirect(&config, &"127.0.0.1:10000".parse().unwrap(), &mut rb).is_ok()); 185 | 186 | let req = "GET /\r\nHost: denied.example.net\r\n".as_bytes(); 187 | let mut rb = B::new(Cursor::new(req.to_vec())); 188 | assert!( 189 | super::try_redirect(&config, &"127.0.0.1:10000".parse().unwrap(), &mut rb).is_err() 190 | ); 191 | 192 | let req = "GET /\r\nHost: foo.example.net\r\n".as_bytes(); 193 | let mut rb = B::new(Cursor::new(req.to_vec())); 194 | assert!( 195 | super::try_redirect(&config, &"127.0.0.1:10000".parse().unwrap(), &mut rb).is_err() 196 | ); 197 | 198 | let req = "GET /\r\nHost: acls.example.net\r\n".as_bytes(); 199 | let mut rb = B::new(Cursor::new(req.to_vec())); 200 | assert!( 201 | super::try_redirect(&config, &"127.0.0.1:10000".parse().unwrap(), &mut rb).is_err() 202 | ); 203 | 204 | let req = "GET /\r\nHost: acls.example.net\r\n".as_bytes(); 205 | let mut rb = B::new(Cursor::new(req.to_vec())); 206 | assert!(super::try_redirect(&config, &"10.0.42.1:10000".parse().unwrap(), &mut rb).is_ok()); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Write, sync::Mutex}; 2 | 3 | use anyhow::Result; 4 | use log::{LevelFilter, Metadata, Record}; 5 | use termcolor::{BufferedStandardStream, Color, ColorChoice, ColorSpec, WriteColor}; 6 | use time::{macros::format_description, OffsetDateTime}; 7 | 8 | use crate::context::REQ_CONTEXT; 9 | 10 | /// Our own simple logger. 11 | pub(crate) struct Logger { 12 | /// Max level the logger will output. 13 | max_level: LevelFilter, 14 | /// We can output messages to stdout and stderr depending on their severity. 15 | stdout: Mutex, 16 | stderr: Mutex, 17 | } 18 | 19 | impl Logger { 20 | pub(crate) fn init(max_level: LevelFilter) -> Result<()> { 21 | log::set_max_level(max_level); 22 | log::set_boxed_logger(Box::new(Self { 23 | max_level, 24 | stdout: Mutex::new(BufferedStandardStream::stdout(ColorChoice::Auto)), 25 | stderr: Mutex::new(BufferedStandardStream::stderr(ColorChoice::Auto)), 26 | }))?; 27 | Ok(()) 28 | } 29 | 30 | fn try_log(&self, out: &mut BufferedStandardStream, record: &Record) -> Result<()> { 31 | static LEVEL_COLORS: &[Option] = &[ 32 | None, // Default. 33 | Some(Color::Red), // Error. 34 | Some(Color::Yellow), // Warn. 35 | Some(Color::Blue), // Info. 36 | Some(Color::Cyan), // Debug. 37 | Some(Color::White), // Trace. 38 | ]; 39 | 40 | // If the log level allows debug! and or trace! messages, show time 41 | // time. 42 | if self.max_level >= LevelFilter::Debug { 43 | OffsetDateTime::now_utc().format_into( 44 | out, 45 | format_description!("[hour]:[minute]:[second]:[subsecond digits:6] "), 46 | )?; 47 | } 48 | 49 | // If we have a request context, use it. Silence access errors, the 50 | // context is not mandatory. 51 | if let Ok(ret) = REQ_CONTEXT.try_with(|context| -> Result<()> { 52 | let context = context.borrow(); 53 | write!(out, "{}>{} ", context.peer, context.local)?; 54 | if let Some(hostname) = &context.hostname { 55 | write!(out, "({hostname}) ")?; 56 | } 57 | Ok(()) 58 | }) { 59 | ret?; 60 | } 61 | 62 | // Show the level for error! and warn! messages, or if the max level 63 | // includes debug!. 64 | if record.level() <= LevelFilter::Warn || self.max_level >= LevelFilter::Debug { 65 | out.set_color(ColorSpec::new().set_fg(LEVEL_COLORS[record.level() as usize]))?; 66 | write!(out, "{:5} ", record.level())?; 67 | out.reset()?; 68 | } 69 | 70 | // Finally write the log message and flush it. 71 | writeln!(out, "{}", record.args())?; 72 | out.flush()?; 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | impl log::Log for Logger { 79 | fn enabled(&self, _metadata: &Metadata) -> bool { 80 | true 81 | } 82 | 83 | fn log(&self, record: &Record) { 84 | if !self.enabled(record.metadata()) { 85 | return; 86 | } 87 | 88 | // Select the output based on the log level. 89 | let mut out = match record.level() { 90 | level if level == LevelFilter::Error => self.stderr.lock().unwrap(), 91 | _ => self.stdout.lock().unwrap(), 92 | }; 93 | 94 | // Not much we can do to report the error. 95 | let _ = self.try_log(&mut out, record); 96 | } 97 | 98 | fn flush(&self) { 99 | // Not much we can do to report the errors. 100 | let _ = self.stdout.lock().unwrap().flush(); 101 | let _ = self.stderr.lock().unwrap().flush(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc, thread}; 2 | 3 | use anyhow::{bail, Result}; 4 | use clap::{builder::PossibleValuesParser, Parser}; 5 | use log::{error, LevelFilter}; 6 | use once_cell::sync::OnceCell; 7 | use tokio::runtime; 8 | 9 | mod config; 10 | mod context; 11 | mod http; 12 | mod logger; 13 | mod proxy_protocol; 14 | mod reader; 15 | mod tcp; 16 | mod tls; 17 | mod zc; 18 | 19 | use crate::{config::Config, logger::Logger}; 20 | 21 | #[derive(Parser)] 22 | #[command(author, version, about, long_about = None)] 23 | struct Args { 24 | #[arg( 25 | long, 26 | value_parser=PossibleValuesParser::new(["error", "warn", "info", "debug", "trace"]), 27 | default_value = "info", 28 | help = "Log level", 29 | )] 30 | pub(crate) log_level: String, 31 | #[arg( 32 | short, 33 | long, 34 | default_value = "sniproxy.yaml", 35 | help = "Path to the configuration file" 36 | )] 37 | config: PathBuf, 38 | } 39 | 40 | fn main() -> Result<()> { 41 | // Start by parsing the cli arguments. 42 | let args = Args::parse(); 43 | 44 | // Set the log level. 45 | let log_level = match args.log_level.as_str() { 46 | "error" => LevelFilter::Error, 47 | "warn" => LevelFilter::Warn, 48 | "info" => LevelFilter::Info, 49 | "debug" => LevelFilter::Debug, 50 | "trace" => LevelFilter::Trace, 51 | x => bail!("Invalid log_level: {}", x), 52 | }; 53 | Logger::init(log_level)?; 54 | 55 | // Parse the configuration file. 56 | let config = Arc::new(Config::from_file(args.config)?); 57 | 58 | let mut threads = Vec::new(); 59 | 60 | // Start the TLS listener and handle incoming connections. 61 | let _config = Arc::clone(&config); 62 | let bind = config.bind_https; 63 | threads.push(( 64 | "HTTPS", 65 | thread::spawn(move || { 66 | if let Err(e) = tcp::listen_and_proxy(_config, bind, tcp::tls::handle_stream) { 67 | error!("HTTPS listener returned: {e}"); 68 | } 69 | }), 70 | )); 71 | 72 | // Start the HTTP listener and handle incoming connections, if needed. 73 | if config.need_http() { 74 | let _config = Arc::clone(&config); 75 | let bind = config.bind_http; 76 | threads.push(( 77 | "HTTP", 78 | thread::spawn(move || { 79 | if let Err(e) = tcp::listen_and_proxy(_config, bind, tcp::http::handle_stream) { 80 | error!("HTTP listener returned: {e}"); 81 | } 82 | }), 83 | )); 84 | } 85 | 86 | // Wait for threads to join. 87 | threads.drain(..).for_each(|(name, t)| { 88 | if t.join().is_err() { 89 | error!("{name} server thread returned unexpectedly"); 90 | } 91 | }); 92 | 93 | Ok(()) 94 | } 95 | 96 | static RUNTIME: OnceCell = OnceCell::new(); 97 | 98 | #[macro_export] 99 | macro_rules! runtime { 100 | () => { 101 | RUNTIME.get_or_try_init(|| runtime::Builder::new_multi_thread().enable_io().build()) 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/proxy_protocol.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Write, 3 | net::{IpAddr, SocketAddr}, 4 | }; 5 | 6 | use anyhow::{bail, Result}; 7 | 8 | /// Writes a `version` HAProxy protocol header. 9 | pub(crate) fn write_header( 10 | w: W, 11 | version: u8, 12 | local: &SocketAddr, 13 | peer: &SocketAddr, 14 | ) -> Result<()> 15 | where 16 | W: Write, 17 | { 18 | match version { 19 | 1 => write_header_v1(w, local, peer), 20 | 2 => write_header_v2(w, local, peer), 21 | x => bail!("Invalid HAProxy protocol header version ({x})"), 22 | } 23 | } 24 | 25 | /// Write an HAProxy protocol header version 1 in a writer. 26 | /// See https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt 27 | fn write_header_v1(mut w: W, local: &SocketAddr, peer: &SocketAddr) -> Result<()> 28 | where 29 | W: Write, 30 | { 31 | Ok(write!( 32 | w, 33 | "PROXY {} {} {} {} {}\r\n", 34 | match (local, peer) { 35 | (SocketAddr::V4(_), SocketAddr::V4(_)) => "TCP4", 36 | (SocketAddr::V6(_), SocketAddr::V6(_)) => "TCP6", 37 | _ => bail!("Invalid address types (mixing IPv4 and IPv6)"), 38 | }, 39 | peer.ip(), 40 | local.ip(), 41 | peer.port(), 42 | local.port() 43 | )?) 44 | } 45 | 46 | /// Write an HAProxy protocol header version 2 in a writer. 47 | /// See https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt 48 | fn write_header_v2(mut w: W, local: &SocketAddr, peer: &SocketAddr) -> Result<()> 49 | where 50 | W: Write, 51 | { 52 | // Protocol signature and the command (\x2 followed by \x0 for 'local' or 53 | // \x1 for 'proxy'). 54 | w.write_all(&[ 55 | 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a, 0x21, 56 | ])?; 57 | 58 | // Transport protocol and address family. The highest 4 bits represent 59 | // the address family (\x1: AF_INET, \x2: AF_INET6) and the lowest 4 60 | // bits the protocol (\x1: SOCK_STREAM). 61 | // 62 | // Followed by the address length. 12 for IPv4 and 36 for IPv6. 63 | match (local, peer) { 64 | (SocketAddr::V4(_), SocketAddr::V4(_)) => { 65 | w.write_all(&[0x11])?; 66 | w.write_all(&u16::to_be_bytes(12))?; 67 | } 68 | (SocketAddr::V6(_), SocketAddr::V6(_)) => { 69 | w.write_all(&[0x21])?; 70 | w.write_all(&u16::to_be_bytes(36))?; 71 | } 72 | _ => bail!("Invalid address types (mixing IPv4 and IPv6)"), 73 | } 74 | 75 | // Now write addresses & ports information. 76 | match peer.ip() { 77 | IpAddr::V4(addr) => w.write_all(&addr.octets())?, 78 | IpAddr::V6(addr) => w.write_all(&addr.octets())?, 79 | } 80 | match local.ip() { 81 | IpAddr::V4(addr) => w.write_all(&addr.octets())?, 82 | IpAddr::V6(addr) => w.write_all(&addr.octets())?, 83 | } 84 | w.write_all(&u16::to_be_bytes(peer.port()))?; 85 | w.write_all(&u16::to_be_bytes(local.port()))?; 86 | 87 | Ok(()) 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use std::str; 93 | 94 | #[test] 95 | fn header_v1() { 96 | let local = "172.16.99.1:443".parse().unwrap(); 97 | let peer = "10.0.42.132:1337".parse().unwrap(); 98 | let mut w = Vec::new(); 99 | assert!(super::write_header_v1(&mut w, &local, &peer).is_ok()); 100 | assert_eq!( 101 | str::from_utf8(&w).unwrap(), 102 | "PROXY TCP4 10.0.42.132 172.16.99.1 1337 443\r\n" 103 | ); 104 | 105 | let local = "[1111:1::42]:10443".parse().unwrap(); 106 | let peer = "[1337:42::700]:12345".parse().unwrap(); 107 | let mut w = Vec::new(); 108 | assert!(super::write_header_v1(&mut w, &local, &peer).is_ok()); 109 | assert_eq!( 110 | str::from_utf8(&w).unwrap(), 111 | "PROXY TCP6 1337:42::700 1111:1::42 12345 10443\r\n" 112 | ); 113 | 114 | let local = "172.16.99.1:443".parse().unwrap(); 115 | let peer = "[1337:42::700]:12345".parse().unwrap(); 116 | let mut w = Vec::new(); 117 | assert!(super::write_header_v1(&mut w, &local, &peer).is_err()); 118 | 119 | let local = "[1111:1::42]:10443".parse().unwrap(); 120 | let peer = "10.0.42.132:1337".parse().unwrap(); 121 | let mut w = Vec::new(); 122 | assert!(super::write_header_v1(&mut w, &local, &peer).is_err()); 123 | } 124 | 125 | #[test] 126 | fn header_v2() { 127 | let local = "172.16.99.1:443".parse().unwrap(); 128 | let peer = "10.0.42.132:1337".parse().unwrap(); 129 | let mut w = Vec::new(); 130 | assert!(super::write_header_v2(&mut w, &local, &peer).is_ok()); 131 | assert_eq!( 132 | &w, 133 | &[ 134 | 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a, 0x21, 0x11, 135 | 0x00, 0x0c, 0x0a, 0x00, 0x2a, 0x84, 0xac, 0x10, 0x63, 0x01, 0x05, 0x39, 0x01, 0xbb 136 | ] 137 | ); 138 | 139 | let local = "[1111:1::42]:10443".parse().unwrap(); 140 | let peer = "[1337:42::700]:12345".parse().unwrap(); 141 | let mut w = Vec::new(); 142 | assert!(super::write_header_v2(&mut w, &local, &peer).is_ok()); 143 | assert_eq!( 144 | &w, 145 | &[ 146 | 0xd, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a, 0x21, 0x21, 147 | 0x00, 0x24, 0x13, 0x37, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 148 | 0x00, 0x00, 0x07, 0x00, 0x11, 0x11, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 149 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x30, 0x39, 0x28, 0xcb, 150 | ] 151 | ); 152 | 153 | let local = "172.16.99.1:443".parse().unwrap(); 154 | let peer = "[1337:42::700]:12345".parse().unwrap(); 155 | let mut w = Vec::new(); 156 | assert!(super::write_header_v2(&mut w, &local, &peer).is_err()); 157 | 158 | let local = "[1111:1::42]:10443".parse().unwrap(); 159 | let peer = "10.0.42.132:1337".parse().unwrap(); 160 | let mut w = Vec::new(); 161 | assert!(super::write_header_v2(&mut w, &local, &peer).is_err()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/reader.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp, io::Read, mem}; 2 | 3 | use anyhow::{bail, Result}; 4 | 5 | /// Fast buffer reader never removing read data from its internal buffer. It 6 | /// does not offer traditional accessors from io::Read and instead returns 7 | /// structured data references to its inner buffer, saving a copy. 8 | pub(crate) struct ReaderBuf { 9 | /// Inner reader, implementing io::Read. 10 | inner: R, 11 | /// Inner buffer, holding the data (both already read + buffered). 12 | buffer: Vec, 13 | /// Cursor keeping track of already read data in the buffer. 14 | cursor: usize, 15 | /// Minimum length to read from the inner reader when filling the inner 16 | /// buffer. Low values might impact performances when the data is not 17 | /// already mapped into memory. 18 | min_read: usize, 19 | } 20 | 21 | impl ReaderBuf { 22 | /// Create a new ReaderBuf with default values. 23 | /// 24 | /// Warning: min_read is initialized to 0. 25 | #[allow(dead_code)] 26 | pub(crate) fn new(inner: R) -> Self { 27 | Self { 28 | inner, 29 | buffer: Vec::new(), 30 | cursor: 0, 31 | min_read: 0, 32 | } 33 | } 34 | 35 | /// Create a new ReaderBuf with default values and with an explicit inner 36 | /// buffer capacity. 37 | /// 38 | /// Warning: min_read is initialized to 0. 39 | #[allow(dead_code)] 40 | pub(crate) fn with_capacity(capacity: usize, inner: R) -> Self { 41 | Self { 42 | inner, 43 | buffer: Vec::with_capacity(capacity), 44 | cursor: 0, 45 | min_read: 0, 46 | } 47 | } 48 | 49 | /// Unwraps the inner reader. 50 | pub(crate) fn into_inner(self) -> R { 51 | self.inner 52 | } 53 | 54 | /// Get a reference to the inner reader. 55 | #[allow(dead_code)] 56 | pub(crate) fn get_ref(&self) -> &R { 57 | &self.inner 58 | } 59 | 60 | /// Get a mutable reference to the inner reader. 61 | pub(crate) fn get_mut(&mut self) -> &mut R { 62 | &mut self.inner 63 | } 64 | 65 | /// Set the minimum length to read from the inner reader when filling the 66 | /// inner buffer (min_read). 67 | pub(crate) fn set_min_read(&mut self, len: usize) { 68 | self.min_read = len; 69 | } 70 | 71 | /// Returns a reference to the start of the inner buffer. 72 | pub(crate) fn buf(&self) -> &[u8] { 73 | &self.buffer 74 | } 75 | 76 | /// Read at most `len` bytes (advancing the inner cursor and filling the 77 | /// inner buffer if needed) and returns a pointer to a byte array to access 78 | /// the data read). 79 | pub(crate) fn read(&mut self, len: usize) -> Result<&[u8]> { 80 | let diff = len.saturating_sub(self.headlen()); 81 | let len = match diff { 82 | x if x > 0 => { 83 | let read = self.fill_buffer(diff)?; 84 | // read could be > diff, if diff < self.min_read. 85 | len - diff.saturating_sub(read) 86 | } 87 | // diff == 0. 88 | _ => len, 89 | }; 90 | 91 | let ptr = &self.buffer[self.cursor..(self.cursor + len)]; 92 | self.cursor += len; 93 | 94 | Ok(ptr) 95 | } 96 | 97 | /// Read `len` bytes (advancing the inner cursor and filling the inner 98 | /// buffer if needed) and returns a pointer to a byte array to access the 99 | /// data read. 100 | /// 101 | /// Returns an error if not enough data could be read. 102 | pub(crate) fn read_exact(&mut self, len: usize) -> Result<&[u8]> { 103 | let diff = len.saturating_sub(self.headlen()); 104 | if diff > 0 { 105 | self.fill_buffer_exact(diff)?; 106 | } 107 | 108 | let ptr = &self.buffer[self.cursor..(self.cursor + len)]; 109 | self.cursor += len; 110 | 111 | Ok(ptr) 112 | } 113 | 114 | /// Read `mem::size_of::()` bytes (advancing the inner cursor and filling 115 | /// the inner buffer if needed) and returns a pointer to a structured type 116 | /// `T` to access the data read. 117 | /// 118 | /// Returns an error if not enough data could be read. 119 | pub(crate) fn read_as(&mut self) -> Result<&T> { 120 | let diff = mem::size_of::().saturating_sub(self.headlen()); 121 | if diff > 0 { 122 | self.fill_buffer_exact(diff)?; 123 | } 124 | 125 | let ptr: &T = unsafe { mem::transmute(&self.buffer[self.cursor]) }; 126 | self.cursor += mem::size_of::(); 127 | 128 | Ok(ptr) 129 | } 130 | 131 | /// Length of the inner buffer, aka. read + unread data. 132 | pub(crate) fn len(&self) -> usize { 133 | self.buffer.len() 134 | } 135 | 136 | /// Length of the head in the inner buffer, aka. unread data. 137 | fn headlen(&self) -> usize { 138 | self.len() - self.cursor 139 | } 140 | 141 | /// Read at least `requested_len` bytes to the internal buffer an return how 142 | /// many bytes were read. 143 | fn fill_buffer(&mut self, requested_len: usize) -> Result { 144 | // Compute how much we'd like to read. 145 | let len = cmp::max(requested_len, self.min_read); 146 | let end = self.len(); 147 | 148 | // Resize the internal buffer to accept the additional data. 149 | self.buffer.resize(self.buffer.len() + len, 0); 150 | 151 | // Try to read `len` bytes. 152 | let read = match self.inner.read(&mut self.buffer[end..]) { 153 | Ok(read) => read, 154 | Err(e) => { 155 | match e.kind() { 156 | // Special case if the read would block. This means we can't 157 | // read data right now, so just report that. 158 | std::io::ErrorKind::WouldBlock => return Ok(0), 159 | _ => return Err(e.into()), 160 | } 161 | } 162 | }; 163 | 164 | // If we read less than requested, truncate the internal buffer. This is 165 | // mandatory as the previous `resize()` call also modified the buffer 166 | // length. 167 | let diff = len.saturating_sub(read); 168 | if diff > 0 { 169 | self.buffer.truncate(self.buffer.len() - diff); 170 | } 171 | 172 | Ok(read) 173 | } 174 | 175 | /// Read `requested_len` bytes to the internal buffer an return an error if 176 | /// the underlying reader couldn't provide the requested read lenght. 177 | fn fill_buffer_exact(&mut self, len: usize) -> Result<()> { 178 | let read = self.fill_buffer(len)?; 179 | if read < len { 180 | bail!("Could not read enough data from the inner reader"); 181 | } 182 | Ok(()) 183 | } 184 | } 185 | 186 | impl Clone for ReaderBuf { 187 | fn clone(&self) -> Self { 188 | Self { 189 | inner: self.inner.clone(), 190 | buffer: self.buffer.clone(), 191 | cursor: self.cursor, 192 | min_read: self.min_read, 193 | } 194 | } 195 | } 196 | 197 | #[cfg(test)] 198 | impl<'a> ReaderBuf<&'a [u8]> { 199 | /// Creates a new ReaderBuf from a byte array, for testing purposes. 200 | pub(crate) fn from_bytes(bytes: &'a [u8]) -> Self { 201 | Self { 202 | inner: bytes, 203 | buffer: Vec::new(), 204 | cursor: 0, 205 | min_read: 0, 206 | } 207 | } 208 | } 209 | 210 | #[cfg(test)] 211 | mod tests { 212 | use super::ReaderBuf as B; 213 | 214 | #[test] 215 | fn reader() { 216 | let data: Vec = (1..=30).collect(); 217 | let mut rb = B::from_bytes(&data); 218 | 219 | // We haven't read anything yet. 220 | assert_eq!(rb.buf(), &[] as &[u8]); 221 | 222 | // Reading 3 bytes using read_exact. 223 | assert_eq!(rb.read_exact(3).unwrap(), &(1..=3).collect::>()); 224 | assert_eq!(rb.buf(), &(1..=3).collect::>()); 225 | 226 | // Reading 7 bytes using read. 227 | assert_eq!(rb.read(7).unwrap(), &(4..=10).collect::>()); 228 | assert_eq!(rb.buf(), &(1..=10).collect::>()); 229 | 230 | // Setting min read. 231 | rb.set_min_read(12); 232 | assert_eq!(rb.read(5).unwrap(), &(11..=15).collect::>()); 233 | assert_eq!(rb.buf(), &(1..=22).collect::>()); 234 | 235 | // Read should still be within the buffered data. 236 | assert_eq!(rb.read(7).unwrap(), &(16..=22).collect::>()); 237 | assert_eq!(rb.buf(), &(1..=22).collect::>()); 238 | 239 | // Trying to read more than the available data (7 bytes left). 240 | assert_eq!(rb.read(10).unwrap(), &(23..=30).collect::>()); 241 | assert_eq!(rb.buf(), &(1..=30).collect::>()); 242 | 243 | // No data left. 244 | assert_eq!(rb.read(1).unwrap(), &[] as &[u8]); 245 | assert!(rb.read_exact(1).is_err()); 246 | } 247 | 248 | #[test] 249 | fn exact_read_and_min_sz() { 250 | let data: Vec = (1..=5).collect(); 251 | let mut rb = B::from_bytes(&data); 252 | rb.set_min_read(12); 253 | 254 | // Even though the min read size is > 5, we only want an error if the 255 | // exact read size cannot be read. 256 | assert_eq!(rb.read_exact(5).unwrap(), &(1..=5).collect::>()); 257 | 258 | // Now it can fail. 259 | assert!(rb.read_exact(1).is_err()); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/tcp/http.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | net::{SocketAddr, TcpStream}, 4 | sync::Arc, 5 | }; 6 | 7 | use anyhow::{bail, Result}; 8 | 9 | use crate::{config::Config, context, http, reader::ReaderBuf}; 10 | 11 | /// Handle TCP/HTTP connections. 12 | pub(crate) async fn handle_stream(config: Arc, stream: TcpStream) -> Result<()> { 13 | // 8KB is the limit size on many web servers. 14 | try_redirect( 15 | &config, 16 | &context::peer_addr()?, 17 | ReaderBuf::with_capacity(8192, stream), 18 | ) 19 | } 20 | 21 | #[inline(always)] 22 | fn try_redirect(config: &Config, client: &SocketAddr, mut rb: ReaderBuf) -> Result<()> 23 | where 24 | R: Read + Write, 25 | { 26 | // First check if the connection looks like an HTTP one. We only need 5 27 | // bytes for `http::is_http`. 28 | rb.read(5)?; 29 | if !http::is_http(&rb) { 30 | bail!("Not an HTTP request"); 31 | } 32 | 33 | // Looks like an HTTP request, try redirecting it. 34 | http::try_redirect(config, client, &mut rb) 35 | } 36 | -------------------------------------------------------------------------------- /src/tcp/mod.rs: -------------------------------------------------------------------------------- 1 | // Re-export skb.rs 2 | #[allow(clippy::module_inception)] 3 | pub(crate) mod tcp; 4 | pub(crate) use tcp::*; 5 | 6 | pub(crate) mod http; 7 | pub(crate) mod tls; 8 | -------------------------------------------------------------------------------- /src/tcp/tcp.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::Future, 3 | io, 4 | net::{SocketAddr, TcpListener, TcpStream}, 5 | sync::Arc, 6 | time::Duration, 7 | }; 8 | 9 | use anyhow::Result; 10 | use log::{debug, error}; 11 | 12 | use crate::{ 13 | config::Config, 14 | context::*, 15 | runtime, 16 | tls::{self, alert}, 17 | zc, RUNTIME, 18 | }; 19 | 20 | /// Starts a TCP server on `bind` and use the given `handle_stream` function to 21 | /// process incoming connections. 22 | pub(crate) fn listen_and_proxy( 23 | config: Arc, 24 | bind: SocketAddr, 25 | handle_stream: fn(Arc, TcpStream) -> Fut, 26 | ) -> Result<()> 27 | where 28 | Fut: Future> + Send + 'static, 29 | { 30 | let runtime = runtime!()?; 31 | let listener = TcpListener::bind(bind)?; 32 | 33 | // Do not return an error starting from here, this would close the whole 34 | // listener. 35 | 36 | for stream in listener.incoming() { 37 | // Do not fail on stream errors. 38 | let mut stream = match stream { 39 | Ok(stream) => stream, 40 | Err(e) => { 41 | error!("Connection error: {e}"); 42 | continue; 43 | } 44 | }; 45 | 46 | // Extract peer & local addresses w/o failing the whole listener in case 47 | // of errors. 48 | let local = match stream.local_addr() { 49 | Ok(local) => local, 50 | Err(e) => { 51 | match e.kind() { 52 | // Even in this small window the client could close the connection. 53 | io::ErrorKind::NotConnected => continue, 54 | _ => { 55 | error!("Could not get local address: {e}"); 56 | continue; 57 | } 58 | } 59 | } 60 | }; 61 | let peer = match stream.peer_addr() { 62 | Ok(peer) => peer, 63 | Err(e) => { 64 | match e.kind() { 65 | // Even in this small window the client could close the connection. 66 | io::ErrorKind::NotConnected => continue, 67 | _ => { 68 | error!("Could not get peer address: {e}"); 69 | continue; 70 | } 71 | } 72 | } 73 | }; 74 | 75 | // Handle the connection async. 76 | let config = Arc::clone(&config); 77 | runtime.spawn(with_req_context( 78 | ReqContext::from(local, peer), 79 | async move { 80 | debug!("New connection from client"); 81 | 82 | // Set read & write timeouts for processing the message. 83 | if let Err(e) = stream.set_read_timeout(Some(Duration::from_secs(3))) { 84 | let _ = alert(&mut stream, tls::AlertDescription::InternalError); 85 | error!("Could not set a read timeout on TCP stream: {e}"); 86 | return; 87 | } 88 | if let Err(e) = stream.set_write_timeout(Some(Duration::from_secs(3))) { 89 | let _ = alert(&mut stream, tls::AlertDescription::InternalError); 90 | error!("Could not set a write timeout on TCP stream: {e}"); 91 | return; 92 | } 93 | 94 | if let Err(e) = handle_stream(config, stream).await { 95 | error!("{e}"); 96 | } 97 | }, 98 | )); 99 | } 100 | 101 | Ok(()) 102 | } 103 | 104 | #[inline(always)] 105 | pub(super) async fn proxy(client: TcpStream, backend: TcpStream) -> Result<()> { 106 | // Send keepalive to both the client and the backend. 107 | let keep_alive = socket2::TcpKeepalive::new() 108 | .with_time(Duration::from_secs(60)) 109 | .with_interval(Duration::from_secs(60)); 110 | socket2::SockRef::from(&client).set_tcp_keepalive(&keep_alive)?; 111 | socket2::SockRef::from(&backend).set_tcp_keepalive(&keep_alive)?; 112 | 113 | // All good, proxy the rest of the connection. 114 | client.set_nonblocking(true)?; 115 | backend.set_nonblocking(true)?; 116 | let mut client = tokio::net::TcpStream::from_std(client)?; 117 | let mut backend = tokio::net::TcpStream::from_std(backend)?; 118 | 119 | // Move data between backend & client until connections are closed. 120 | debug!("Starting proxying the connection"); 121 | let _ = zc::copy_bidirectional(&mut backend, &mut client).await; 122 | debug!("Connection shut down"); 123 | 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /src/tcp/tls.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{BufWriter, Write}, 3 | net::TcpStream, 4 | sync::Arc, 5 | time::Duration, 6 | }; 7 | 8 | use anyhow::{bail, Result}; 9 | use log::{debug, info}; 10 | 11 | use crate::{ 12 | config::{self, Config}, 13 | context, http, proxy_protocol, 14 | reader::ReaderBuf, 15 | tls::{self, Tls}, 16 | }; 17 | 18 | /// Handle TCP/TLS connections. 19 | pub(crate) async fn handle_stream(config: Arc, stream: TcpStream) -> Result<()> { 20 | let mut rb = ReaderBuf::with_capacity(tls::RECORD_MAX_LEN, stream); 21 | 22 | // Start by checking we got a valid TLS message, and if true parse it. 23 | let tls = match Tls::from(&mut rb) { 24 | Ok(tls) => tls, 25 | Err(e) => { 26 | // If this looks like an HTTP request, try to redirect it. 27 | // Luckily http::is_http needs 5 bytes in the buffer and the 28 | // minimal TLS parsing reads 5 bytes. 29 | if http::is_http(&rb) { 30 | return http::try_redirect(&config, &context::peer_addr()?, &mut rb); 31 | } 32 | 33 | tls::alert(rb.get_mut(), tls::AlertDescription::InternalError)?; 34 | bail!("Could not parse TLS message: {e}"); 35 | } 36 | }; 37 | 38 | // Retrieve the SNI hostname. 39 | let hostname = match tls.hostname() { 40 | Some(name) => name, 41 | // None was present, which is valid. But we can't do anything with that message. 42 | None => { 43 | tls::alert(rb.get_mut(), tls::AlertDescription::UnrecognizedName)?; 44 | info!("No SNI hostname in message"); 45 | return Ok(()); 46 | } 47 | }; 48 | debug!("Found SNI {hostname} in TLS handshake"); 49 | context::set_hostname(hostname)?; 50 | 51 | let peer = &context::peer_addr()?; 52 | let backend = config 53 | .get_backend(hostname, peer, tls.is_challenge()) 54 | .or_else(|e| match e.downcast() { 55 | Ok(e) => match e { 56 | config::Error::HostnameNotFound => { 57 | tls::alert(rb.get_mut(), tls::AlertDescription::UnrecognizedName)?; 58 | bail!("No route found for '{hostname}'") 59 | } 60 | config::Error::NoBackend => { 61 | tls::alert(rb.get_mut(), tls::AlertDescription::AccessDenied)?; 62 | bail!("No backend defined for '{hostname}'") 63 | } 64 | config::Error::AccessDenied => { 65 | tls::alert(rb.get_mut(), tls::AlertDescription::AccessDenied)?; 66 | bail!("Request from {peer} for '{hostname}' was denied by ACLs") 67 | } 68 | }, 69 | Err(e) => bail!(e), 70 | })?; 71 | debug!( 72 | "Using backend {:?} (is alpn challenge? {})", 73 | backend.to_socket_addr(), 74 | tls.is_challenge(), 75 | ); 76 | 77 | // Connect to the backend. 78 | let conn = match TcpStream::connect_timeout(&backend.to_socket_addr()?, Duration::from_secs(3)) 79 | { 80 | Ok(conn) => conn, 81 | Err(e) => { 82 | tls::alert(rb.get_mut(), tls::AlertDescription::InternalError)?; 83 | bail!("Could not connect to backend '{}': {e}", &backend.address); 84 | } 85 | }; 86 | 87 | // Use a buffered writer to avoid small writes until we start forwarding the 88 | // data. 89 | let mut bw = BufWriter::new(conn); 90 | 91 | // Send an HAProxy protocol header if needed. 92 | if let Some(version) = backend.proxy_protocol { 93 | proxy_protocol::write_header(&mut bw, version, &context::local_addr()?, peer)?; 94 | } 95 | 96 | // Replay the handshake. 97 | bw.write_all(rb.buf())?; 98 | 99 | // We can now flush the buffered writer and stop using it to avoid adding 100 | // buffering in the middle of the connection. 101 | let mut conn = bw.into_inner()?; 102 | 103 | // Do not use read & write timeouts for proxying. 104 | if let Err(e) = conn.set_read_timeout(None) { 105 | tls::alert(&mut conn, tls::AlertDescription::InternalError)?; 106 | bail!("Could not unset the read timeout on TCP stream: {e}"); 107 | } 108 | if let Err(e) = conn.set_write_timeout(None) { 109 | tls::alert(&mut conn, tls::AlertDescription::InternalError)?; 110 | bail!("Could not unset the write timeout on TCP stream: {e}"); 111 | } 112 | 113 | super::tcp::proxy(rb.into_inner(), conn).await?; 114 | Ok(()) 115 | } 116 | -------------------------------------------------------------------------------- /src/tls.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | mem, 4 | }; 5 | 6 | use anyhow::{bail, Result}; 7 | 8 | use crate::reader::ReaderBuf; 9 | 10 | pub const RECORD_MAX_LEN: usize = 16 * 1024; 11 | 12 | /// Main representation of a TLS connection. We do not parse all the fields and extensions, nor we 13 | /// read all the data from it. This is mostly used to get to the extensions we care about so we can 14 | /// make a decision on what to do with a new TLS connection. 15 | #[derive(Default)] 16 | pub(crate) struct Tls { 17 | /// Server name indication hostname, if found. 18 | sni_hostname: Option, 19 | /// Is the ALPN extension 20 | alpn_is_challenge: bool, 21 | } 22 | 23 | impl Tls { 24 | pub(crate) fn from(reader: &mut ReaderBuf) -> Result { 25 | // Start by parsing the message up to the extensions. 26 | // 27 | // As soon as we know it's likely to be a TLS record, extend the reader 28 | // min read to match (MAX_TLS_LEN - RECORD_HDR_LEN). 29 | Self::parse_plaintext_record_header(reader)?; 30 | reader.set_min_read(RECORD_MAX_LEN - 5); 31 | Self::parse_handshake_header(reader)?; 32 | Self::parse_client_hello(reader)?; 33 | 34 | // Now we can access the extensions and see if we can find something interesting. 35 | let mut len = Self::read_vector_size(reader, 2)?; 36 | 37 | // No extension, which is valid. 38 | if len == 0 { 39 | return Ok(Tls::default()); 40 | } 41 | 42 | // We have a len but it can't even hold the extension description. 43 | if len < 2 { 44 | bail!("Invalid extensions section length ({} < 2)", len); 45 | } 46 | 47 | // Loop while we have potential valid extension headers. 48 | // https://www.rfc-editor.org/rfc/rfc8446#section-4.2 49 | let mut tls = Tls::default(); 50 | while len >= 4 { 51 | // Extension type: u16 52 | // Vector size: u16 53 | let header = reader.read_as::<[u8; 4]>()?; 54 | len -= mem::size_of_val(header); 55 | 56 | let r#type = u16::from_be_bytes(header[0..=1].try_into()?); 57 | let size = u16::from_be_bytes(header[2..=3].try_into()?) as usize; 58 | 59 | // Check we can't go past the extension section. 60 | if size > len { 61 | bail!( 62 | "Invalid extension: goes past the buffer len ({} > {})", 63 | size, 64 | len 65 | ); 66 | } 67 | 68 | // Extension is empty, can happen e.g. on session_ticket 69 | if size == 0 { 70 | continue; 71 | } 72 | 73 | // Read the extension data. Even if we do not support the extension, we can't seek as 74 | // we need to replay the TLS message. 75 | let extension = reader.read_exact(size)?; 76 | len -= size; 77 | 78 | // Specific handling depending on the extension type. 79 | match r#type { 80 | // Server name indication. 81 | 0 => tls.sni_hostname = Some(Self::sni_ext_get_hostname(extension)?), 82 | // Application layer protocol negotiation. 83 | 16 => tls.alpn_is_challenge = Self::alpn_ext_is_challenge(extension)?, 84 | _ => (), 85 | } 86 | } 87 | 88 | Ok(tls) 89 | } 90 | 91 | /// Parse a TLS plaintext record header. 92 | /// https://www.rfc-editor.org/rfc/rfc8446#section-5.1 93 | fn parse_plaintext_record_header(reader: &mut ReaderBuf) -> Result<()> { 94 | // Record header: 95 | // type: u8 96 | // major: u8 97 | // minor: u8 98 | // length: u16 99 | let record = reader.read_as::<[u8; 5]>()?; 100 | 101 | // Check if record type is 22, aka handshake. 102 | if record[0] != 22 { 103 | bail!("Record is not a TLS handshake."); 104 | } 105 | 106 | // Check the TLS version is supported. 107 | // 3.1: TLS 1.0, 3.2: TLS 1.1, 3.3: TLS 1.2 & TLS 1.3 108 | if record[1] != 3 || (record[2] < 1 || record[2] > 3) { 109 | bail!("TLS version not supported: {}.{}.", record[1], record[2]); 110 | } 111 | 112 | // Check the length does not exceed the maximum authorized. 113 | if u16::from_be_bytes(record[3..=4].try_into()?) > RECORD_MAX_LEN as u16 { 114 | bail!("TLS record length exceed the maximum authorized."); 115 | } 116 | 117 | Ok(()) 118 | } 119 | 120 | /// Parse a TLS handshake header. 121 | /// https://www.rfc-editor.org/rfc/rfc8446#section-4 122 | fn parse_handshake_header(reader: &mut ReaderBuf) -> Result<()> { 123 | // Handshake header: 124 | // Message Type: u8 125 | // Message Len: [u8; 3] 126 | let handshake = reader.read_as::<[u8; 4]>()?; 127 | 128 | // Check we're dealing with a ClientHello message. 129 | if handshake[0] != 1 { 130 | bail!( 131 | "TLS handshake is not a ClientHello message ({})", 132 | handshake[0] 133 | ); 134 | } 135 | 136 | // We're not checking the length here as we'll try to read it anyway. 137 | 138 | Ok(()) 139 | } 140 | 141 | /// Parse a TLS client hello message up to the extensions section. 142 | /// https://www.rfc-editor.org/rfc/rfc8446#section-4.1.2 143 | /// 144 | /// CLientHello header: 145 | /// Version: u16 146 | /// Random bytes: [u8; 32] 147 | /// Session id: 148 | /// Cipher suite: 149 | /// Compression method: 150 | /// Extensions: 151 | fn parse_client_hello(reader: &mut ReaderBuf) -> Result<()> { 152 | // Start by parsing the two first fields (version & random) as they have a fixed length, 153 | // which is not true for later fields. 154 | let hello = reader.read_as::<[u8; 34]>()?; 155 | 156 | // Check the version. 0x301: TLS 1.0, 0x302: TLS 1.1, 0x303: >= TLS 1.2. 157 | match u16::from_be_bytes(hello[0..=1].try_into()?) { 158 | 0x301..=0x303 => (), 159 | x => bail!("Invalid client version in ClientHello ({:#x})", x), 160 | } 161 | 162 | // Read the session id. 163 | let len = Self::read_vector(reader, 1)?.len(); 164 | if len > 32 { 165 | bail!("Session id has an invalid length ({} > 32)", len); 166 | } 167 | 168 | // Read the cipher suites. 169 | let len = Self::read_vector(reader, 2)?.len(); 170 | if len < 2 { 171 | bail!("Cipher suites length is too small ({} < 2)", len); 172 | } else if len % 2 != 0 { 173 | bail!("Cipher suites length is invalid ({} % 2 != 0)", len); 174 | } 175 | 176 | // Read the compression methods. 177 | let len = Self::read_vector(reader, 1)?.len(); 178 | if len < 1 { 179 | bail!("Compression methods length is too small ({} < 1)", len); 180 | } 181 | 182 | // We reached the extensions (or none, which is also valid). 183 | Ok(()) 184 | } 185 | 186 | /// Parse and read a vector size field. Takes the length of the field size as a parameter. 187 | fn read_vector_size(reader: &mut ReaderBuf, len: usize) -> Result { 188 | Ok(match len { 189 | 1 => *reader.read_as::()? as usize, 190 | 2 => { 191 | let size = reader.read_as::<[u8; 2]>()?; 192 | u16::from_be_bytes(*size) as usize 193 | } 194 | x => bail!("Vector length unsupported ({})", x), 195 | }) 196 | } 197 | 198 | /// Parse the server name indication extension, look for an host name and return it if found. 199 | /// https://www.rfc-editor.org/rfc/rfc6066#section-3 200 | fn sni_ext_get_hostname(ext: &[u8]) -> Result { 201 | let buf_len = ext.len(); 202 | 203 | // No need to go further if we can't even read the field size below. 204 | if buf_len < 2 { 205 | bail!("SNI extension len is too small ({} < 2)", buf_len); 206 | } 207 | 208 | // Retrieve the size of the extension and take into account the len field itself. 209 | let len = u16::from_be_bytes(ext[0..=1].try_into()?) as usize + mem::size_of::(); 210 | // We read the extension size above, initialize the cursor to go past it. 211 | let mut cursor = mem::size_of::(); 212 | 213 | // Check the buffer we are working on matches the size it contains. 214 | if len != buf_len { 215 | bail!( 216 | "SNI extension len does not match the buffer one ({} != {})", 217 | len, 218 | buf_len 219 | ); 220 | } 221 | 222 | // Go through the names as defined in RFC 6066. Only name type 0 is valid for now and the 223 | // RFC states "The ServerNameList MUST NOT contain more than one name of the same 224 | // name_type". Because of this we're taking a shortcut below and do not actually loop 225 | // through the names. 226 | // 227 | // https://www.rfc-editor.org/rfc/rfc6066#section-3 228 | 229 | // Check we won't go past the buffer. 230 | if cursor + mem::size_of::() /* type */ + mem::size_of::() /* size */ > len { 231 | bail!("Reached the end of the SNI extension buffer while processing"); 232 | } 233 | 234 | // First parse the name type. 235 | let r#type = ext[cursor]; 236 | cursor += mem::size_of::(); 237 | 238 | // Then its size. 239 | let size = u16::from_be_bytes(ext[cursor..(cursor + 2)].try_into()?) as usize; 240 | cursor += mem::size_of::(); 241 | 242 | // Only type 0 (host name) is valid so far. 243 | if r#type != 0 { 244 | bail!("Unknown name type in the SNI extension ({})", r#type); 245 | } 246 | 247 | // Check we won't go past the buffer. 248 | if cursor + size > len { 249 | bail!("Reached the end of the SNI extension buffer while processing"); 250 | } 251 | 252 | // As only one name type is allowed and only one name per type can be found, check we 253 | // reached the end of the buffer. 254 | if cursor + size != len { 255 | bail!("SNI extension has more than one name"); 256 | } 257 | 258 | // Finally retrieve the SNI. 259 | Ok(String::from_utf8(ext[cursor..(cursor + size)].into())?) 260 | } 261 | 262 | /// Parse the ALPN extension, look for TLS challenge strings and return true if found. 263 | /// 264 | /// https://www.rfc-editor.org/rfc/rfc8737 265 | fn alpn_ext_is_challenge(ext: &[u8]) -> Result { 266 | let buf_len = ext.len(); 267 | 268 | // No need to go further if we can't even read the field size below. 269 | if buf_len < 2 { 270 | bail!("ALPN extension len is too small ({} < 2)", buf_len); 271 | } 272 | 273 | // Retrieve the size of the extension and take into account the len field itself. 274 | let len = u16::from_be_bytes(ext[0..=1].try_into()?) as usize + mem::size_of::(); 275 | // We read the extension size above, initialize the cursor to go past it. 276 | let mut cursor = mem::size_of::(); 277 | 278 | // Check the buffer we are working on matches the size it contains. 279 | if len != buf_len { 280 | bail!( 281 | "ALPN extension len does not match the buffer one ({} != {})", 282 | len, 283 | buf_len 284 | ); 285 | } 286 | 287 | // Go through the protocol names as defined in RFC7301. 288 | // 289 | // We're not looping through the names below and instead look for the first valid one. 290 | // We're taking this shortcut as RFC8737 explicitly states "the ACME server MUST provide 291 | // an ALPN extension with the single protocol name 'acme-tls/1'". 292 | // 293 | // https://datatracker.ietf.org/doc/html/rfc7301#section-3.1 294 | // https://www.rfc-editor.org/rfc/rfc8737#section-3 295 | 296 | // Check we won't go past the buffer. 297 | if cursor + mem::size_of::() /* size */ > len { 298 | bail!("Reached the end of the ALPN extension buffer while processing"); 299 | } 300 | 301 | // First parse the name string size. 302 | let size = ext[cursor] as usize; 303 | cursor += mem::size_of::(); 304 | 305 | // Check we won't go past the buffer. 306 | if cursor + size > len { 307 | println!("{cursor} + {size} > {len}"); 308 | bail!("Reached the end of the ALPN extension buffer while processing"); 309 | } 310 | 311 | // Finally retrieve the protocol name. 312 | let name = String::from_utf8(ext[cursor..(cursor + size)].into())?; 313 | cursor += size; 314 | 315 | // Check if the protocol name is for tls-alpn-01 and is the only protocol listed in the 316 | // extension. https://www.rfc-editor.org/rfc/rfc8737#section-3 317 | if cursor == len && name == "acme-tls/1" { 318 | return Ok(true); 319 | } 320 | 321 | Ok(false) 322 | } 323 | 324 | /// Parse and read a vector, and return a Vec with its data. Takes the length of the field 325 | /// size as a parameter. 326 | fn read_vector(reader: &mut ReaderBuf, len: usize) -> Result> { 327 | let size = Self::read_vector_size(reader, len)?; 328 | 329 | // Valid, can be checked for specific cases outside this helper. 330 | if size == 0 { 331 | return Ok(Vec::new()); 332 | } 333 | 334 | // Finally read the vector data. 335 | Ok(reader.read_exact(size)?.to_vec()) 336 | } 337 | 338 | /// Get the hostname we read from the SNI extension, if any. None is a valid valid regarding the 339 | /// TLS spec. 340 | pub(crate) fn hostname(&self) -> Option<&String> { 341 | self.sni_hostname.as_ref() 342 | } 343 | 344 | /// Check if the ALPN extension was a valid tls-alpn-01 challenge, if any. 345 | pub(crate) fn is_challenge(&self) -> bool { 346 | self.alpn_is_challenge 347 | } 348 | } 349 | 350 | /// https://www.rfc-editor.org/rfc/rfc8446#section-6 351 | pub(crate) enum AlertDescription { 352 | AccessDenied = 49, 353 | InternalError = 80, 354 | UnrecognizedName = 112, 355 | } 356 | 357 | /// Send a fatal alert message with the provided desc code to the remote end. 358 | pub(crate) fn alert(writer: &mut T, desc: AlertDescription) -> Result<()> { 359 | // Send back a crafted alert message. 360 | // https://www.rfc-editor.org/rfc/rfc8446#section-5.1 361 | // https://www.rfc-editor.org/rfc/rfc8446#section-6 362 | // 363 | // - Content type: 21 364 | // - TLS version: 3.x 365 | // - Length: 2 366 | // - Level: 2 (fatal) 367 | // - Desc. 368 | writer.write_all(&[21, 3, 0, 0, 2, 2, desc as u8])?; 369 | Ok(()) 370 | } 371 | 372 | #[cfg(test)] 373 | pub(crate) mod tests { 374 | use super::*; 375 | use crate::reader::ReaderBuf as B; 376 | 377 | // Valid record, including an SNI but no ALPN extension. 378 | pub(crate) const RECORD_SNI: &[u8] = &[ 379 | 22, 3, 1, 1, 54, 1, 0, 1, 50, 3, 3, 203, 69, 166, 24, 168, 5, 235, 3, 40, 94, 250, 34, 63, 380 | 198, 156, 194, 25, 13, 0, 80, 200, 213, 125, 74, 215, 165, 193, 219, 143, 84, 201, 35, 32, 381 | 232, 149, 249, 110, 18, 24, 36, 194, 152, 145, 10, 139, 7, 175, 172, 173, 61, 56, 71, 185, 382 | 191, 71, 213, 156, 229, 62, 54, 91, 75, 253, 9, 104, 0, 72, 19, 2, 19, 3, 19, 1, 19, 4, 383 | 192, 44, 192, 48, 204, 169, 204, 168, 192, 173, 192, 43, 192, 47, 192, 172, 192, 35, 192, 384 | 39, 192, 10, 192, 20, 192, 9, 192, 19, 0, 157, 192, 157, 0, 156, 192, 156, 0, 61, 0, 60, 0, 385 | 53, 0, 47, 0, 159, 204, 170, 192, 159, 0, 158, 192, 158, 0, 107, 0, 103, 0, 57, 0, 51, 0, 386 | 255, 1, 0, 0, 161, 0, 0, 0, 16, 0, 14, 0, 0, 11, 101, 120, 97, 109, 112, 108, 101, 46, 110, 387 | 101, 116, 0, 11, 0, 4, 3, 0, 1, 2, 0, 10, 0, 22, 0, 20, 0, 29, 0, 23, 0, 30, 0, 25, 0, 24, 388 | 1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 0, 35, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 13, 0, 34, 0, 32, 389 | 4, 3, 5, 3, 6, 3, 8, 7, 8, 8, 8, 9, 8, 10, 8, 11, 8, 4, 8, 5, 8, 6, 4, 1, 5, 1, 6, 1, 3, 3, 390 | 3, 1, 0, 43, 0, 5, 4, 3, 4, 3, 3, 0, 45, 0, 2, 1, 1, 0, 51, 0, 38, 0, 36, 0, 29, 0, 32, 391 | 240, 147, 220, 154, 241, 161, 127, 109, 148, 66, 113, 35, 83, 38, 72, 28, 160, 33, 215, 392 | 192, 53, 121, 246, 185, 203, 110, 197, 32, 128, 254, 152, 97, 393 | ]; 394 | 395 | // Valid record, including an SNI and an ALPN extension. 396 | pub(crate) const RECORD_SNI_ALPN: &[u8] = &[ 397 | 22, 3, 1, 1, 71, 1, 0, 1, 67, 3, 3, 200, 84, 240, 198, 191, 79, 87, 134, 132, 184, 32, 142, 398 | 147, 79, 172, 138, 254, 33, 184, 196, 224, 73, 186, 162, 178, 28, 93, 80, 154, 180, 197, 399 | 117, 32, 105, 182, 50, 2, 25, 6, 98, 98, 89, 78, 89, 134, 43, 34, 138, 16, 244, 31, 185, 400 | 254, 246, 209, 12, 203, 31, 69, 37, 134, 237, 216, 165, 5, 0, 72, 19, 2, 19, 3, 19, 1, 19, 401 | 4, 192, 44, 192, 48, 204, 169, 204, 168, 192, 173, 192, 43, 192, 47, 192, 172, 192, 35, 402 | 192, 39, 192, 10, 192, 20, 192, 9, 192, 19, 0, 157, 192, 157, 0, 156, 192, 156, 0, 61, 0, 403 | 60, 0, 53, 0, 47, 0, 159, 204, 170, 192, 159, 0, 158, 192, 158, 0, 107, 0, 103, 0, 57, 0, 404 | 51, 0, 255, 1, 0, 0, 178, 0, 0, 0, 16, 0, 14, 0, 0, 11, 101, 120, 97, 109, 112, 108, 101, 405 | 46, 110, 101, 116, 0, 11, 0, 4, 3, 0, 1, 2, 0, 10, 0, 22, 0, 20, 0, 29, 0, 23, 0, 30, 0, 406 | 25, 0, 24, 1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 0, 35, 0, 0, 0, 16, 0, 13, 0, 11, 10, 97, 99, 109, 407 | 101, 45, 116, 108, 115, 47, 49, 0, 22, 0, 0, 0, 23, 0, 0, 0, 13, 0, 34, 0, 32, 4, 3, 5, 3, 408 | 6, 3, 8, 7, 8, 8, 8, 9, 8, 10, 8, 11, 8, 4, 8, 5, 8, 6, 4, 1, 5, 1, 6, 1, 3, 3, 3, 1, 0, 409 | 43, 0, 5, 4, 3, 4, 3, 3, 0, 45, 0, 2, 1, 1, 0, 51, 0, 38, 0, 36, 0, 29, 0, 32, 205, 54, 410 | 119, 60, 111, 182, 114, 106, 157, 109, 117, 208, 183, 128, 208, 86, 101, 69, 206, 87, 119, 411 | 236, 20, 71, 211, 71, 215, 186, 239, 195, 3, 21, 412 | ]; 413 | 414 | // Valid record, no extension. 415 | pub(crate) const RECORD_NO_EXT: &[u8] = &[ 416 | 22, 3, 1, 1, 34, 1, 0, 1, 30, 3, 3, 174, 236, 43, 233, 60, 1, 225, 235, 52, 225, 121, 90, 417 | 72, 102, 153, 32, 127, 186, 243, 82, 5, 211, 126, 210, 140, 62, 55, 13, 105, 153, 87, 230, 418 | 32, 242, 103, 97, 74, 54, 19, 236, 162, 139, 127, 239, 150, 191, 164, 241, 242, 223, 41, 419 | 73, 93, 70, 173, 109, 216, 49, 64, 180, 72, 158, 82, 151, 159, 0, 72, 19, 2, 19, 3, 19, 1, 420 | 19, 4, 192, 44, 192, 48, 204, 169, 204, 168, 192, 173, 192, 43, 192, 47, 192, 172, 192, 35, 421 | 192, 39, 192, 10, 192, 20, 192, 9, 192, 19, 0, 157, 192, 157, 0, 156, 192, 156, 0, 61, 0, 422 | 60, 0, 53, 0, 47, 0, 159, 204, 170, 192, 159, 0, 158, 192, 158, 0, 107, 0, 103, 0, 57, 0, 423 | 51, 0, 255, 1, 0, 0, 141, 0, 11, 0, 4, 3, 0, 1, 2, 0, 10, 0, 22, 0, 20, 0, 29, 0, 23, 0, 424 | 30, 0, 25, 0, 24, 1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 0, 35, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 425 | 13, 0, 34, 0, 32, 4, 3, 5, 3, 6, 3, 8, 7, 8, 8, 8, 9, 8, 10, 8, 11, 8, 4, 8, 5, 8, 6, 4, 1, 426 | 5, 1, 6, 1, 3, 3, 3, 1, 0, 43, 0, 5, 4, 3, 4, 3, 3, 0, 45, 0, 2, 1, 1, 0, 51, 0, 38, 0, 36, 427 | 0, 29, 0, 32, 87, 236, 148, 113, 132, 227, 66, 188, 129, 107, 224, 171, 174, 68, 70, 34, 428 | 200, 235, 65, 252, 62, 213, 12, 28, 115, 126, 46, 52, 72, 108, 158, 10, 429 | ]; 430 | 431 | #[test] 432 | fn vector() { 433 | // Valid vectors with no data. 434 | assert!(Tls::read_vector(&mut B::from_bytes(&[0]), 1).is_ok()); 435 | assert!(Tls::read_vector(&mut B::from_bytes(&[0, 0]), 2).is_ok()); 436 | 437 | // Valid vectors with data. 438 | assert!(Tls::read_vector(&mut B::from_bytes(&[1, 42]), 1).is_ok()); 439 | assert!(Tls::read_vector(&mut B::from_bytes(&[5, 42, 0, 10, 255, 3]), 1).is_ok()); 440 | let vector = [vec![255], vec![42; 255]].concat(); 441 | assert!(Tls::read_vector(&mut B::from_bytes(&vector), 1).is_ok()); 442 | assert!(Tls::read_vector(&mut B::from_bytes(&[0, 1, 42]), 2).is_ok()); 443 | assert!(Tls::read_vector(&mut B::from_bytes(&[0, 3, 42, 13, 37]), 2).is_ok()); 444 | let vector = [vec![1, 0], vec![10; 256]].concat(); 445 | assert!(Tls::read_vector(&mut B::from_bytes(&vector), 2).is_ok()); 446 | let vector = [vec![255, 255], vec![99; 255 << 16 | 255]].concat(); 447 | assert!(Tls::read_vector(&mut B::from_bytes(&vector), 2).is_ok()); 448 | 449 | // Empty vectors. 450 | assert!(Tls::read_vector(&mut B::from_bytes(&[]), 1).is_err()); 451 | assert!(Tls::read_vector(&mut B::from_bytes(&[]), 2).is_err()); 452 | 453 | // Vectors too small. 454 | assert!(Tls::read_vector(&mut B::from_bytes(&[0]), 2).is_err()); 455 | assert!(Tls::read_vector(&mut B::from_bytes(&[1]), 1).is_err()); 456 | assert!(Tls::read_vector(&mut B::from_bytes(&[2, 0]), 1).is_err()); 457 | assert!(Tls::read_vector(&mut B::from_bytes(&[255, 0]), 1).is_err()); 458 | assert!(Tls::read_vector(&mut B::from_bytes(&[0, 1]), 2).is_err()); 459 | assert!(Tls::read_vector(&mut B::from_bytes(&[0, 3, 0, 0]), 2).is_err()); 460 | assert!(Tls::read_vector(&mut B::from_bytes(&[255, 255, 0]), 2).is_err()); 461 | } 462 | 463 | #[test] 464 | fn record() { 465 | // Valid record headers, using different TLS versions and lengths. 466 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 1, 0, 0])).is_ok()); 467 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 2, 0, 0])).is_ok()); 468 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 3, 0, 0])).is_ok()); 469 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 1, 64, 0])).is_ok()); 470 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 2, 0, 42])).is_ok()); 471 | assert!( 472 | Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 3, 13, 37])).is_ok() 473 | ); 474 | 475 | // Invalid records. 476 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[0, 0, 0, 0, 0])).is_err()); 477 | assert!( 478 | Tls::parse_plaintext_record_header(&mut B::from_bytes(&[255, 1, 0, 0, 0])).is_err() 479 | ); 480 | 481 | // Invalid versions. 482 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 0, 3, 0, 0])).is_err()); 483 | assert!( 484 | Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 255, 3, 0, 0])).is_err() 485 | ); 486 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 0, 0, 0])).is_err()); 487 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 4, 0, 0])).is_err()); 488 | assert!( 489 | Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 255, 0, 0])).is_err() 490 | ); 491 | 492 | // Invalid length field. 493 | assert!( 494 | Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 3, 64, 1])).is_err() 495 | ); 496 | assert!( 497 | Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 3, 255, 255])).is_err() 498 | ); 499 | 500 | // Not enough data in the reader. 501 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 3, 0])).is_err()); 502 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3, 3])).is_err()); 503 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22, 3])).is_err()); 504 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[22])).is_err()); 505 | assert!(Tls::parse_plaintext_record_header(&mut B::from_bytes(&[])).is_err()); 506 | } 507 | 508 | #[test] 509 | fn handshake() { 510 | // Client Hello empty message. 511 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[1, 0, 0, 0])).is_ok()); 512 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[1, 255, 255, 255])).is_ok()); 513 | 514 | // Invalid messages (non-client hello). 515 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[0, 0, 0, 0])).is_err()); 516 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[42, 0, 0, 0])).is_err()); 517 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[255, 0, 0, 0])).is_err()); 518 | 519 | // Not enough data in the reader. 520 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[1, 0, 0])).is_err()); 521 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[1, 0])).is_err()); 522 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[1])).is_err()); 523 | assert!(Tls::parse_handshake_header(&mut B::from_bytes(&[])).is_err()); 524 | } 525 | 526 | #[test] 527 | fn client_hello() { 528 | let protocol_version = vec![0x3, 0x3]; 529 | #[rustfmt::skip] 530 | let random = vec![ 531 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 532 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 533 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 534 | 0x0, 0x0, 535 | ]; 536 | let session_id = vec![0x0]; 537 | let cipher_suites = vec![0x0, 0x2, 0x0, 0x0]; 538 | let compression_methods = vec![0x1, 0x0]; 539 | 540 | // Full ClientHello message (w/o extensions). 541 | let hello = [ 542 | protocol_version.clone(), 543 | random.clone(), 544 | session_id.clone(), 545 | cipher_suites.clone(), 546 | compression_methods.clone(), 547 | ] 548 | .concat(); 549 | 550 | // Valid protocol versions. 551 | let mut buf = hello.clone(); 552 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_ok()); 553 | buf[1] = 0x1; 554 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_ok()); 555 | buf[1] = 0x2; 556 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_ok()); 557 | 558 | // Invalid protocol versions. 559 | let mut buf = hello.clone(); 560 | buf[0] = 1; 561 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 562 | buf[0] = 0; 563 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 564 | buf[0] = 255; 565 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 566 | buf[0] = 3; 567 | buf[1] = 0; 568 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 569 | buf[1] = 4; 570 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 571 | buf[1] = 255; 572 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 573 | 574 | // Invalid random. 575 | let invalid = vec![ 576 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 577 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 578 | ]; 579 | let buf = [ 580 | protocol_version.clone(), 581 | invalid, 582 | session_id.clone(), 583 | cipher_suites.clone(), 584 | compression_methods.clone(), 585 | ] 586 | .concat(); 587 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 588 | 589 | let invalid = vec![ 590 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 591 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 592 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 593 | ]; 594 | let buf = [ 595 | protocol_version.clone(), 596 | invalid, 597 | session_id.clone(), 598 | cipher_suites.clone(), 599 | compression_methods.clone(), 600 | ] 601 | .concat(); 602 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 603 | 604 | // Valid longer session ids. 605 | let valid = vec![0x1, 0x42]; 606 | let buf = [ 607 | protocol_version.clone(), 608 | random.clone(), 609 | valid, 610 | cipher_suites.clone(), 611 | compression_methods.clone(), 612 | ] 613 | .concat(); 614 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_ok()); 615 | 616 | let valid = vec![0x3, 0x42, 0x13, 0x37]; 617 | let buf = [ 618 | protocol_version.clone(), 619 | random.clone(), 620 | valid, 621 | cipher_suites.clone(), 622 | compression_methods.clone(), 623 | ] 624 | .concat(); 625 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_ok()); 626 | 627 | // Invalid session id. 628 | let mut buf = hello.clone(); 629 | buf[34] = 33; 630 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 631 | buf[34] = 255; 632 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 633 | buf[34] = 16; /* Valid len but buffer too small */ 634 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 635 | 636 | // Invalid cipher suites. 637 | let mut buf = hello.clone(); 638 | buf[36] = 0; 639 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 640 | buf[36] = 3; 641 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 642 | buf[35] = 3; 643 | buf[36] = 5; 644 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 645 | 646 | // Invalid compression method. 647 | let mut buf = hello.clone(); 648 | buf[39] = 0; 649 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 650 | buf[39] = 255; /* Valid len but buffer too small */ 651 | assert!(Tls::parse_client_hello(&mut B::from_bytes(&buf)).is_err()); 652 | } 653 | 654 | #[test] 655 | fn sni_hostname() { 656 | // Valid, single SNI record. 657 | let sni = &[ 658 | 0, 14, 0, 0, 11, 101, 120, 97, 109, 112, 108, 101, 46, 110, 101, 116, 659 | ]; 660 | assert!(Tls::sni_ext_get_hostname(sni).unwrap() == "example.net"); 661 | 662 | // Invalid lengths. 663 | assert!(Tls::sni_ext_get_hostname(&[]).is_err()); 664 | assert!(Tls::sni_ext_get_hostname(&[0]).is_err()); 665 | assert!(Tls::sni_ext_get_hostname(&[0, 0]).is_err()); 666 | assert!(Tls::sni_ext_get_hostname(&[255, 255]).is_err()); 667 | assert!(Tls::sni_ext_get_hostname(&[0, 10, 0, 0]).is_err()); 668 | assert!(Tls::sni_ext_get_hostname(&[0, 10, 0, 0, 0]).is_err()); 669 | assert!(Tls::sni_ext_get_hostname(&[0, 10, 0, 255, 255]).is_err()); 670 | 671 | // Invalid SNI length. 672 | let sni = &[ 673 | 0, 15, 1, 0, 11, 101, 120, 97, 109, 112, 108, 101, 46, 110, 101, 116, 674 | ]; 675 | assert!(Tls::sni_ext_get_hostname(sni).is_err()); 676 | let sni = &[ 677 | 0, 13, 1, 0, 12, 101, 120, 97, 109, 112, 108, 101, 46, 110, 101, 116, 678 | ]; 679 | assert!(Tls::sni_ext_get_hostname(sni).is_err()); 680 | let sni = &[ 681 | 0, 14, 1, 0, 13, 101, 120, 97, 109, 112, 108, 101, 46, 110, 101, 116, 682 | ]; 683 | assert!(Tls::sni_ext_get_hostname(sni).is_err()); 684 | 685 | // Invalid SNI types. 686 | let sni = &[ 687 | 0, 14, 1, 0, 11, 101, 120, 97, 109, 112, 108, 101, 46, 110, 101, 116, 688 | ]; 689 | assert!(Tls::sni_ext_get_hostname(sni).is_err()); 690 | let sni = &[ 691 | 0, 14, 255, 0, 11, 101, 120, 97, 109, 112, 108, 101, 46, 110, 101, 116, 692 | ]; 693 | assert!(Tls::sni_ext_get_hostname(sni).is_err()); 694 | 695 | // Invalid, multiple SNI records. 696 | #[rustfmt::skip] 697 | let sni = &[ 698 | 0, 26, 699 | 1, 0, 13, 101, 120, 97, 109, 112, 108, 101, 46, 110, 101, 116, 700 | 0, 0, 9, 108, 111, 99, 97, 108, 104, 111, 115, 116, 701 | ]; 702 | assert!(Tls::sni_ext_get_hostname(sni).is_err()); 703 | } 704 | 705 | #[test] 706 | fn alpn_tls_challenge() { 707 | // Valid tls-alpn-01 challenge record. 708 | let alpn = &[0, 11, 10, 97, 99, 109, 101, 45, 116, 108, 115, 47, 49]; 709 | assert!(Tls::alpn_ext_is_challenge(alpn).unwrap() == true); 710 | 711 | // Valid non tls-alpn-01 challenge record. 712 | assert!(Tls::alpn_ext_is_challenge(&[0, 3, 2, 104, 50]).unwrap() == false); 713 | 714 | // Invalid lengths. 715 | assert!(Tls::alpn_ext_is_challenge(&[]).is_err()); 716 | assert!(Tls::alpn_ext_is_challenge(&[0]).is_err()); 717 | assert!(Tls::alpn_ext_is_challenge(&[0, 0]).is_err()); 718 | assert!(Tls::alpn_ext_is_challenge(&[255, 255]).is_err()); 719 | assert!(Tls::alpn_ext_is_challenge(&[0, 10, 0]).is_err()); 720 | assert!(Tls::alpn_ext_is_challenge(&[0, 10, 255]).is_err()); 721 | 722 | // Invalid ALPN length. 723 | let alpn = &[0, 12, 10, 97, 99, 109, 101, 45, 116, 108, 115, 47]; 724 | assert!(Tls::alpn_ext_is_challenge(alpn).is_err()); 725 | let alpn = &[0, 11, 10, 97, 99, 109, 101, 45, 116, 108, 115, 47]; 726 | assert!(Tls::alpn_ext_is_challenge(alpn).is_err()); 727 | let alpn = &[0, 11, 11, 97, 99, 109, 101, 45, 116, 108, 115, 47, 49]; 728 | assert!(Tls::alpn_ext_is_challenge(alpn).is_err()); 729 | 730 | // Multiple ALPN records. Valid, but not for tls-alpn-01. 731 | #[rustfmt::skip] 732 | let alpn = &[ 733 | 0, 14, 734 | 10, 97, 99, 109, 101, 45, 116, 108, 115, 47, 49, 735 | 2, 104, 50, 736 | ]; 737 | assert!(Tls::alpn_ext_is_challenge(alpn).unwrap() == false); 738 | } 739 | 740 | #[test] 741 | fn tls() { 742 | let tls = Tls::from(&mut B::from_bytes(RECORD_SNI)).unwrap(); 743 | assert!(tls.hostname().unwrap() == "example.net"); 744 | assert!(tls.is_challenge() == false); 745 | 746 | let tls = Tls::from(&mut B::from_bytes(RECORD_SNI_ALPN)).unwrap(); 747 | assert!(tls.hostname().unwrap() == "example.net"); 748 | assert!(tls.is_challenge() == true); 749 | 750 | let tls = Tls::from(&mut B::from_bytes(RECORD_NO_EXT)).unwrap(); 751 | assert!(tls.hostname().is_none()); 752 | assert!(tls.is_challenge() == false); 753 | } 754 | } 755 | -------------------------------------------------------------------------------- /src/zc.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::poll_fn, 3 | io::{self, Error}, 4 | mem, 5 | os::fd::{AsRawFd, RawFd}, 6 | pin::Pin, 7 | ptr, 8 | task::{ready, Context, Poll}, 9 | }; 10 | 11 | use anyhow::{anyhow, bail, Result}; 12 | use tokio::io::AsyncWriteExt; 13 | 14 | // Use 1M pipe size as this is the default max pipe buffer size 15 | // (see /proc/sys/fs/pipe-max-size). 16 | const PIPE_SIZE: usize = 1 << 20; 17 | 18 | /// Bidirectional copy between two ZcAsyncIo enabled-types, in a zero-copy 19 | /// fashion. 20 | pub(crate) async fn copy_bidirectional(a: &mut T, b: &mut T) -> Result<(usize, usize)> 21 | where 22 | T: ZcAsyncIo, 23 | { 24 | let mut ab = Splice::new()?; 25 | let mut ba = Splice::new()?; 26 | 27 | let ret = poll_fn(|ctx| { 28 | // Do not wait for both ends to gracefully shutdown. 29 | match (ab.process(ctx, a, b)?, ba.process(ctx, b, a)?) { 30 | (Poll::Ready(a), Poll::Ready(b)) => Poll::Ready(Ok((a, b))), 31 | (Poll::Ready(a), Poll::Pending) => Poll::Ready(Ok((a, 0))), 32 | (Poll::Pending, Poll::Ready(b)) => Poll::Ready(Ok((0, b))), 33 | _ => Poll::Pending, 34 | } 35 | }) 36 | .await; 37 | 38 | if ret.is_err() { 39 | // In case the copy did not returned cleanly, make sure both sides are 40 | // closed. 41 | let _ = a.shutdown().await; 42 | let _ = b.shutdown().await; 43 | } 44 | ret 45 | } 46 | 47 | /// Trait to access async io operations. Direct 1:1 mapping to what some tokio 48 | /// types provide. 49 | pub(crate) trait ZcAsyncIo: 50 | Unpin + AsRawFd + tokio::io::AsyncRead + tokio::io::AsyncWrite 51 | { 52 | fn poll_read_ready(&self, cx: &mut Context<'_>) -> Poll>; 53 | fn poll_write_ready(&self, cx: &mut Context<'_>) -> Poll>; 54 | fn try_io( 55 | &self, 56 | interest: tokio::io::Interest, 57 | f: impl FnOnce() -> io::Result, 58 | ) -> io::Result; 59 | async fn shutdown(&mut self) -> io::Result<()>; 60 | } 61 | 62 | macro_rules! zero_copy { 63 | ($target: ty) => { 64 | impl ZcAsyncIo for $target { 65 | #[inline(always)] 66 | fn poll_read_ready(&self, ctx: &mut Context<'_>) -> Poll> { 67 | self.poll_read_ready(ctx) 68 | } 69 | #[inline(always)] 70 | fn poll_write_ready(&self, ctx: &mut Context<'_>) -> Poll> { 71 | self.poll_write_ready(ctx) 72 | } 73 | #[inline(always)] 74 | fn try_io( 75 | &self, 76 | interest: tokio::io::Interest, 77 | f: impl FnOnce() -> io::Result, 78 | ) -> io::Result { 79 | self.try_io(interest, f) 80 | } 81 | #[inline(always)] 82 | async fn shutdown(&mut self) -> io::Result<()> { 83 | AsyncWriteExt::shutdown(self).await 84 | } 85 | } 86 | }; 87 | } 88 | zero_copy!(tokio::net::TcpStream); 89 | 90 | /// Splice represents a stateful zero-copy between two file descriptors, under 91 | /// the hood using pipe2(2) and splice(2). 92 | enum Splice { 93 | /// Move state: data is being moved from one fd to the other. 94 | Move(SpliceMove), 95 | /// Shutting down state: copy is over, some data might need to be flushed. 96 | ShuttingDown(usize), 97 | /// Done: all data was moved and was flushed, no further action can be 98 | /// taken. 99 | Done, 100 | } 101 | 102 | impl Splice { 103 | fn new() -> Result { 104 | Ok(Self::Move(SpliceMove { 105 | pipe: Pipe::new()?, 106 | processed: 0, 107 | in_pipe: 0, 108 | flush: false, 109 | })) 110 | } 111 | 112 | /// Calls internal logic depending on the current Splice state. This should 113 | /// be called for transferring data between fds and for all subsequent 114 | /// operations. 115 | fn process(&mut self, ctx: &mut Context<'_>, src: &mut T, dst: &mut T) -> Poll> 116 | where 117 | T: ZcAsyncIo, 118 | { 119 | loop { 120 | match self { 121 | Self::Move(r#move) => { 122 | let processed = ready!(r#move.copy(ctx, src, dst))?; 123 | *self = Self::ShuttingDown(processed); 124 | } 125 | Self::ShuttingDown(processed) => { 126 | if let Err(e) = ready!(Pin::new(dst).poll_shutdown(ctx)) { 127 | match e.kind() { 128 | // Ignore error if the socket is already closed. 129 | io::ErrorKind::NotConnected => (), 130 | _ => return Poll::Ready(Err(e.into())), 131 | } 132 | } 133 | 134 | let ret = Poll::Ready(Ok(*processed)); 135 | *self = Self::Done; 136 | return ret; 137 | } 138 | Self::Done => return Poll::Ready(Err(anyhow!("Splice is in \"done\" state"))), 139 | } 140 | } 141 | } 142 | } 143 | 144 | /// Hold data for the Move state of Splice. 145 | struct SpliceMove { 146 | pipe: Pipe, 147 | processed: usize, 148 | in_pipe: usize, 149 | flush: bool, 150 | } 151 | 152 | impl SpliceMove { 153 | fn copy(&mut self, ctx: &mut Context<'_>, src: &mut T, dst: &mut T) -> Poll> 154 | where 155 | T: ZcAsyncIo, 156 | { 157 | loop { 158 | while self.in_pipe == 0 { 159 | match src.poll_read_ready(ctx) { 160 | Poll::Ready(ret) => ret, 161 | Poll::Pending => { 162 | // Flush in case stream is not ready for read because 163 | // the other end of the connection is waiting for 164 | // buffered data on our end. 165 | if self.flush { 166 | ready!(Pin::new(dst).poll_flush(ctx))?; 167 | self.flush = false; 168 | } 169 | 170 | return Poll::Pending; 171 | } 172 | }?; 173 | 174 | let ret = src.try_io(tokio::io::Interest::READABLE, || { 175 | Self::splice(src.as_raw_fd(), self.pipe.write, PIPE_SIZE) 176 | }); 177 | 178 | match ret { 179 | Ok(0) => return Poll::Ready(Ok(self.processed)), 180 | Ok(moved) => self.in_pipe = moved, 181 | Err(e) => { 182 | match e.kind() { 183 | io::ErrorKind::WouldBlock => continue, 184 | io::ErrorKind::ConnectionReset 185 | | io::ErrorKind::ConnectionAborted 186 | | io::ErrorKind::BrokenPipe => return Poll::Ready(Ok(self.processed)), 187 | _ => (), 188 | } 189 | 190 | return Poll::Ready(Err(e.into())); 191 | } 192 | } 193 | } 194 | 195 | // Keep track of how much data we transferred. 196 | self.processed += self.in_pipe; 197 | 198 | // Transfer data from the pipe. 199 | while self.in_pipe > 0 { 200 | ready!(dst.poll_write_ready(ctx))?; 201 | 202 | let ret = dst.try_io(tokio::io::Interest::WRITABLE, || { 203 | Self::splice(self.pipe.read, dst.as_raw_fd(), self.in_pipe) 204 | }); 205 | 206 | match ret { 207 | Ok(moved) => { 208 | self.in_pipe -= moved; 209 | self.flush = true; 210 | } 211 | Err(e) => { 212 | match e.kind() { 213 | io::ErrorKind::WouldBlock => continue, 214 | io::ErrorKind::ConnectionReset 215 | | io::ErrorKind::ConnectionAborted 216 | | io::ErrorKind::BrokenPipe => return Poll::Ready(Ok(self.processed)), 217 | _ => (), 218 | } 219 | 220 | return Poll::Ready(Err(e.into())); 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | fn splice(src: RawFd, dst: RawFd, size: usize) -> io::Result { 228 | loop { 229 | let ret = unsafe { 230 | libc::splice( 231 | src, 232 | ptr::null_mut::(), 233 | dst, 234 | ptr::null_mut::(), 235 | size, 236 | libc::SPLICE_F_MOVE | libc::SPLICE_F_NONBLOCK, 237 | ) 238 | }; 239 | 240 | // Convert the raw result to io::Result. Both EAGAIN and EWOULDBLOCK 241 | // are converted to io::ErrorKind::WouldBlock; this extremely 242 | // important because we might get called from tokio try_io helpers 243 | // which intercept io::ErrorKind::WouldBlock and acts on the socket 244 | // readiness accordingly. 245 | match ret { 246 | x if x < 0 => { 247 | let err = Error::last_os_error(); 248 | match err.raw_os_error() { 249 | Some(e) if e == libc::EINTR => continue, 250 | _ => return Err(err), 251 | } 252 | } 253 | _ => return Ok(ret as usize), 254 | } 255 | } 256 | } 257 | } 258 | 259 | /// Represents a Linux pipe, which can be used as an unidirectional channel for 260 | /// moving data from or to an outside file descriptor. 261 | struct Pipe { 262 | read: RawFd, 263 | write: RawFd, 264 | } 265 | 266 | impl Pipe { 267 | /// Creates a non-blocking Linux pipe, see pipe(2). 268 | fn new() -> Result { 269 | let mut pipefd = mem::MaybeUninit::<[libc::c_int; 2]>::uninit(); 270 | 271 | let [read, write] = unsafe { 272 | if libc::pipe2( 273 | pipefd.as_mut_ptr() as *mut libc::c_int, 274 | libc::O_CLOEXEC | libc::O_NONBLOCK, 275 | ) < 0 276 | { 277 | bail!("Could not create pipe: {}", io::Error::last_os_error()); 278 | } 279 | 280 | pipefd.assume_init() 281 | }; 282 | 283 | unsafe { 284 | // Ignore errors here are not using the bigger buffer will work too, 285 | // just result in more syscalls. 286 | libc::fcntl(read, libc::F_SETPIPE_SZ, PIPE_SIZE); 287 | } 288 | 289 | Ok(Self { read, write }) 290 | } 291 | } 292 | 293 | impl Drop for Pipe { 294 | fn drop(&mut self) { 295 | unsafe { 296 | libc::close(self.read); 297 | libc::close(self.write); 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /test_data/example.net.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFgTCCA2mgAwIBAgIUejdZ74Jt7h7eIb9EC2ueZUdiuiswDQYJKoZIhvcNAQEL 3 | BQAwUDELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE 4 | CgwTRGVmYXVsdCBDb21wYW55IEx0ZDEMMAoGA1UEAwwDOjoxMB4XDTIzMTAzMTE3 5 | NTgzN1oXDTIzMTEzMDE3NTgzN1owUDELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERl 6 | ZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEMMAoGA1UE 7 | AwwDOjoxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0r/DB/JDB7Ok 8 | JM/bQjxryUde1RRpk5+w4nWcQ23Gml9GbdconGvKMADj89J0V5/hnAcnADMHcWnz 9 | JVfZhC8mM9FgxuJep08t6pfCQtdGTWFAzkuJI0Qz4PbUUMKS82L3rfEXvJlp7FSS 10 | wHAYqG8+wl6jRF/lF0zrKrQVFaFq62akcnVGZMTbJEv8k2mOJN1MqG6Uh+WG9CeK 11 | Ku5bApoLXKPkWqEyB/MkOV4SOH6Ga1nUHMZpJSmpTkTKJHF+ggwItHwRxrvNuW36 12 | PN4O0aicKiMyqyma9oC/fj0DpfrI3iGSPA8baj4XdLu3VZLLnINPOoIM5t+C+Ft1 13 | kjq9HRNANKUIdYGU+DWJl9fDGIeRZJvYZjOtnjlyOLIu4du/+mSwsA3Pra7ukaQB 14 | Afnb3VGkP909luqkHHYOHQKvueTU72VhLNH2TNa/hJUwvps7EAFysjCjM1Iq/ync 15 | gEz6W00VUCVdnwDCtZimWMjeeILdQQ+KNHPZYbffVxtUw1ygj9PGyw996ZAjUx0Q 16 | eq+uOotidBMien64oBWC4iNHBAGXxmRBduPEXtPWONSPa6QnNh32J0JPadzzO2d3 17 | KKxoykENUEWX+qFZXaeWP69/NlhJrIA+1VRdQiv2srv8c8kLQsgLCMbkHFyPYG+n 18 | txA6NpP+VvMbYTyYIyaBPbpzJCFppscCAwEAAaNTMFEwHQYDVR0OBBYEFPFM3dvB 19 | TqNS55LONvT71hQDphGmMB8GA1UdIwQYMBaAFPFM3dvBTqNS55LONvT71hQDphGm 20 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAIz+gWRc7hi10T27 21 | Wu0UNMKj1t3XUtAv8t+hak0Jd+b1YEcXMK9XO50JTCdMw8MSGPfCstKaPnjJzk9/ 22 | nZal/ChzmkXfYZpFjbcSnY2LKu8TsaMmXB70x6oJ+X2SCbQix8TBA8rpqx0QQzuy 23 | UxpirYjNYm3NerdLYEi5Njzc03hIoAt3fU+IOWBkPNHmzg+WSRFg1u1AJ9hy1cHc 24 | l2ugp7hv9+ysdVZNQ1tUv39mpLhHQbrF5bs4NoX0AozjCCJLlNVARKrVj1cjBBVh 25 | iqNN1murmMMiDcG7F7pdvqW6UhRMGAPaEO5qu+0vzqPbFRXPHGGTDLsvaSaNOISs 26 | t7uI7eRtOechyKaEgdxTyjH4GlFlxlEpO2Y1CvdBuQ+R43U2gBv/vHYLbAak5sdg 27 | QUG4phKrr0XF0DajziQauRVfU1Rt6WFwedMCEY05siM73IOloBo4XeWKl02VbLAX 28 | WHY3KUL603BUSmAafAaCDR1LmBjubeb2FIBTwAS046tspN721whh4DgEG32F589C 29 | EkN0cDg10qVftAdzRJ9rktWcPmZsVzxwUpgMxtJROQds9gNqf5/VdpVZQrRr+/CX 30 | hhHQ2WvCTlg6XPzWk35WzHsUMLeG6r//6R+D/CMRonBdbxlQ2twRQ4KrOzWx+8ot 31 | PC4waTDj1umGvsMN+5sg5a2/46bd 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test_data/example.net.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDSv8MH8kMHs6Qk 3 | z9tCPGvJR17VFGmTn7DidZxDbcaaX0Zt1yica8owAOPz0nRXn+GcBycAMwdxafMl 4 | V9mELyYz0WDG4l6nTy3ql8JC10ZNYUDOS4kjRDPg9tRQwpLzYvet8Re8mWnsVJLA 5 | cBiobz7CXqNEX+UXTOsqtBUVoWrrZqRydUZkxNskS/yTaY4k3UyobpSH5Yb0J4oq 6 | 7lsCmgtco+RaoTIH8yQ5XhI4foZrWdQcxmklKalORMokcX6CDAi0fBHGu825bfo8 7 | 3g7RqJwqIzKrKZr2gL9+PQOl+sjeIZI8DxtqPhd0u7dVksucg086ggzm34L4W3WS 8 | Or0dE0A0pQh1gZT4NYmX18MYh5Fkm9hmM62eOXI4si7h27/6ZLCwDc+tru6RpAEB 9 | +dvdUaQ/3T2W6qQcdg4dAq+55NTvZWEs0fZM1r+ElTC+mzsQAXKyMKMzUir/KdyA 10 | TPpbTRVQJV2fAMK1mKZYyN54gt1BD4o0c9lht99XG1TDXKCP08bLD33pkCNTHRB6 11 | r646i2J0EyJ6frigFYLiI0cEAZfGZEF248Re09Y41I9rpCc2HfYnQk9p3PM7Z3co 12 | rGjKQQ1QRZf6oVldp5Y/r382WEmsgD7VVF1CK/ayu/xzyQtCyAsIxuQcXI9gb6e3 13 | EDo2k/5W8xthPJgjJoE9unMkIWmmxwIDAQABAoICAAF0yL9eeanCddZj8fMQYXry 14 | ZCHbHRWji1XX6TIAFANkuCMHKGUtoNKEbGQhZqNlBpU3lBnowFrqF/+AjroJQggm 15 | oLa5wVys6+Ih6qTf7EDu9JiC+29ucmCnY/UrkFdf2NqZ/rIovzYeVCXslbGy6b9E 16 | ZyK6+jrB0c1+5rcPtqI/bdB1UKdemC21MV7ADSGNE+awCOpdeCQUvTF/FHWSpJR0 17 | dMikMiki5skCBjnfX0S7n1RpkeLArxuhgbdkaggyVsG51RJHTEHFPu7lETt6oPBG 18 | Q6cyHOYqIpNCjwAPw7aL4ID2ERhCqz0nv48MT3LakhVok6KUFclrMbHxIksNTdVs 19 | E5PqvYitrP/RWb7+9SzSPVPnElYdVhDnk3UKptHPqfteeOljzqwFlX+4aq2Ibd73 20 | QkKN8rz+G1AyRtGREzvp97+fAl7r2gvlzwxRW8hRvZPptlE/1+KepAISYDn62QO6 21 | fiFjwobkdmqtsvBZWz56vLNCrEiuNqrfAPwFJ6Gv6+VqAd1xsWD6+5Pef5KO0uJm 22 | Sd2J2UKMqreRtZQy9YfksT3zfiNYbDpuKic506g7LLK8TJ3uJrOHKVlTJ99mhD+u 23 | bR6n/uLYNNu03hEgHL9V8NiRPeIU0vZhkv1Clhg7FzH31hWkhnLaWg8TeJZ/W+Ki 24 | 1XP/pBSMHueI63tIjzF9AoIBAQDwtk8pDmyyd/Z+mTwwR5u50IMHHFCpEa/S0+m+ 25 | EF43urzn+JVVL+OxxeyVnMCzMMHPRugTzcO4ux9TVPkjXNnZmXUXzA7JxOrmWBGD 26 | NCf9mS+DENtVJHfhsc7D0J5lg1C6bK8j+YOKXYikpT55yPAOotz0uSqA0tzeKUaS 27 | wSSp42YrDavCXG18rulwCHDinOPGWjHGrIdpm/kaOCv9ftRwZZ1baM5lOEpmcPZs 28 | ofIjePA+70Ykro5KB8cyTZx8nMof/xycZ75RrRiu1gZJe46hrSUCUkrhpOOkVu7k 29 | njob4+SG+gP1WBZvQya01vftK3oNN2eUbqk5oV4aqoaprjKLAoIBAQDgIkn4Wmal 30 | pFY+jpN8mPYoVIDdGRCxiQIOKtmG0k37I+lHyiui64gEx93xWo8JoqmbEvXz+YZW 31 | EqecH/P6ZbeWVdkHtsQYrjvrjkOGNaJHLpmZLw5PfMwkqs8XvXbzQcWn41dhNuCn 32 | gJtP4EEfo9B+9vvkAxb+IXDoi9zQ2oOhmMT1bdGy7f7Eh4c33DdMLz+Ma6r3GekJ 33 | aFTd0h9qGok9jsoi1KLdeVy63MDLO1zytWv3lkgIMCD9qGR9StK3usC/eqtziTiU 34 | 7LAPxSzxAkAnZNWtRv2sqYHnenWV7V9wpY3Ckep0izcU44VpDSTdcbkojInDQNS8 35 | SI9aPSWAIZA1AoIBAQCYvQXT5mxAfnBxemg/tmBw8ocBzwy7ixaG76KE0LwIHrzZ 36 | cNnI+R9A4XcypAl+anTAQpzmXA1SPaIt7WA8+SDeu0oadIf5rbLjwIQ0PbcWaspE 37 | H0EdcIuFMZ8ij1DC1yU1ddtYN24pMRqQZq4S9takYuFTg98daS9u5L/8RQQvrLRa 38 | o00WgjGTCSXkyeZfVPAPH5IwH6+46piQ842uR3kjbMcBhqpYffRvtkQXQFd1/Lpt 39 | JMTcZ8qv3Omf2LFwPMjgrq8H66hPCUf9QJ8yp20k18oBBBixv0AXOnZRqLj0k5Gh 40 | uCkk1+U8al64JyOgYZMLqdlWzTUh9WvR/dx+9KLrAoIBAAK/6PBnC4NUtMonrZ+F 41 | sN9lyf7sY8wJxkCuPeTth49hLWd4D3j8Mrr8SLjBR6fymuoQHXxUv1Um/W9o5PFZ 42 | dhseCn+Rum4jFREaubFXzxnYdIHwldhby5VXkxrTdDYfxHD+1P+YUME9Tqp4MdRV 43 | iiVEmmp9rzaG6n+v15GoXJ0K5r+sDTqIuOtoTjxdM0B4A+uRFpPZeYZoBUKDZWdD 44 | 1BMxQUGUIjtdVcnlE2hq3gUMPLrY2SFglccG3dS7qkIexGaU9q2ALXWGQdKvtJFw 45 | WaEiF4z8pfgMTE+urOqd8uHK5iZQ6/NqEc7ry6MyShV9Y6wfrUD79qAos7mDroYX 46 | 41ECggEBAK1dAQB4JoVKZF7Nkx4mTO8MtWOcBx0UfaoCPqyrksIXCy9ctAXrrhLV 47 | Cj/XEIymkFt2feIhfLIiWXj6iyZrYeuVk70CeNjUM+rpEueVd2im5qRcvrwLYHYB 48 | uxJXJGe3DXisg305MrU6PpLs+3NcswkzSkDd0WWixqq4Wa7uR7RZoHrgEu6ksFxx 49 | MQ5SrDHB9us2YDPQm84arxcLdfH+xMQf99DtL+v1xSEHnqbReu+/P28byxsArW4Q 50 | towbjMB/JdY+9Z+SBjiL3v8y3YMJcOO8wwqg/zULvq4+Gx8ywWR3yL5AdXi8aXtv 51 | ZJ+1prgHK0aO2i9tS5hFVUvb5wMVabk= 52 | -----END PRIVATE KEY----- 53 | --------------------------------------------------------------------------------