From 6db5f49a59a2dba9b9310196f0ba692a5a95b604 Mon Sep 17 00:00:00 2001 From: Matthieu Olivier Date: Mon, 9 Mar 2026 13:49:06 +0100 Subject: [PATCH 01/10] chore: update dependencies --- Cargo.lock | 371 ++++++++++++++++++----------------------------------- Cargo.toml | 20 ++- src/pow.rs | 4 +- 3 files changed, 143 insertions(+), 252 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f4e1f7..3676a73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,35 +4,20 @@ # SPDX-License-Identifier: CC0-1.0 version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -45,76 +30,61 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - [[package]] name = "bitflags" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -127,21 +97,21 @@ dependencies = [ [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.31" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -149,9 +119,9 @@ dependencies = [ [[package]] name = "clap-verbosity-flag" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394" dependencies = [ "clap", "log", @@ -159,9 +129,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -171,9 +141,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -183,15 +153,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "command-group" @@ -216,9 +186,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -236,9 +206,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -246,9 +216,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -257,6 +227,16 @@ dependencies = [ "log", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -269,21 +249,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", "wasi", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "heck" version = "0.5.0" @@ -292,28 +266,28 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "jiff" -version = "0.2.3" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c163c633eb184a4ad2a5e7a5dacf12a58c830d717a7963563d4eceb4ced079f" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.3" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc3e0019b0f5f43038cf46471b1312136f29e36f54436c6042c8f155fec8789" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -322,40 +296,31 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.170" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "log" -version = "0.4.26" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miniz_oxide" -version = "0.8.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" -dependencies = [ - "adler2", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -370,37 +335,28 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.7" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -416,18 +372,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.39" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -464,9 +420,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -476,9 +432,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -487,30 +443,24 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rustc-demangle" -version = "0.1.24" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "serde" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -519,9 +469,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -530,26 +480,27 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "sossette" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", @@ -570,9 +521,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -581,11 +532,10 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", "libc", "mio", @@ -593,14 +543,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -609,15 +559,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" @@ -633,9 +583,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "winapi" @@ -660,101 +610,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 67f8355..39dc817 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,21 +4,31 @@ [package] name = "sossette" authors = ["erdnaxe "] -version = "0.1.0" -edition = "2021" +version = "0.2.0" +edition = "2024" [dependencies] anyhow = "1.0" -clap = { version = "4.4", features = ["derive", "env"] } +clap = { version = "4.5", features = ["derive", "env"] } clap-verbosity-flag = "3.0" command-group = { version = "5.0", features = ["with-tokio"] } env_logger = "0.11" log = "0.4" rand = "0.8" sha2 = "0.10" -tokio = { version = "1.43", features = ["rt-multi-thread", "io-util", "signal", "net", "time", "process", "macros"] } +tokio = { version = "1.5", features = [ + "rt-multi-thread", + "io-util", + "signal", + "net", + "time", + "process", + "macros", +] } [profile.release] -strip = true +codegen-units = 1 lto = true +opt-level = 3 panic = "abort" +strip = true diff --git a/src/pow.rs b/src/pow.rs index 307df6e..301fd88 100644 --- a/src/pow.rs +++ b/src/pow.rs @@ -54,10 +54,8 @@ pub async fn proof_of_work_prompt Date: Mon, 9 Mar 2026 15:35:18 +0100 Subject: [PATCH 02/10] feat: add support for proxy protocol v2 --- README.md | 83 ++++++++++++++++++++++ src/handler.rs | 48 ++++++++++++- src/main.rs | 11 ++- src/proxy.rs | 188 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 src/proxy.rs diff --git a/README.md b/README.md index 3d94725..47b202b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,89 @@ world ^C ``` +## PROXY protocol support + +Sossette supports the [PROXY protocol v2](https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt) to preserve client IP addresses when running behind a load balancer or reverse proxy. + +### Usage + +Enable PROXY protocol parsing with the `--proxy-protocol` flag: + +```bash +$ sossette --proxy-protocol -l 0.0.0.0:4000 cat +``` + +Or using the environment variable: + +```bash +$ WRAPPER_PROXY_PROTOCOL=true sossette -l 0.0.0.0:4000 cat +``` + +To **require** PROXY protocol headers and reject connections without them: + +```bash +$ sossette --proxy-protocol-required -l 0.0.0.0:4000 cat +``` + +### Accessing client information + +When PROXY protocol is enabled and a valid header is received, sossette: + +1. **Logs the real client IP** instead of the proxy's IP: + ``` + [2024-03-09T10:15:23Z INFO sossette] Client [::1]:55438 connected + [2024-03-09T10:15:23Z INFO sossette] Real client: 192.0.2.123:54321 (via proxy [::1]:55438) + ``` + +2. **Passes client information to the wrapped process** via environment variables: + - `CLIENT_IP`: The real client's IP address (e.g., `192.0.2.123`) + - `CLIENT_PORT`: The real client's source port (e.g., `54321`) + - `PROXY_DEST_IP`: The destination IP the client connected to + - `PROXY_DEST_PORT`: The destination port the client connected to + + Example usage in a bash script: + ```bash + #!/bin/bash + echo "Welcome! Your IP is: $CLIENT_IP:$CLIENT_PORT" + ``` + +### Load balancer configuration + +#### HAProxy + +Configure HAProxy to send PROXY protocol v2 headers: + +```haproxy +frontend tcp_front + bind *:443 + mode tcp + default_backend tcp_back + +backend tcp_back + mode tcp + server sossette 127.0.0.1:4000 send-proxy-v2 +``` + +#### nginx + +Configure nginx stream module with PROXY protocol: + +```nginx +stream { + upstream sossette { + server 127.0.0.1:4000; + } + + server { + listen 443; + proxy_pass sossette; + proxy_protocol on; + } +} +``` + +**Security note**: When using PROXY protocol, ensure that only trusted load balancers can connect to sossette (e.g., using firewall rules). Otherwise, clients could spoof their IP addresses by sending fake PROXY protocol headers. + ## Applying transformations to stdin `process_stdin` in [src/main.rs](./src/main.rs) can be easily patched to apply diff --git a/src/handler.rs b/src/handler.rs index 374e993..e2a48ea 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,15 +1,17 @@ // SPDX-FileCopyrightText: 2023-2025 erdnaxe // SPDX-License-Identifier: MIT -use crate::pow; use crate::Args; +use crate::pow; +use crate::proxy; +use std::net::SocketAddr; use std::process::Stdio; use std::time::Duration; use anyhow::{Context, Result}; use command_group::AsyncCommandGroup; -use log::debug; +use log::{debug, info, warn}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::process::Command; @@ -61,7 +63,38 @@ async fn process_stdout( /// /// Spawn one process and then spawn 3 tasks to manage input, output and /// timeout. If one of these tasks reach its end, kill the process. -pub async fn handle_client(mut socket: TcpStream, args: Args) -> Result<()> { +pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: Args) -> Result<()> { + // Parse PROXY protocol header if enabled + let proxy_info = if args.proxy_protocol || args.proxy_protocol_required { + match proxy::parse_proxy_v2_header(&mut socket).await { + Ok(info) => { + if let Some(ref proxy_info) = info { + info!( + "Real client: {}:{} (via proxy {})", + proxy_info.src_addr, proxy_info.src_port, peer_addr + ); + } else { + debug!("PROXY protocol LOCAL command (health check)"); + } + info + } + Err(e) => { + if args.proxy_protocol_required { + warn!( + "Rejecting connection from {} due to PROXY protocol error: {:?}", + peer_addr, e + ); + return Err(e); + } else { + debug!("PROXY protocol parsing failed (continuing anyway): {:?}", e); + None + } + } + } + } else { + None + }; + // Send message of the day if let Some(motd) = &args.motd { socket.write_all(motd.as_bytes()).await?; @@ -80,6 +113,15 @@ pub async fn handle_client(mut socket: TcpStream, args: Args) -> Result<()> { let mut command = Command::new(&args.command); command.args(&args.arguments); command.stdin(Stdio::piped()).stdout(Stdio::piped()); + + // Pass PROXY protocol information to child process via environment variables + if let Some(ref proxy_info) = proxy_info { + command.env("CLIENT_IP", proxy_info.src_addr.to_string()); + command.env("CLIENT_PORT", proxy_info.src_port.to_string()); + command.env("PROXY_DEST_IP", proxy_info.dst_addr.to_string()); + command.env("PROXY_DEST_PORT", proxy_info.dst_port.to_string()); + } + let mut child = command.group_spawn().context("Failed to run command")?; let child_stdin = child.inner().stdin.take().context("Failed to open stdin")?; let child_stdout = child diff --git a/src/main.rs b/src/main.rs index 047e033..2eb5316 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod handler; mod pow; +mod proxy; use anyhow::{Context, Result}; use clap::Parser; @@ -35,6 +36,14 @@ struct Args { #[arg(long, value_name = "STRING", env = "WRAPPER_POW_BACKDOOR")] pow_backdoor: Option, + /// Enable PROXY protocol v2 parsing to extract real client IP + #[arg(long, env = "WRAPPER_PROXY_PROTOCOL")] + proxy_protocol: bool, + + /// Require PROXY protocol header, reject connections without it + #[arg(long, env = "WRAPPER_PROXY_PROTOCOL_REQUIRED")] + proxy_protocol_required: bool, + #[command(flatten)] verbose: Verbosity, @@ -60,7 +69,7 @@ async fn serve(args: Args) -> Result<()> { // Spawn task to handle this client let my_args = args.clone(); tokio::spawn(async move { - match handler::handle_client(socket, my_args).await { + match handler::handle_client(socket, peer_addr, my_args).await { Ok(()) => { info!("Client {:?} disconnected", peer_addr) } diff --git a/src/proxy.rs b/src/proxy.rs new file mode 100644 index 0000000..b80d867 --- /dev/null +++ b/src/proxy.rs @@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: 2023-2026 erdnaxe +// SPDX-License-Identifier: MIT + +use anyhow::{Result, anyhow}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use tokio::io::AsyncReadExt; +use tokio::time::{Duration, timeout}; + +/// PROXY protocol v2 signature (12 bytes) +const PROXY_V2_SIGNATURE: &[u8; 12] = b"\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; + +const VERSION_MASK: u8 = 0xF0; +const COMMAND_MASK: u8 = 0x0F; +const VERSION_2: u8 = 0x20; +const CMD_LOCAL: u8 = 0x00; +const CMD_PROXY: u8 = 0x01; + +const AF_UNSPEC: u8 = 0x00; +const AF_INET: u8 = 0x10; +const AF_INET6: u8 = 0x20; +const AF_UNIX: u8 = 0x30; + +// const PROTO_UNSPEC: u8 = 0x00; +const PROTO_STREAM: u8 = 0x01; + +const MAX_PROXY_ADDR_LEN: usize = 512; +const READ_TIMEOUT: Duration = Duration::from_secs(2); + +#[derive(Debug, Clone)] +pub struct ProxyInfo { + pub src_addr: IpAddr, + pub src_port: u16, + pub dst_addr: IpAddr, + pub dst_port: u16, +} + +impl ProxyInfo { + pub fn new(src_addr: IpAddr, src_port: u16, dst_addr: IpAddr, dst_port: u16) -> Self { + Self { + src_addr, + src_port, + dst_addr, + dst_port, + } + } +} + +/// Parse PROXY protocol v2 header +pub async fn parse_proxy_v2_header( + stream: &mut R, +) -> Result> { + let mut header = [0u8; 16]; + timeout(READ_TIMEOUT, stream.read_exact(&mut header)).await??; + + // Signature check + if &header[0..12] != PROXY_V2_SIGNATURE { + return Err(anyhow!("Invalid PROXY protocol v2 signature")); + } + + let version_command = header[12]; + let family_protocol = header[13]; + let addr_len = u16::from_be_bytes([header[14], header[15]]) as usize; + + if (version_command & VERSION_MASK) != VERSION_2 { + return Err(anyhow!("Unsupported PROXY protocol version")); + } + + let command = version_command & COMMAND_MASK; + + if addr_len > MAX_PROXY_ADDR_LEN { + return Err(anyhow!("PROXY header too large: {}", addr_len)); + } + + // Handle LOCAL command + if command == CMD_LOCAL { + if addr_len > 0 { + let mut discard = vec![0u8; addr_len]; + timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; + } + return Ok(None); + } + + if command != CMD_PROXY { + return Err(anyhow!("Unsupported PROXY command: {}", command)); + } + + let family = family_protocol & 0xF0; + let protocol = family_protocol & 0x0F; + + if protocol != PROTO_STREAM { + return Err(anyhow!("Unsupported transport protocol: {}", protocol)); + } + + match family { + AF_INET => parse_ipv4(stream, addr_len).await, + AF_INET6 => parse_ipv6(stream, addr_len).await, + AF_UNSPEC => { + if addr_len > 0 { + let mut discard = vec![0u8; addr_len]; + timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; + } + Ok(None) + } + AF_UNIX => Err(anyhow!("UNIX addresses not supported")), + _ => Err(anyhow!("Unknown address family: {}", family)), + } +} + +/// Parse IPv4 address block (12 bytes) + skip TLVs +async fn parse_ipv4( + stream: &mut R, + addr_len: usize, +) -> Result> { + if addr_len < 12 { + return Err(anyhow!("IPv4 address block too short: {}", addr_len)); + } + + let mut addr = [0u8; 12]; + timeout(READ_TIMEOUT, stream.read_exact(&mut addr)).await??; + + // Skip extra TLV bytes if any + if addr_len > 12 { + let mut discard = vec![0u8; addr_len - 12]; + timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; + } + + let src_addr = Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]); + let dst_addr = Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7]); + let src_port = u16::from_be_bytes([addr[8], addr[9]]); + let dst_port = u16::from_be_bytes([addr[10], addr[11]]); + + Ok(Some(ProxyInfo::new( + IpAddr::V4(src_addr), + src_port, + IpAddr::V4(dst_addr), + dst_port, + ))) +} + +/// Parse IPv6 address block (36 bytes) + skip TLVs +async fn parse_ipv6( + stream: &mut R, + addr_len: usize, +) -> Result> { + if addr_len < 36 { + return Err(anyhow!("IPv6 address block too short: {}", addr_len)); + } + + let mut addr = [0u8; 36]; + timeout(READ_TIMEOUT, stream.read_exact(&mut addr)).await??; + + if addr_len > 36 { + let mut discard = vec![0u8; addr_len - 36]; + timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; + } + + let src_addr = Ipv6Addr::new( + u16::from_be_bytes([addr[0], addr[1]]), + u16::from_be_bytes([addr[2], addr[3]]), + u16::from_be_bytes([addr[4], addr[5]]), + u16::from_be_bytes([addr[6], addr[7]]), + u16::from_be_bytes([addr[8], addr[9]]), + u16::from_be_bytes([addr[10], addr[11]]), + u16::from_be_bytes([addr[12], addr[13]]), + u16::from_be_bytes([addr[14], addr[15]]), + ); + + let dst_addr = Ipv6Addr::new( + u16::from_be_bytes([addr[16], addr[17]]), + u16::from_be_bytes([addr[18], addr[19]]), + u16::from_be_bytes([addr[20], addr[21]]), + u16::from_be_bytes([addr[22], addr[23]]), + u16::from_be_bytes([addr[24], addr[25]]), + u16::from_be_bytes([addr[26], addr[27]]), + u16::from_be_bytes([addr[28], addr[29]]), + u16::from_be_bytes([addr[30], addr[31]]), + ); + + let src_port = u16::from_be_bytes([addr[32], addr[33]]); + let dst_port = u16::from_be_bytes([addr[34], addr[35]]); + + Ok(Some(ProxyInfo::new( + IpAddr::V6(src_addr), + src_port, + IpAddr::V6(dst_addr), + dst_port, + ))) +} From 5d70200d9a949c54288849d49c7e25254605e939 Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:17:43 +0100 Subject: [PATCH 03/10] refactor: make ppv2 required if specified --- README.md | 8 +------- src/handler.rs | 17 ++++++----------- src/main.rs | 6 +----- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 47b202b..593fb24 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Sossette supports the [PROXY protocol v2](https://github.com/haproxy/haproxy/blo ### Usage -Enable PROXY protocol parsing with the `--proxy-protocol` flag: +Enable PROXY protocol v2 with the `--proxy-protocol` flag. When enabled, a valid PROXY protocol v2 header is **required** and connections without one are rejected: ```bash $ sossette --proxy-protocol -l 0.0.0.0:4000 cat @@ -84,12 +84,6 @@ Or using the environment variable: $ WRAPPER_PROXY_PROTOCOL=true sossette -l 0.0.0.0:4000 cat ``` -To **require** PROXY protocol headers and reject connections without them: - -```bash -$ sossette --proxy-protocol-required -l 0.0.0.0:4000 cat -``` - ### Accessing client information When PROXY protocol is enabled and a valid header is received, sossette: diff --git a/src/handler.rs b/src/handler.rs index e2a48ea..429e15b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -65,7 +65,7 @@ async fn process_stdout( /// timeout. If one of these tasks reach its end, kill the process. pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: Args) -> Result<()> { // Parse PROXY protocol header if enabled - let proxy_info = if args.proxy_protocol || args.proxy_protocol_required { + let proxy_info = if args.proxy_protocol { match proxy::parse_proxy_v2_header(&mut socket).await { Ok(info) => { if let Some(ref proxy_info) = info { @@ -79,16 +79,11 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A info } Err(e) => { - if args.proxy_protocol_required { - warn!( - "Rejecting connection from {} due to PROXY protocol error: {:?}", - peer_addr, e - ); - return Err(e); - } else { - debug!("PROXY protocol parsing failed (continuing anyway): {:?}", e); - None - } + warn!( + "Rejecting connection from {} due to PROXY protocol error: {:?}", + peer_addr, e + ); + return Err(e); } } } else { diff --git a/src/main.rs b/src/main.rs index 2eb5316..5a2e901 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,14 +36,10 @@ struct Args { #[arg(long, value_name = "STRING", env = "WRAPPER_POW_BACKDOOR")] pow_backdoor: Option, - /// Enable PROXY protocol v2 parsing to extract real client IP + /// Require PROXY protocol v2 header, reject connections without it #[arg(long, env = "WRAPPER_PROXY_PROTOCOL")] proxy_protocol: bool, - /// Require PROXY protocol header, reject connections without it - #[arg(long, env = "WRAPPER_PROXY_PROTOCOL_REQUIRED")] - proxy_protocol_required: bool, - #[command(flatten)] verbose: Verbosity, From 7d8f3ac80fada73354bc2c68a8261876e70dd2b8 Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:30:07 +0100 Subject: [PATCH 04/10] fix: clippy recommandations --- src/handler.rs | 20 +++++++---- src/main.rs | 14 ++++---- src/pow.rs | 91 +++++++++++++++++++++++++++++--------------------- src/proxy.rs | 25 ++++++++------ 4 files changed, 88 insertions(+), 62 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 429e15b..fb01055 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -29,13 +29,16 @@ async fn process_stdin( if n == 0 { return Ok(()); // socket closed } - if in_buf[0] == 3 { + if in_buf.first() == Some(&3) { debug!("Client sent Ctrl-C"); return Ok(()); } - debug!("Writting to stdin: {:?}", &in_buf[0..n]); + let data = in_buf + .get(..n) + .context("stdin read index out of bounds")?; + debug!("Writting to stdin: {data:?}"); child_stdin - .write_all(&in_buf[0..n]) + .write_all(data) .await .context("Failed to write to stdin")?; } @@ -52,8 +55,12 @@ async fn process_stdout( if n == 0 { return Ok(()); // process closed } + let data = out_buf + .get(..n) + .context("stdout read index out of bounds")?; + debug!("Reading from stdout: {data:?}"); socket - .write_all(&out_buf[0..n]) + .write_all(data) .await .context("Failed to write to socket")?; } @@ -80,8 +87,7 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A } Err(e) => { warn!( - "Rejecting connection from {} due to PROXY protocol error: {:?}", - peer_addr, e + "Rejecting connection from {peer_addr} due to PROXY protocol error: {e:?}" ); return Err(e); } @@ -98,7 +104,7 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A // Proof-of-work prompt if args.pow > 0 { - let valid = pow::proof_of_work_prompt(&mut socket, args.pow, &args.pow_backdoor).await?; + let valid = pow::proof_of_work_prompt(&mut socket, args.pow, args.pow_backdoor.as_ref()).await?; if !valid { return Ok(()); } diff --git a/src/main.rs b/src/main.rs index 5a2e901..5b77351 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,22 +60,22 @@ async fn serve(args: Args) -> Result<()> { // See https://docs.rs/tokio/latest/tokio/net/struct.TcpListener.html#errors match listener.accept().await { Ok((socket, peer_addr)) => { - info!("Client {:?} connected", peer_addr); + info!("Client {peer_addr:?} connected"); // Spawn task to handle this client let my_args = args.clone(); tokio::spawn(async move { match handler::handle_client(socket, peer_addr, my_args).await { Ok(()) => { - info!("Client {:?} disconnected", peer_addr) + info!("Client {peer_addr:?} disconnected"); } Err(e) => { - warn!("Handling client {:?} failed: {:?}", peer_addr, e) + warn!("Handling client {peer_addr:?} failed: {e:?}"); } } }); } - Err(e) => warn!("Unable to accept client: {:?}", e), + Err(e) => warn!("Unable to accept client: {e:?}"), } } } @@ -92,14 +92,14 @@ async fn main() { tokio::spawn(async move { match serve(args).await { Ok(()) => info!("Server stopped gracefully"), - Err(e) => error!("Server stopped due to an error: {:?}", e), + Err(e) => error!("Server stopped due to an error: {e:?}"), } }); match tokio::signal::ctrl_c().await { Ok(()) => {} Err(err) => { - warn!("Unable to listen for shutdown signal: {}", err); + warn!("Unable to listen for shutdown signal: {err}"); } } } @@ -109,6 +109,6 @@ mod tests { #[test] fn verify_cli() { use clap::CommandFactory; - crate::Args::command().debug_assert() + crate::Args::command().debug_assert(); } } diff --git a/src/pow.rs b/src/pow.rs index 301fd88..20a3eee 100644 --- a/src/pow.rs +++ b/src/pow.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2023-2025 erdnaxe // SPDX-License-Identifier: MIT -use anyhow::Result; +use anyhow::{Context, Result}; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use sha2::{Digest, Sha256}; @@ -17,7 +17,7 @@ More details can be found on .\r\n"; pub async fn proof_of_work_prompt( socket: &mut S, difficulty: u32, - backdoor: &Option, + backdoor: Option<&String>, ) -> Result { // Generate prefix using OS random let prefix: [u8; 16] = thread_rng() @@ -26,63 +26,78 @@ pub async fn proof_of_work_prompt>() .as_slice() .try_into() - .unwrap(); + .context("Failed to generate random prefix")?; // Prompt user socket.write_all(POW_HEADER_MESSAGE).await?; let prompt = format!("Please provide an ASCII printable string S such that SHA256({} || S) starts with {} bits equal to 0 (the string concatenation is denoted ||): ", String::from_utf8(prefix.into())?, difficulty); socket.write_all(prompt.as_bytes()).await?; - let mut buf = [0; 256]; - let mut buf_n = 0; + let mut buf = [0u8; 256]; + let mut buf_n: usize = 0; while buf_n < 256 { - let n = socket.read(&mut buf[buf_n..buf_n + 1]).await?; - if n == 0 || buf[buf_n] == b'\x03' { - return Ok(false); // socket closed or Ctrl-C + let byte = buf + .get_mut(buf_n..=buf_n) + .context("read index out of bounds")?; + let n = socket.read(byte).await?; + if n == 0 { + return Ok(false); // socket closed } - if buf[buf_n] == b'\0' || buf[buf_n] == b'\n' { + let current = *buf.get(buf_n).context("index out of bounds")?; + if current == b'\x03' { + return Ok(false); // Ctrl-C + } + if current == b'\0' || current == b'\n' { break; // telnet uses \r\0, netcat \r\n } - if buf[buf_n] >= 127 || buf[buf_n] < 32 { + if !(32..127).contains(¤t) { continue; // ignore non ascii printable } - buf_n += n; + buf_n = buf_n.checked_add(n).context("buffer index overflow")?; } - while buf_n > 0 - && (buf[buf_n - 1] == b'\n' || buf[buf_n - 1] == b'\r' || buf[buf_n - 1] == b'\0') - { - buf_n -= 1; // trim input + + // Trim trailing carriage return + if buf_n > 0 { + let last = *buf.get(buf_n.checked_sub(1).context("underflow")?).context("index out of bounds")?; + if last == b'\r' { + buf_n = buf_n.checked_sub(1).context("underflow")?; + } } - // Backdoor for staff testing - if let Some(backdoor_str) = backdoor && backdoor_str.as_bytes() == &buf[..buf_n] { + // Get the user input as a slice + let suffix = buf.get(..buf_n).context("slice out of bounds")?; + + // Check backdoor + if let Some(bd) = backdoor && suffix == bd.as_bytes() { return Ok(true); } - // Compute hash + // Verify proof of work let mut hasher = Sha256::new(); hasher.update(prefix); - hasher.update(&buf[..buf_n]); - let hash: [u8; 32] = hasher.finalize().into(); + hasher.update(suffix); + let hash = hasher.finalize(); + Ok(check_leading_zeros(&hash, difficulty)) +} - // Count zeros - let mut measured_difficulty = 0; - for hash_byte in hash.iter() { - if *hash_byte == 0 { - measured_difficulty += 8; +/// Check that the hash starts with at least `difficulty` zero bits +fn check_leading_zeros(hash: &[u8], difficulty: u32) -> bool { + let mut remaining = difficulty; + for &byte in hash { + if remaining == 0 { + return true; + } + if remaining >= 8 { + if byte != 0 { + return false; + } + remaining = remaining.saturating_sub(8); } else { - measured_difficulty += hash_byte.leading_zeros(); - break; + // Check the top `remaining` bits of this byte + let mask = 0xFF_u8 + .checked_shl(8_u32.saturating_sub(remaining)) + .unwrap_or(0); + return byte & mask == 0; } } - - if measured_difficulty < difficulty { - let message = format!( - "Wrong proof-of-work, hash starts with only {measured_difficulty} bits equal to 0.\r\n" - ); - socket.write_all(message.as_bytes()).await?; - Ok(false) - } else { - socket.write_all(b"Thank you for solving our proof-of-work, we hope you had a great time! Launching challenge...\r\n\r\n").await?; - Ok(true) - } + remaining == 0 } diff --git a/src/proxy.rs b/src/proxy.rs index b80d867..ce8d8f5 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -35,7 +35,7 @@ pub struct ProxyInfo { } impl ProxyInfo { - pub fn new(src_addr: IpAddr, src_port: u16, dst_addr: IpAddr, dst_port: u16) -> Self { + pub const fn new(src_addr: IpAddr, src_port: u16, dst_addr: IpAddr, dst_port: u16) -> Self { Self { src_addr, src_port, @@ -68,7 +68,7 @@ pub async fn parse_proxy_v2_header( let command = version_command & COMMAND_MASK; if addr_len > MAX_PROXY_ADDR_LEN { - return Err(anyhow!("PROXY header too large: {}", addr_len)); + return Err(anyhow!("PROXY header too large: {addr_len}")); } // Handle LOCAL command @@ -81,14 +81,14 @@ pub async fn parse_proxy_v2_header( } if command != CMD_PROXY { - return Err(anyhow!("Unsupported PROXY command: {}", command)); + return Err(anyhow!("Unsupported PROXY command: {command}")); } let family = family_protocol & 0xF0; let protocol = family_protocol & 0x0F; if protocol != PROTO_STREAM { - return Err(anyhow!("Unsupported transport protocol: {}", protocol)); + return Err(anyhow!("Unsupported transport protocol: {protocol}")); } match family { @@ -102,7 +102,7 @@ pub async fn parse_proxy_v2_header( Ok(None) } AF_UNIX => Err(anyhow!("UNIX addresses not supported")), - _ => Err(anyhow!("Unknown address family: {}", family)), + _ => Err(anyhow!("Unknown address family: {family}")), } } @@ -112,15 +112,17 @@ async fn parse_ipv4( addr_len: usize, ) -> Result> { if addr_len < 12 { - return Err(anyhow!("IPv4 address block too short: {}", addr_len)); + return Err(anyhow!("IPv4 address block too short: {addr_len}")); } let mut addr = [0u8; 12]; timeout(READ_TIMEOUT, stream.read_exact(&mut addr)).await??; - // Skip extra TLV bytes if any if addr_len > 12 { - let mut discard = vec![0u8; addr_len - 12]; + let tlv_len = addr_len + .checked_sub(12) + .ok_or_else(|| anyhow!("IPv4 address length underflow"))?; + let mut discard = vec![0u8; tlv_len]; timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; } @@ -143,14 +145,17 @@ async fn parse_ipv6( addr_len: usize, ) -> Result> { if addr_len < 36 { - return Err(anyhow!("IPv6 address block too short: {}", addr_len)); + return Err(anyhow!("IPv6 address block too short: {addr_len}")); } let mut addr = [0u8; 36]; timeout(READ_TIMEOUT, stream.read_exact(&mut addr)).await??; if addr_len > 36 { - let mut discard = vec![0u8; addr_len - 36]; + let tlv_len = addr_len + .checked_sub(36) + .ok_or_else(|| anyhow!("IPv6 address length underflow"))?; + let mut discard = vec![0u8; tlv_len]; timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; } From 0e362f38c24c143bd2b1b623c76af4936c25f971 Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:32:46 +0100 Subject: [PATCH 05/10] chore: bump rand to v0.10.0 --- Cargo.lock | 298 +++++++++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- src/pow.rs | 6 +- 3 files changed, 270 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3676a73..d4565c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + [[package]] name = "clap" version = "4.5.60" @@ -184,6 +195,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -227,6 +247,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -237,6 +263,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "generic-array" version = "0.14.7" @@ -249,27 +281,69 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "jiff" version = "0.2.23" @@ -294,6 +368,12 @@ dependencies = [ "syn", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" @@ -362,12 +442,13 @@ dependencies = [ ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "zerocopy", + "proc-macro2", + "syn", ] [[package]] @@ -389,34 +470,27 @@ dependencies = [ ] [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rand" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom", "rand_core", ] [[package]] name = "rand_core" -version = "0.6.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "regex" @@ -447,6 +521,21 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -467,6 +556,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha2" version = "0.10.9" @@ -474,7 +576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -569,6 +671,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -587,6 +695,58 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "winapi" version = "0.3.9" @@ -625,21 +785,95 @@ dependencies = [ ] [[package]] -name = "zerocopy" -version = "0.8.41" +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ - "zerocopy-derive", + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", ] [[package]] -name = "zerocopy-derive" -version = "0.8.41" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ + "anyhow", + "prettyplease", "proc-macro2", "quote", "syn", + "wit-bindgen-core", + "wit-bindgen-rust", ] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 39dc817..4c778f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ clap-verbosity-flag = "3.0" command-group = { version = "5.0", features = ["with-tokio"] } env_logger = "0.11" log = "0.4" -rand = "0.8" +rand = "0.10" sha2 = "0.10" tokio = { version = "1.5", features = [ "rt-multi-thread", diff --git a/src/pow.rs b/src/pow.rs index 20a3eee..3dad891 100644 --- a/src/pow.rs +++ b/src/pow.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: MIT use anyhow::{Context, Result}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; +use rand::RngExt; +use rand::distr::Alphanumeric; use sha2::{Digest, Sha256}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -20,7 +20,7 @@ pub async fn proof_of_work_prompt, ) -> Result { // Generate prefix using OS random - let prefix: [u8; 16] = thread_rng() + let prefix: [u8; 16] = rand::rng() .sample_iter(Alphanumeric) .take(16) .collect::>() From 68637e4e7a6f28497b2bf3eedb638e666a8a617a Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:34:47 +0100 Subject: [PATCH 06/10] update cargo for size optimization --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4c778f4..d6a225b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,6 @@ tokio = { version = "1.5", features = [ [profile.release] codegen-units = 1 lto = true -opt-level = 3 +opt-level = "z" panic = "abort" strip = true From a7e666c59ef3abd65b4f1634e7a9055891174637 Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:04:53 +0100 Subject: [PATCH 07/10] fix: proxy v2 check local WIP --- src/handler.rs | 98 +++++++++++++++++++++++++++++++------------------- src/pow.rs | 19 ++++++---- src/proxy.rs | 22 +++++++----- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index fb01055..a90e805 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -18,6 +18,8 @@ use tokio::process::Command; use tokio::task::JoinSet; use tokio::time::sleep; +const LOCAL_HEALTHCHECK_TIMEOUT: Duration = Duration::from_secs(1); + /// Handle message exchange from TCP socket to process stdin async fn process_stdin( mut socket: R, @@ -29,13 +31,7 @@ async fn process_stdin( if n == 0 { return Ok(()); // socket closed } - if in_buf.first() == Some(&3) { - debug!("Client sent Ctrl-C"); - return Ok(()); - } - let data = in_buf - .get(..n) - .context("stdin read index out of bounds")?; + let data = in_buf.get(..n).context("stdin read index out of bounds")?; debug!("Writting to stdin: {data:?}"); child_stdin .write_all(data) @@ -71,24 +67,25 @@ async fn process_stdout( /// Spawn one process and then spawn 3 tasks to manage input, output and /// timeout. If one of these tasks reach its end, kill the process. pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: Args) -> Result<()> { + let mut is_local_healthcheck = false; + // Parse PROXY protocol header if enabled let proxy_info = if args.proxy_protocol { match proxy::parse_proxy_v2_header(&mut socket).await { - Ok(info) => { - if let Some(ref proxy_info) = info { - info!( - "Real client: {}:{} (via proxy {})", - proxy_info.src_addr, proxy_info.src_port, peer_addr - ); - } else { - debug!("PROXY protocol LOCAL command (health check)"); - } - info + Ok(proxy::ProxyHeader::Proxied(info)) => { + info!( + "Real client: {}:{} (via proxy {})", + info.src_addr, info.src_port, peer_addr + ); + Some(info) + } + Ok(proxy::ProxyHeader::Local) => { + is_local_healthcheck = true; + debug!("PROXY protocol LOCAL command (health check)"); + None } Err(e) => { - warn!( - "Rejecting connection from {peer_addr} due to PROXY protocol error: {e:?}" - ); + warn!("Rejecting connection from {peer_addr} due to PROXY protocol error: {e:?}"); return Err(e); } } @@ -96,17 +93,23 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A None }; - // Send message of the day - if let Some(motd) = &args.motd { - socket.write_all(motd.as_bytes()).await?; - socket.write_all(b"\r\n").await?; - } + // Wrapper prelude is skipped for HAProxy LOCAL health checks so they can + // talk directly to the wrapped binary. + if !is_local_healthcheck { + // MOTD + if let Some(motd) = &args.motd { + socket.write_all(motd.as_bytes()).await?; + socket.write_all(b"\r\n").await?; + } - // Proof-of-work prompt - if args.pow > 0 { - let valid = pow::proof_of_work_prompt(&mut socket, args.pow, args.pow_backdoor.as_ref()).await?; - if !valid { - return Ok(()); + // Proof-of-work + if args.pow > 0 { + let valid = + pow::proof_of_work_prompt(&mut socket, args.pow, args.pow_backdoor.as_ref()) + .await?; + if !valid { + return Ok(()); + } } } @@ -115,7 +118,6 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A command.args(&args.arguments); command.stdin(Stdio::piped()).stdout(Stdio::piped()); - // Pass PROXY protocol information to child process via environment variables if let Some(ref proxy_info) = proxy_info { command.env("CLIENT_IP", proxy_info.src_addr.to_string()); command.env("CLIENT_PORT", proxy_info.src_port.to_string()); @@ -124,6 +126,7 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A } let mut child = command.group_spawn().context("Failed to run command")?; + let child_stdin = child.inner().stdin.take().context("Failed to open stdin")?; let child_stdout = child .inner() @@ -131,22 +134,43 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A .take() .context("Failed to open stdout")?; - // Start tasks - let mut set = JoinSet::new(); + // Split socket let (read_half, write_half) = socket.into_split(); + + let mut set = JoinSet::new(); + set.spawn(async move { process_stdin(read_half, child_stdin).await }); + set.spawn(async move { process_stdout(write_half, child_stdout).await }); - if let Some(timeout) = args.timeout { + + let session_timeout = if is_local_healthcheck { + Some(match args.timeout { + Some(timeout) => Duration::from_secs(timeout).min(LOCAL_HEALTHCHECK_TIMEOUT), + None => LOCAL_HEALTHCHECK_TIMEOUT, + }) + } else { + args.timeout.map(Duration::from_secs) + }; + + if let Some(timeout) = session_timeout { set.spawn(async move { - sleep(Duration::from_secs(timeout)).await; + sleep(timeout).await; debug!("Timeout reached"); Ok(()) }); } - // If one task exits, drop the others - // Child group should always be killed before dropping child handle. + // Wait for first task to finish let res = set.join_next().await; + + // Cancel remaining tasks immediately + set.abort_all(); + + // Kill the process group child.kill().await.context("Failed to kill process group")?; + + // Await child to avoid zombie process + let _ = child.wait().await; + res.unwrap_or(Ok(Ok(())))? } diff --git a/src/pow.rs b/src/pow.rs index 3dad891..253a4a6 100644 --- a/src/pow.rs +++ b/src/pow.rs @@ -30,7 +30,11 @@ pub async fn proof_of_work_prompt 0 { - let last = *buf.get(buf_n.checked_sub(1).context("underflow")?).context("index out of bounds")?; + let last = *buf + .get(buf_n.checked_sub(1).context("underflow")?) + .context("index out of bounds")?; if last == b'\r' { buf_n = buf_n.checked_sub(1).context("underflow")?; } @@ -67,8 +70,10 @@ pub async fn proof_of_work_prompt( - stream: &mut R, -) -> Result> { +pub async fn parse_proxy_v2_header(stream: &mut R) -> Result { let mut header = [0u8; 16]; timeout(READ_TIMEOUT, stream.read_exact(&mut header)).await??; @@ -77,7 +81,7 @@ pub async fn parse_proxy_v2_header( let mut discard = vec![0u8; addr_len]; timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; } - return Ok(None); + return Ok(ProxyHeader::Local); } if command != CMD_PROXY { @@ -99,7 +103,7 @@ pub async fn parse_proxy_v2_header( let mut discard = vec![0u8; addr_len]; timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; } - Ok(None) + Ok(ProxyHeader::Local) } AF_UNIX => Err(anyhow!("UNIX addresses not supported")), _ => Err(anyhow!("Unknown address family: {family}")), @@ -110,7 +114,7 @@ pub async fn parse_proxy_v2_header( async fn parse_ipv4( stream: &mut R, addr_len: usize, -) -> Result> { +) -> Result { if addr_len < 12 { return Err(anyhow!("IPv4 address block too short: {addr_len}")); } @@ -131,7 +135,7 @@ async fn parse_ipv4( let src_port = u16::from_be_bytes([addr[8], addr[9]]); let dst_port = u16::from_be_bytes([addr[10], addr[11]]); - Ok(Some(ProxyInfo::new( + Ok(ProxyHeader::Proxied(ProxyInfo::new( IpAddr::V4(src_addr), src_port, IpAddr::V4(dst_addr), @@ -143,7 +147,7 @@ async fn parse_ipv4( async fn parse_ipv6( stream: &mut R, addr_len: usize, -) -> Result> { +) -> Result { if addr_len < 36 { return Err(anyhow!("IPv6 address block too short: {addr_len}")); } @@ -184,7 +188,7 @@ async fn parse_ipv6( let src_port = u16::from_be_bytes([addr[32], addr[33]]); let dst_port = u16::from_be_bytes([addr[34], addr[35]]); - Ok(Some(ProxyInfo::new( + Ok(ProxyHeader::Proxied(ProxyInfo::new( IpAddr::V6(src_addr), src_port, IpAddr::V6(dst_addr), From 35589e5f2efb206a5134346c4182b8364ee27b0d Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:25:00 +0100 Subject: [PATCH 08/10] cleanup ppv2 --- Cargo.toml | 32 +++++ src/handler.rs | 54 +++----- src/main.rs | 8 +- src/proxy.rs | 355 ++++++++++++++++++++++++++++++++++++------------- 4 files changed, 322 insertions(+), 127 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d6a225b..c617ec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,38 @@ tokio = { version = "1.5", features = [ "macros", ] } +[lints.rust] +arithmetic_overflow = { level = "deny", priority = -1 } + +[lints.clippy] +pedantic = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +missing-errors-doc = "allow" + +indexing_slicing = { level = "deny", priority = -1 } +fallible_impl_from = { level = "deny", priority = -1 } +wildcard_enum_match_arm = { level = "deny", priority = -1 } +unneeded_field_pattern = { level = "deny", priority = -1 } +fn_params_excessive_bools = { level = "deny", priority = -1 } +must_use_candidate = { level = "deny", priority = -1 } +checked_conversions = { level = "deny", priority = -1 } +cast_possible_truncation = { level = "deny", priority = -1 } +cast_sign_loss = { level = "deny", priority = -1 } +cast_possible_wrap = { level = "deny", priority = -1 } +cast_precision_loss = { level = "deny", priority = -1 } +integer_division = { level = "deny", priority = -1 } +arithmetic_side_effects = { level = "deny", priority = -1 } +unchecked_time_subtraction = { level = "deny", priority = -1 } +unwrap_used = "warn" +expect_used = "warn" +panicking_unwrap = { level = "deny", priority = -1 } +option_env_unwrap = { level = "deny", priority = -1 } +join_absolute_paths = { level = "deny", priority = -1 } +serde_api_misuse = { level = "deny", priority = -1 } +uninit_vec = { level = "deny", priority = -1 } +transmute_ptr_to_ref = { level = "deny", priority = -1 } +transmute_undefined_repr = { level = "deny", priority = -1 } + [profile.release] codegen-units = 1 lto = true diff --git a/src/handler.rs b/src/handler.rs index a90e805..6344150 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -18,8 +18,6 @@ use tokio::process::Command; use tokio::task::JoinSet; use tokio::time::sleep; -const LOCAL_HEALTHCHECK_TIMEOUT: Duration = Duration::from_secs(1); - /// Handle message exchange from TCP socket to process stdin async fn process_stdin( mut socket: R, @@ -66,22 +64,23 @@ async fn process_stdout( /// /// Spawn one process and then spawn 3 tasks to manage input, output and /// timeout. If one of these tasks reach its end, kill the process. -pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: Args) -> Result<()> { - let mut is_local_healthcheck = false; - +pub async fn handle_client( + mut socket: TcpStream, + peer_addr: SocketAddr, + args: Args, +) -> Result> { // Parse PROXY protocol header if enabled let proxy_info = if args.proxy_protocol { match proxy::parse_proxy_v2_header(&mut socket).await { Ok(proxy::ProxyHeader::Proxied(info)) => { info!( - "Real client: {}:{} (via proxy {})", + "Client: {}:{} (via proxy {}) connected", info.src_addr, info.src_port, peer_addr ); Some(info) } Ok(proxy::ProxyHeader::Local) => { - is_local_healthcheck = true; - debug!("PROXY protocol LOCAL command (health check)"); + debug!("PROXY protocol LOCAL command"); None } Err(e) => { @@ -93,23 +92,18 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A None }; - // Wrapper prelude is skipped for HAProxy LOCAL health checks so they can - // talk directly to the wrapped binary. - if !is_local_healthcheck { - // MOTD - if let Some(motd) = &args.motd { - socket.write_all(motd.as_bytes()).await?; - socket.write_all(b"\r\n").await?; - } + // MOTD + if let Some(motd) = &args.motd { + socket.write_all(motd.as_bytes()).await?; + socket.write_all(b"\r\n").await?; + } - // Proof-of-work - if args.pow > 0 { - let valid = - pow::proof_of_work_prompt(&mut socket, args.pow, args.pow_backdoor.as_ref()) - .await?; - if !valid { - return Ok(()); - } + // Proof-of-work + if args.pow > 0 { + let valid = + pow::proof_of_work_prompt(&mut socket, args.pow, args.pow_backdoor.as_ref()).await?; + if !valid { + return Ok(proxy_info); } } @@ -143,14 +137,7 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A set.spawn(async move { process_stdout(write_half, child_stdout).await }); - let session_timeout = if is_local_healthcheck { - Some(match args.timeout { - Some(timeout) => Duration::from_secs(timeout).min(LOCAL_HEALTHCHECK_TIMEOUT), - None => LOCAL_HEALTHCHECK_TIMEOUT, - }) - } else { - args.timeout.map(Duration::from_secs) - }; + let session_timeout = args.timeout.map(Duration::from_secs); if let Some(timeout) = session_timeout { set.spawn(async move { @@ -172,5 +159,6 @@ pub async fn handle_client(mut socket: TcpStream, peer_addr: SocketAddr, args: A // Await child to avoid zombie process let _ = child.wait().await; - res.unwrap_or(Ok(Ok(())))? + res.unwrap_or(Ok(Ok(())))??; + Ok(proxy_info) } diff --git a/src/main.rs b/src/main.rs index 5b77351..6021421 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,7 +66,13 @@ async fn serve(args: Args) -> Result<()> { let my_args = args.clone(); tokio::spawn(async move { match handler::handle_client(socket, peer_addr, my_args).await { - Ok(()) => { + Ok(Some(proxy_info)) => { + info!( + "Client: {}:{} (via proxy {}) disconnected", + proxy_info.src_addr, proxy_info.src_port, peer_addr + ); + } + Ok(None) => { info!("Client {peer_addr:?} disconnected"); } Err(e) => { diff --git a/src/proxy.rs b/src/proxy.rs index f69173f..70f1577 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -3,25 +3,22 @@ use anyhow::{Result, anyhow}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use tokio::io::AsyncReadExt; +use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::time::{Duration, timeout}; /// PROXY protocol v2 signature (12 bytes) -const PROXY_V2_SIGNATURE: &[u8; 12] = b"\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; +const PROXY_V2_SIGNATURE: [u8; 12] = [ + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, +]; const VERSION_MASK: u8 = 0xF0; -const COMMAND_MASK: u8 = 0x0F; const VERSION_2: u8 = 0x20; -const CMD_LOCAL: u8 = 0x00; -const CMD_PROXY: u8 = 0x01; +const LOW_NIBBLE_MASK: u8 = 0x0F; +const HIGH_NIBBLE_MASK: u8 = 0xF0; -const AF_UNSPEC: u8 = 0x00; -const AF_INET: u8 = 0x10; -const AF_INET6: u8 = 0x20; -const AF_UNIX: u8 = 0x30; - -// const PROTO_UNSPEC: u8 = 0x00; -const PROTO_STREAM: u8 = 0x01; +const PROXY_V2_HEADER_LEN: usize = 16; +const IPV4_BLOCK_LEN: usize = 12; +const IPV6_BLOCK_LEN: usize = 36; const MAX_PROXY_ADDR_LEN: usize = 512; const READ_TIMEOUT: Duration = Duration::from_secs(2); @@ -51,83 +48,141 @@ pub enum ProxyHeader { Proxied(ProxyInfo), } -/// Parse PROXY protocol v2 header -pub async fn parse_proxy_v2_header(stream: &mut R) -> Result { - let mut header = [0u8; 16]; - timeout(READ_TIMEOUT, stream.read_exact(&mut header)).await??; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Command { + Local, + Proxy, +} - // Signature check - if &header[0..12] != PROXY_V2_SIGNATURE { - return Err(anyhow!("Invalid PROXY protocol v2 signature")); - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AddressFamily { + Unspec, + Inet, + Inet6, + Unix, +} - let version_command = header[12]; - let family_protocol = header[13]; - let addr_len = u16::from_be_bytes([header[14], header[15]]) as usize; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TransportProtocol { + Unspec, + Stream, + Datagram, +} - if (version_command & VERSION_MASK) != VERSION_2 { +fn parse_command(version_command: u8) -> Result { + if version_command & VERSION_MASK != VERSION_2 { return Err(anyhow!("Unsupported PROXY protocol version")); } - let command = version_command & COMMAND_MASK; + match version_command & LOW_NIBBLE_MASK { + 0x00 => Ok(Command::Local), + 0x01 => Ok(Command::Proxy), + command => Err(anyhow!("Unsupported PROXY command: {command}")), + } +} + +fn parse_family_protocol(family_protocol: u8) -> Result<(AddressFamily, TransportProtocol)> { + let family = match family_protocol & HIGH_NIBBLE_MASK { + 0x00 => AddressFamily::Unspec, + 0x10 => AddressFamily::Inet, + 0x20 => AddressFamily::Inet6, + 0x30 => AddressFamily::Unix, + family => return Err(anyhow!("Unknown address family: {family}")), + }; + + let protocol = match family_protocol & LOW_NIBBLE_MASK { + 0x00 => TransportProtocol::Unspec, + 0x01 => TransportProtocol::Stream, + 0x02 => TransportProtocol::Datagram, + protocol => return Err(anyhow!("Unknown transport protocol: {protocol}")), + }; + + Ok((family, protocol)) +} + +async fn read_exact_with_timeout( + stream: &mut R, + buf: &mut [u8], +) -> Result<()> { + timeout(READ_TIMEOUT, stream.read_exact(buf)).await??; + Ok(()) +} + +async fn drain_bytes(stream: &mut R, mut len: usize) -> Result<()> { + let mut scratch = [0u8; 256]; + + while len > 0 { + let chunk_len = len.min(scratch.len()); + let chunk = scratch + .get_mut(..chunk_len) + .ok_or_else(|| anyhow!("Read chunk length out of bounds"))?; + read_exact_with_timeout(stream, chunk).await?; + len = len + .checked_sub(chunk_len) + .ok_or_else(|| anyhow!("Drained length underflow"))?; + } + + Ok(()) +} + +/// Parse PROXY protocol v2 header +pub async fn parse_proxy_v2_header(stream: &mut R) -> Result { + let mut header = [0u8; PROXY_V2_HEADER_LEN]; + read_exact_with_timeout(stream, &mut header).await?; + + // Signature check + if header[..12] != PROXY_V2_SIGNATURE { + return Err(anyhow!("Invalid PROXY protocol v2 signature")); + } + + let command = parse_command(header[12])?; + let addr_len = usize::from(u16::from_be_bytes([header[14], header[15]])); if addr_len > MAX_PROXY_ADDR_LEN { return Err(anyhow!("PROXY header too large: {addr_len}")); } // Handle LOCAL command - if command == CMD_LOCAL { + if command == Command::Local { if addr_len > 0 { - let mut discard = vec![0u8; addr_len]; - timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; + drain_bytes(stream, addr_len).await?; } return Ok(ProxyHeader::Local); } - if command != CMD_PROXY { - return Err(anyhow!("Unsupported PROXY command: {command}")); - } + let (family, protocol) = parse_family_protocol(header[13])?; - let family = family_protocol & 0xF0; - let protocol = family_protocol & 0x0F; - - if protocol != PROTO_STREAM { - return Err(anyhow!("Unsupported transport protocol: {protocol}")); + if protocol != TransportProtocol::Stream { + return Err(anyhow!("Unsupported transport protocol: {protocol:?}")); } match family { - AF_INET => parse_ipv4(stream, addr_len).await, - AF_INET6 => parse_ipv6(stream, addr_len).await, - AF_UNSPEC => { + AddressFamily::Inet => parse_ipv4(stream, addr_len).await, + AddressFamily::Inet6 => parse_ipv6(stream, addr_len).await, + AddressFamily::Unspec => { if addr_len > 0 { - let mut discard = vec![0u8; addr_len]; - timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; + drain_bytes(stream, addr_len).await?; } Ok(ProxyHeader::Local) } - AF_UNIX => Err(anyhow!("UNIX addresses not supported")), - _ => Err(anyhow!("Unknown address family: {family}")), + AddressFamily::Unix => Err(anyhow!("UNIX addresses not supported")), } } /// Parse IPv4 address block (12 bytes) + skip TLVs -async fn parse_ipv4( - stream: &mut R, - addr_len: usize, -) -> Result { - if addr_len < 12 { +async fn parse_ipv4(stream: &mut R, addr_len: usize) -> Result { + if addr_len < IPV4_BLOCK_LEN { return Err(anyhow!("IPv4 address block too short: {addr_len}")); } - let mut addr = [0u8; 12]; - timeout(READ_TIMEOUT, stream.read_exact(&mut addr)).await??; + let mut addr = [0u8; IPV4_BLOCK_LEN]; + read_exact_with_timeout(stream, &mut addr).await?; - if addr_len > 12 { - let tlv_len = addr_len - .checked_sub(12) - .ok_or_else(|| anyhow!("IPv4 address length underflow"))?; - let mut discard = vec![0u8; tlv_len]; - timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; + let tlv_len = addr_len + .checked_sub(IPV4_BLOCK_LEN) + .ok_or_else(|| anyhow!("IPv4 TLV length underflow"))?; + if tlv_len > 0 { + drain_bytes(stream, tlv_len).await?; } let src_addr = Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]); @@ -144,46 +199,28 @@ async fn parse_ipv4( } /// Parse IPv6 address block (36 bytes) + skip TLVs -async fn parse_ipv6( - stream: &mut R, - addr_len: usize, -) -> Result { - if addr_len < 36 { +async fn parse_ipv6(stream: &mut R, addr_len: usize) -> Result { + if addr_len < IPV6_BLOCK_LEN { return Err(anyhow!("IPv6 address block too short: {addr_len}")); } - let mut addr = [0u8; 36]; - timeout(READ_TIMEOUT, stream.read_exact(&mut addr)).await??; - - if addr_len > 36 { - let tlv_len = addr_len - .checked_sub(36) - .ok_or_else(|| anyhow!("IPv6 address length underflow"))?; - let mut discard = vec![0u8; tlv_len]; - timeout(READ_TIMEOUT, stream.read_exact(&mut discard)).await??; - } - - let src_addr = Ipv6Addr::new( - u16::from_be_bytes([addr[0], addr[1]]), - u16::from_be_bytes([addr[2], addr[3]]), - u16::from_be_bytes([addr[4], addr[5]]), - u16::from_be_bytes([addr[6], addr[7]]), - u16::from_be_bytes([addr[8], addr[9]]), - u16::from_be_bytes([addr[10], addr[11]]), - u16::from_be_bytes([addr[12], addr[13]]), - u16::from_be_bytes([addr[14], addr[15]]), - ); - - let dst_addr = Ipv6Addr::new( - u16::from_be_bytes([addr[16], addr[17]]), - u16::from_be_bytes([addr[18], addr[19]]), - u16::from_be_bytes([addr[20], addr[21]]), - u16::from_be_bytes([addr[22], addr[23]]), - u16::from_be_bytes([addr[24], addr[25]]), - u16::from_be_bytes([addr[26], addr[27]]), - u16::from_be_bytes([addr[28], addr[29]]), - u16::from_be_bytes([addr[30], addr[31]]), - ); + let mut addr = [0u8; IPV6_BLOCK_LEN]; + read_exact_with_timeout(stream, &mut addr).await?; + + let tlv_len = addr_len + .checked_sub(IPV6_BLOCK_LEN) + .ok_or_else(|| anyhow!("IPv6 TLV length underflow"))?; + if tlv_len > 0 { + drain_bytes(stream, tlv_len).await?; + } + + let mut src_addr_bytes = [0u8; 16]; + src_addr_bytes.copy_from_slice(&addr[..16]); + let src_addr = Ipv6Addr::from(src_addr_bytes); + + let mut dst_addr_bytes = [0u8; 16]; + dst_addr_bytes.copy_from_slice(&addr[16..32]); + let dst_addr = Ipv6Addr::from(dst_addr_bytes); let src_port = u16::from_be_bytes([addr[32], addr[33]]); let dst_port = u16::from_be_bytes([addr[34], addr[35]]); @@ -195,3 +232,135 @@ async fn parse_ipv6( dst_port, ))) } + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + const IPV4_BLOCK_LEN_U16: u16 = 12; + const IPV6_BLOCK_WITH_TLV_LEN_U16: u16 = 38; + + async fn parse_from_bytes(data: &[u8]) -> Result<(Result, Vec)> { + let (mut writer, mut reader) = tokio::io::duplex(2048); + writer.write_all(data).await?; + drop(writer); + + let header = parse_proxy_v2_header(&mut reader).await; + let mut remaining = Vec::new(); + reader.read_to_end(&mut remaining).await?; + + Ok((header, remaining)) + } + + fn build_header(version_command: u8, family_protocol: u8, addr_len: u16) -> Vec { + let mut data = Vec::with_capacity(PROXY_V2_HEADER_LEN); + data.extend_from_slice(&PROXY_V2_SIGNATURE); + data.push(version_command); + data.push(family_protocol); + data.extend_from_slice(&addr_len.to_be_bytes()); + data + } + + #[tokio::test] + async fn parses_local_header_and_discards_payload() -> Result<()> { + let mut data = build_header(0x20, 0x00, 3); + data.extend_from_slice(&[0xAA, 0xBB, 0xCC]); + + let (header, remaining) = parse_from_bytes(&data).await?; + + assert!(matches!(header, Ok(ProxyHeader::Local))); + assert!(remaining.is_empty()); + Ok(()) + } + + #[tokio::test] + async fn parses_local_header_with_unknown_family_and_protocol() -> Result<()> { + let mut data = build_header(0x20, 0xFF, 1); + data.push(0xAA); + + let (header, remaining) = parse_from_bytes(&data).await?; + + assert!(matches!(header, Ok(ProxyHeader::Local))); + assert!(remaining.is_empty()); + Ok(()) + } + + #[tokio::test] + async fn parses_ipv4_proxy_header() -> Result<()> { + let mut data = build_header(0x21, 0x11, IPV4_BLOCK_LEN_U16); + data.extend_from_slice(&[ + 192, 0, 2, 10, // source IPv4 + 198, 51, 100, 7, // destination IPv4 + 0x30, 0x39, // source port 12345 + 0x00, 0x50, // destination port 80 + ]); + + let (header, remaining) = parse_from_bytes(&data).await?; + let header = match header { + Ok(header) => header, + Err(error) => panic!("header should parse: {error}"), + }; + assert!(remaining.is_empty()); + + match header { + ProxyHeader::Proxied(info) => { + assert_eq!(info.src_addr, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10))); + assert_eq!(info.dst_addr, IpAddr::V4(Ipv4Addr::new(198, 51, 100, 7))); + assert_eq!(info.src_port, 12345); + assert_eq!(info.dst_port, 80); + } + ProxyHeader::Local => panic!("expected proxied header"), + } + Ok(()) + } + + #[tokio::test] + async fn parses_ipv6_proxy_header_and_discards_tlv() -> Result<()> { + let mut data = build_header(0x21, 0x21, IPV6_BLOCK_WITH_TLV_LEN_U16); + data.extend_from_slice(&[ + 0x20, 0x01, 0x0D, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, // source IPv6 + 0x20, 0x01, 0x0D, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, // destination IPv6 + 0x01, 0xBB, // source port 443 + 0x82, 0x35, // destination port 33333 + 0xEE, 0xFF, // TLV payload to skip + ]); + + let (header, remaining) = parse_from_bytes(&data).await?; + let header = match header { + Ok(header) => header, + Err(error) => panic!("header should parse: {error}"), + }; + assert!(remaining.is_empty()); + + match header { + ProxyHeader::Proxied(info) => { + assert_eq!( + info.src_addr, + IpAddr::V6(Ipv6Addr::new(0x2001, 0x0DB8, 0, 0, 0, 0, 0, 1)) + ); + assert_eq!( + info.dst_addr, + IpAddr::V6(Ipv6Addr::new(0x2001, 0x0DB8, 0, 0, 0, 0, 0, 2)) + ); + assert_eq!(info.src_port, 443); + assert_eq!(info.dst_port, 33333); + } + ProxyHeader::Local => panic!("expected proxied header"), + } + Ok(()) + } + + #[tokio::test] + async fn rejects_unknown_command() -> Result<()> { + let data = build_header(0x22, 0x11, 0); + let (header, _) = parse_from_bytes(&data).await?; + let Err(error) = header else { + panic!("header should fail"); + }; + + assert!(error.to_string().contains("Unsupported PROXY command")); + Ok(()) + } +} From bdd5b81b7303974647ae436ac886fde32c87b977 Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:25:47 +0100 Subject: [PATCH 09/10] remove env var injection to wrapped process --- README.md | 12 ------------ src/handler.rs | 7 ------- 2 files changed, 19 deletions(-) diff --git a/README.md b/README.md index 593fb24..f083f04 100644 --- a/README.md +++ b/README.md @@ -94,18 +94,6 @@ When PROXY protocol is enabled and a valid header is received, sossette: [2024-03-09T10:15:23Z INFO sossette] Real client: 192.0.2.123:54321 (via proxy [::1]:55438) ``` -2. **Passes client information to the wrapped process** via environment variables: - - `CLIENT_IP`: The real client's IP address (e.g., `192.0.2.123`) - - `CLIENT_PORT`: The real client's source port (e.g., `54321`) - - `PROXY_DEST_IP`: The destination IP the client connected to - - `PROXY_DEST_PORT`: The destination port the client connected to - - Example usage in a bash script: - ```bash - #!/bin/bash - echo "Welcome! Your IP is: $CLIENT_IP:$CLIENT_PORT" - ``` - ### Load balancer configuration #### HAProxy diff --git a/src/handler.rs b/src/handler.rs index 6344150..a83952a 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -112,13 +112,6 @@ pub async fn handle_client( command.args(&args.arguments); command.stdin(Stdio::piped()).stdout(Stdio::piped()); - if let Some(ref proxy_info) = proxy_info { - command.env("CLIENT_IP", proxy_info.src_addr.to_string()); - command.env("CLIENT_PORT", proxy_info.src_port.to_string()); - command.env("PROXY_DEST_IP", proxy_info.dst_addr.to_string()); - command.env("PROXY_DEST_PORT", proxy_info.dst_port.to_string()); - } - let mut child = command.group_spawn().context("Failed to run command")?; let child_stdin = child.inner().stdin.take().context("Failed to open stdin")?; From 9ad434b966517e86baa41ab4b08679b2a7e411a9 Mon Sep 17 00:00:00 2001 From: Matthieu OLIVIER <2124293+molivier@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:35:37 +0100 Subject: [PATCH 10/10] fix clippy --- Cargo.toml | 2 +- src/handler.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c617ec8..afb85d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ cast_possible_wrap = { level = "deny", priority = -1 } cast_precision_loss = { level = "deny", priority = -1 } integer_division = { level = "deny", priority = -1 } arithmetic_side_effects = { level = "deny", priority = -1 } -unchecked_time_subtraction = { level = "deny", priority = -1 } +unchecked_duration_subtraction = { level = "deny", priority = -1 } unwrap_used = "warn" expect_used = "warn" panicking_unwrap = { level = "deny", priority = -1 } diff --git a/src/handler.rs b/src/handler.rs index a83952a..572462c 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -23,7 +23,7 @@ async fn process_stdin( mut socket: R, mut child_stdin: W, ) -> Result<()> { - let mut in_buf = [0; 1024]; + let mut in_buf = [0u8; 1024]; loop { let n = socket.read(&mut in_buf).await?; if n == 0 { @@ -43,7 +43,7 @@ async fn process_stdout( mut socket: W, mut child_stdout: R, ) -> Result<()> { - let mut out_buf = [0; 1024]; + let mut out_buf = [0u8; 1024]; loop { let n = child_stdout.read(&mut out_buf).await?; if n == 0 { @@ -74,8 +74,8 @@ pub async fn handle_client( match proxy::parse_proxy_v2_header(&mut socket).await { Ok(proxy::ProxyHeader::Proxied(info)) => { info!( - "Client: {}:{} (via proxy {}) connected", - info.src_addr, info.src_port, peer_addr + "Client: {}:{} -> {}:{} (via proxy {}) connected", + info.src_addr, info.src_port, info.dst_addr, info.dst_port, peer_addr ); Some(info) } @@ -95,7 +95,7 @@ pub async fn handle_client( // MOTD if let Some(motd) = &args.motd { socket.write_all(motd.as_bytes()).await?; - socket.write_all(b"\r\n").await?; + socket.write_all(&b"\r\n"[..]).await?; } // Proof-of-work