├── .github └── workflows │ └── docker-publish.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.md ├── README.md └── src ├── args.rs ├── listener ├── mod.rs ├── tcp.rs └── udp.rs ├── main.rs ├── pipe.rs └── util.rs /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | schedule: 10 | - cron: '45 21 * * *' 11 | push: 12 | branches: [ "main" ] 13 | # Publish semver tags as releases. 14 | tags: [ 'v*.*.*' ] 15 | pull_request: 16 | branches: [ "main" ] 17 | 18 | env: 19 | # Use docker.io for Docker Hub if empty 20 | REGISTRY: ghcr.io 21 | # github.repository as / 22 | IMAGE_NAME: ${{ github.repository }} 23 | 24 | 25 | jobs: 26 | build: 27 | 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: read 31 | packages: write 32 | # This is used to complete the identity challenge 33 | # with sigstore/fulcio when running outside of PRs. 34 | id-token: write 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v3 39 | 40 | # Install the cosign tool except on PR 41 | # https://github.com/sigstore/cosign-installer 42 | - name: Install cosign 43 | if: github.event_name != 'pull_request' 44 | uses: sigstore/cosign-installer@f3c664df7af409cb4873aa5068053ba9d61a57b6 #v2.6.0 45 | with: 46 | cosign-release: 'v1.13.1' 47 | 48 | 49 | # Workaround: https://github.com/docker/build-push-action/issues/461 50 | - name: Setup Docker buildx 51 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 52 | 53 | # Login against a Docker registry except on PR 54 | # https://github.com/docker/login-action 55 | - name: Log into registry ${{ env.REGISTRY }} 56 | if: github.event_name != 'pull_request' 57 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 58 | with: 59 | registry: ${{ env.REGISTRY }} 60 | username: ${{ github.actor }} 61 | password: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | # Extract metadata (tags, labels) for Docker 64 | # https://github.com/docker/metadata-action 65 | - name: Extract Docker metadata 66 | id: meta 67 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 68 | with: 69 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 70 | 71 | # Build and push Docker image with Buildx (don't push on PR) 72 | # https://github.com/docker/build-push-action 73 | - name: Build and push Docker image 74 | id: build-and-push 75 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a 76 | with: 77 | context: . 78 | push: ${{ github.event_name != 'pull_request' }} 79 | tags: ${{ steps.meta.outputs.tags }} 80 | labels: ${{ steps.meta.outputs.labels }} 81 | cache-from: type=gha 82 | cache-to: type=gha,mode=max 83 | 84 | 85 | # Sign the resulting Docker image digest except on PRs. 86 | # This will only write to the public Rekor transparency log when the Docker 87 | # repository is public to avoid leaking data. If you would like to publish 88 | # transparency data even for private images, pass --force to cosign below. 89 | # https://github.com/sigstore/cosign 90 | - name: Sign the published Docker image 91 | if: ${{ github.event_name != 'pull_request' }} 92 | env: 93 | COSIGN_EXPERIMENTAL: "true" 94 | # This step uses the identity token to provision an ephemeral certificate 95 | # against the sigstore community Fulcio instance. 96 | run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.2.2] - 2023-01-04 6 | 7 | ### Bug Fixes 8 | 9 | - Fixed a memory leak where mmproxy doesn't free pipe fds after failing to set its buffer size. 10 | 11 | ## [0.2.1] - 2023-01-04 12 | 13 | ### Features 14 | 15 | - Introduced context messages for each error message. See [#13](https://github.com/saiko-tech/mmproxy-rs/pull/13). 16 | 17 | ## [0.1.1] - 2022-12-30 18 | 19 | ### Features 20 | 21 | - Made the TCP proxy to be zero-copy whenever possible. See [#10](https://github.com/saiko-tech/mmproxy-rs/pull/10). 22 | 23 | ### Documentation 24 | 25 | - MIT license added. 26 | - TCP benchmarks added. 27 | 28 | ### Miscellaneous Tasks 29 | 30 | - Published the project on [crates.io](https://crates.io/crates/mmproxy). 31 | 32 | ## [0.1.0] - 2022-12-15 33 | 34 | - Initial version of the project. 35 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "argwerk" 16 | version = "0.20.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "5c8afb154b22e25692fe2d5f30ae3e29344d3ebe546b7dce3c4063aab13b5775" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 25 | 26 | [[package]] 27 | name = "bitflags" 28 | version = "1.3.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 31 | 32 | [[package]] 33 | name = "bytes" 34 | version = "1.3.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 37 | 38 | [[package]] 39 | name = "cc" 40 | version = "1.0.77" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "cidr" 52 | version = "0.2.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "300bccc729b1ada84523246038aad61fead689ac362bb9d44beea6f6a188c34b" 55 | 56 | [[package]] 57 | name = "doc-comment" 58 | version = "0.3.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 61 | 62 | [[package]] 63 | name = "env_logger" 64 | version = "0.10.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 67 | dependencies = [ 68 | "humantime", 69 | "is-terminal", 70 | "log", 71 | "regex", 72 | "termcolor", 73 | ] 74 | 75 | [[package]] 76 | name = "errno" 77 | version = "0.2.8" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 80 | dependencies = [ 81 | "errno-dragonfly", 82 | "libc", 83 | "winapi", 84 | ] 85 | 86 | [[package]] 87 | name = "errno-dragonfly" 88 | version = "0.1.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 91 | dependencies = [ 92 | "cc", 93 | "libc", 94 | ] 95 | 96 | [[package]] 97 | name = "eyre" 98 | version = "0.6.8" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" 101 | dependencies = [ 102 | "indenter", 103 | "once_cell", 104 | ] 105 | 106 | [[package]] 107 | name = "hermit-abi" 108 | version = "0.1.19" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 111 | dependencies = [ 112 | "libc", 113 | ] 114 | 115 | [[package]] 116 | name = "hermit-abi" 117 | version = "0.2.6" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 120 | dependencies = [ 121 | "libc", 122 | ] 123 | 124 | [[package]] 125 | name = "humantime" 126 | version = "2.1.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 129 | 130 | [[package]] 131 | name = "indenter" 132 | version = "0.3.3" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 135 | 136 | [[package]] 137 | name = "io-lifetimes" 138 | version = "1.0.3" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 141 | dependencies = [ 142 | "libc", 143 | "windows-sys", 144 | ] 145 | 146 | [[package]] 147 | name = "is-terminal" 148 | version = "0.4.1" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" 151 | dependencies = [ 152 | "hermit-abi 0.2.6", 153 | "io-lifetimes", 154 | "rustix", 155 | "windows-sys", 156 | ] 157 | 158 | [[package]] 159 | name = "libc" 160 | version = "0.2.138" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 163 | 164 | [[package]] 165 | name = "linux-raw-sys" 166 | version = "0.1.3" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" 169 | 170 | [[package]] 171 | name = "log" 172 | version = "0.4.17" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 175 | dependencies = [ 176 | "cfg-if", 177 | ] 178 | 179 | [[package]] 180 | name = "memchr" 181 | version = "2.5.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 184 | 185 | [[package]] 186 | name = "mio" 187 | version = "0.8.5" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 190 | dependencies = [ 191 | "libc", 192 | "log", 193 | "wasi", 194 | "windows-sys", 195 | ] 196 | 197 | [[package]] 198 | name = "mmproxy" 199 | version = "0.2.2" 200 | dependencies = [ 201 | "argwerk", 202 | "cidr", 203 | "env_logger", 204 | "libc", 205 | "log", 206 | "proxy-protocol", 207 | "simple-eyre", 208 | "socket2", 209 | "tokio", 210 | ] 211 | 212 | [[package]] 213 | name = "num_cpus" 214 | version = "1.14.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" 217 | dependencies = [ 218 | "hermit-abi 0.1.19", 219 | "libc", 220 | ] 221 | 222 | [[package]] 223 | name = "once_cell" 224 | version = "1.17.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 227 | 228 | [[package]] 229 | name = "pin-project-lite" 230 | version = "0.2.9" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 233 | 234 | [[package]] 235 | name = "proc-macro2" 236 | version = "1.0.47" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 239 | dependencies = [ 240 | "unicode-ident", 241 | ] 242 | 243 | [[package]] 244 | name = "proxy-protocol" 245 | version = "0.5.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "0e50c72c21c738f5c5f350cc33640aee30bf7cd20f9d9da20ed41bce2671d532" 248 | dependencies = [ 249 | "bytes", 250 | "snafu", 251 | ] 252 | 253 | [[package]] 254 | name = "quote" 255 | version = "1.0.21" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 258 | dependencies = [ 259 | "proc-macro2", 260 | ] 261 | 262 | [[package]] 263 | name = "regex" 264 | version = "1.7.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 267 | dependencies = [ 268 | "aho-corasick", 269 | "memchr", 270 | "regex-syntax", 271 | ] 272 | 273 | [[package]] 274 | name = "regex-syntax" 275 | version = "0.6.28" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 278 | 279 | [[package]] 280 | name = "rustix" 281 | version = "0.36.5" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" 284 | dependencies = [ 285 | "bitflags", 286 | "errno", 287 | "io-lifetimes", 288 | "libc", 289 | "linux-raw-sys", 290 | "windows-sys", 291 | ] 292 | 293 | [[package]] 294 | name = "simple-eyre" 295 | version = "0.3.1" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "1b561532e8ffe7ecf09108c4f662896a9ec3eac4999eba84015ec3dcb8cc630a" 298 | dependencies = [ 299 | "eyre", 300 | "indenter", 301 | ] 302 | 303 | [[package]] 304 | name = "snafu" 305 | version = "0.6.10" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" 308 | dependencies = [ 309 | "doc-comment", 310 | "snafu-derive", 311 | ] 312 | 313 | [[package]] 314 | name = "snafu-derive" 315 | version = "0.6.10" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" 318 | dependencies = [ 319 | "proc-macro2", 320 | "quote", 321 | "syn", 322 | ] 323 | 324 | [[package]] 325 | name = "socket2" 326 | version = "0.4.7" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 329 | dependencies = [ 330 | "libc", 331 | "winapi", 332 | ] 333 | 334 | [[package]] 335 | name = "syn" 336 | version = "1.0.105" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 339 | dependencies = [ 340 | "proc-macro2", 341 | "quote", 342 | "unicode-ident", 343 | ] 344 | 345 | [[package]] 346 | name = "termcolor" 347 | version = "1.1.3" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 350 | dependencies = [ 351 | "winapi-util", 352 | ] 353 | 354 | [[package]] 355 | name = "tokio" 356 | version = "1.24.2" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" 359 | dependencies = [ 360 | "autocfg", 361 | "bytes", 362 | "libc", 363 | "memchr", 364 | "mio", 365 | "num_cpus", 366 | "pin-project-lite", 367 | "socket2", 368 | "tokio-macros", 369 | "windows-sys", 370 | ] 371 | 372 | [[package]] 373 | name = "tokio-macros" 374 | version = "1.8.2" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 377 | dependencies = [ 378 | "proc-macro2", 379 | "quote", 380 | "syn", 381 | ] 382 | 383 | [[package]] 384 | name = "unicode-ident" 385 | version = "1.0.5" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 388 | 389 | [[package]] 390 | name = "wasi" 391 | version = "0.11.0+wasi-snapshot-preview1" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 394 | 395 | [[package]] 396 | name = "winapi" 397 | version = "0.3.9" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 400 | dependencies = [ 401 | "winapi-i686-pc-windows-gnu", 402 | "winapi-x86_64-pc-windows-gnu", 403 | ] 404 | 405 | [[package]] 406 | name = "winapi-i686-pc-windows-gnu" 407 | version = "0.4.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 410 | 411 | [[package]] 412 | name = "winapi-util" 413 | version = "0.1.5" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 416 | dependencies = [ 417 | "winapi", 418 | ] 419 | 420 | [[package]] 421 | name = "winapi-x86_64-pc-windows-gnu" 422 | version = "0.4.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 425 | 426 | [[package]] 427 | name = "windows-sys" 428 | version = "0.42.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 431 | dependencies = [ 432 | "windows_aarch64_gnullvm", 433 | "windows_aarch64_msvc", 434 | "windows_i686_gnu", 435 | "windows_i686_msvc", 436 | "windows_x86_64_gnu", 437 | "windows_x86_64_gnullvm", 438 | "windows_x86_64_msvc", 439 | ] 440 | 441 | [[package]] 442 | name = "windows_aarch64_gnullvm" 443 | version = "0.42.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 446 | 447 | [[package]] 448 | name = "windows_aarch64_msvc" 449 | version = "0.42.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 452 | 453 | [[package]] 454 | name = "windows_i686_gnu" 455 | version = "0.42.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 458 | 459 | [[package]] 460 | name = "windows_i686_msvc" 461 | version = "0.42.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 464 | 465 | [[package]] 466 | name = "windows_x86_64_gnu" 467 | version = "0.42.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 470 | 471 | [[package]] 472 | name = "windows_x86_64_gnullvm" 473 | version = "0.42.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 476 | 477 | [[package]] 478 | name = "windows_x86_64_msvc" 479 | version = "0.42.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 482 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mmproxy" 3 | description = "Rust implementation of mmproxy (TCP + UDP)" 4 | repository = "https://github.com/saiko-tech/mmproxy-rs" 5 | version = "0.2.2" 6 | edition = "2021" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | log = "0.4.17" 11 | env_logger = "0.10.0" 12 | argwerk = "0.20.1" 13 | cidr = "0.2.1" 14 | proxy-protocol = "0.5.0" 15 | socket2 = "0.4.7" 16 | libc = "0.2.138" 17 | simple-eyre = "0.3.1" 18 | 19 | [dependencies.tokio] 20 | version = "1.24.2" 21 | features = ["net", "rt-multi-thread", "macros", "io-util", "sync", "time"] 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.66-alpine AS builder 2 | 3 | RUN apk add --no-cache musl-dev 4 | 5 | WORKDIR /build 6 | 7 | COPY . . 8 | 9 | RUN cargo build --release 10 | 11 | FROM alpine 12 | 13 | COPY --from=builder /build/target/release/mmproxy /usr/bin/ 14 | 15 | ENTRYPOINT [ "mmproxy" ] 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Saiko Technology Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mmproxy-rs 2 | 3 | A Rust implementation of MMProxy! 🚀 4 | 5 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md) 6 | [![crates.io](https://img.shields.io/crates/v/mmproxy.svg)](https://crates.io/crates/mmproxy) 7 | 8 | ## Rationale 9 | 10 | Many previous implementations only support [PROXY Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) for either TCP or UDP, whereas this version supports both TCP and UDP. 11 | 12 | Another reason to choose mmproxy-rs may be if you want to avoid interference from Garbage Collection pauses, which is what originally triggered the re-write from the amazing [go-mmproxy](https://github.com/path-network/go-mmproxy). 13 | 14 | ## Features 15 | 16 | - [x] TCP - Accepts PROXY Protocol enabled requests from [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/#proxy-protocol-for-a-tcp-connection-to-an-upstream), [HAProxy](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) 17 | - [x] UDP - Accepts PROXY Protocol enabled requests from [udppp](https://github.com/b23r0/udppp), [Cloudflare Spectrum](https://www.cloudflare.com/products/cloudflare-spectrum/) 18 | - [x] No Garbage Collection pauses 19 | 20 | ## Requirements 21 | 22 | Install Rust with [rustup](https://rustup.rs/) if you haven't already. 23 | 24 | ```sh 25 | $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 26 | $ cargo --version 27 | ``` 28 | 29 | ## Installation 30 | 31 | From git: 32 | ```sh 33 | cargo install --git https://github.com/saiko-tech/mmproxy-rs 34 | ``` 35 | 36 | From [crates.io](https://crates.io/crates/mmproxy) 37 | ```sh 38 | cargo install mmproxy 39 | ``` 40 | 41 | Via Docker (ghcr.io): 42 | ```sh 43 | docker run ghcr.io/saiko-tech/mmproxy-rs:main --help 44 | ``` 45 | 46 | ## Usage 47 | 48 | ``` 49 | Usage: mmproxy [-h] [options] 50 | 51 | Options: 52 | -h, --help Prints the help string. 53 | -4, --ipv4 Address to which IPv4 traffic will be forwarded to. 54 | (default: "127.0.0.1:443") 55 | -6, --ipv6 Address to which IPv6 traffic will be forwarded to. 56 | (default: "[::1]:443") 57 | 58 | -a, --allowed-subnets 59 | Path to a file that contains allowed subnets of the 60 | proxy servers. 61 | 62 | -c, --close-after Number of seconds after which UDP socket will be 63 | cleaned up. (default: 60) 64 | 65 | -l, --listen-addr 66 | Address the proxy listens on. (default: 67 | "0.0.0.0:8443") 68 | 69 | --listeners Number of listener sockets that will be opened for the 70 | listen address. (Linux 3.9+) (default: 1) 71 | -p, --protocol

Protocol that will be proxied: tcp, udp. (default: 72 | tcp) 73 | -m, --mark The mark that will be set on outbound packets. 74 | (default: 0) 75 | ``` 76 | 77 | ### Example 78 | 79 | You'll need root permissions or `CAP_NET_ADMIN` capability set on the mmproxy binary with [setcap(8)](https://man7.org/linux/man-pages/man8/setcap.8.html). 80 | 81 | ```sh 82 | address=X.X.X.X # get this via "ip addr" command - don't use 0.0.0.0! 83 | bind_port=8080 84 | upstream_port=8081 85 | sudo ip rule add from 127.0.0.1/8 iif lo table 123 86 | sudo ip route add local 0.0.0.0/0 dev lo table 123 87 | sudo mmproxy -m 123 -l $address:$bind_port -4 127.0.0.1:$upstream_port -p udp 88 | ``` 89 | 90 | ## Benchmarking 91 | 92 | Tests were run on a `Linux 6.0.12-arch1-1` box with an AMD Ryzen 5 5600H @ 3.3GHz (12 logical cores). 93 | 94 | ### TCP mode 95 | 96 | #### Setup 97 | 98 | [bpf-echo](https://github.com/path-network/bpf-echo) server simulated the upstream service that the proxy sent traffic to. The traffic was generated using [tcpkali](https://github.com/satori-com/tcpkali). 99 | 100 | The following command was used to generate load: 101 | 102 | ```sh 103 | tcpkali -c 50 -T 10s -e1 'PROXY TCP4 127.0.0.1 127.0.0.1 \{connection.uid} 25578\r\n' -m 'PING\r\n' 127.0.0.1:1122 104 | ``` 105 | 106 | which specifies 50 concurrent connections, a runtime of 10 seconds, sending a PROXYv1 header for each connection, and using the message `PING\r\n` over TCP. 107 | 108 | #### Results 109 | 110 | | | ↓ Mbps | ↑ Mbps | ↓ pkt/s | ↑ pkt/s | 111 | | ---------- | --------- | --------- | --------- | --------- | 112 | | no-proxy | 34662.036 | 53945.378 | 3173626.3 | 4630027.6 | 113 | | go-mmproxy | 27527.743 | 44128.818 | 2520408.4 | 3787491.3 | 114 | | mmproxy-rs | 27228.169 | 50173.384 | 2492924.1 | 4306284.7 | 115 | 116 | ### UDP Mode 117 | 118 | #### Setup 119 | 120 | ``` 121 | iperf client -> udppp -> mmproxy-rs/go-mmproxy -> iperf server 122 | ``` 123 | 124 | ``` 125 | $ udppp -m 1 -l 25578 -r 25577 -h "127.0.0.1" -b "127.0.0.1" -p // udppp 126 | # mmproxy -l "127.0.0.1:25577" -4 "127.0.0.1:1122" -p udp -c 1 // mmproxy-rs 127 | # mmproxy -l "127.0.0.1:25577" -4 "127.0.0.1:1122" -p udp -close-after 1 // go-mmproxy 128 | $ iperf -sup 1122 // iperf server 129 | $ iperf -c 127.0.0.1 -p 25578 -Rub 10G // iperf client 130 | ``` 131 | 132 | #### Results 133 | 134 | | | transfer | bandwidth | 135 | |------------|-------------|----------------| 136 | | no-proxy | 6.31 GBytes | 5.42 Gbits/sec | 137 | | go-mmproxy | 3.13 GBytes | 2.69 Gbits/sec | 138 | | mmproxy-rs | 3.70 GBytes | 3.18 Gbits/sec | 139 | 140 | The iperf test was run in reverse mode, with the server sending data to the client. The results suggest that mmproxy-rs has higher throughput from upstream to downstream compared to go-mmproxy. 141 | 142 | ## Acknowledgements and References 143 | 144 | - https://blog.cloudflare.com/mmproxy-creative-way-of-preserving-client-ips-in-spectrum/ 145 | - https://github.com/cloudflare/mmproxy 146 | - https://github.com/path-network/go-mmproxy 147 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use crate::util::{self, Protocol}; 2 | use std::{net::SocketAddr, time::Duration}; 3 | 4 | argwerk::define! { 5 | #[usage = "mmproxy [-h] [options]"] 6 | #[derive(Clone)] 7 | pub struct Args { 8 | pub help: bool = false, 9 | pub ipv4_fwd: SocketAddr = "127.0.0.1:443".parse().unwrap(), 10 | pub ipv6_fwd: SocketAddr = "[::1]:443".parse().unwrap(), 11 | pub allowed_subnets: Option> = None, 12 | pub close_after: Duration = Duration::from_secs(60), 13 | pub mark: u32 = 0, 14 | pub listen_addr: SocketAddr = "0.0.0.0:8443".parse().unwrap(), 15 | pub listeners: u32 = 1, 16 | pub protocol: Protocol = Protocol::Tcp 17 | } 18 | /// Prints the help string. 19 | ["-h" | "--help"] => { 20 | println!("{}", Args::help()); 21 | help = true; 22 | } 23 | /// Address to which IPv4 traffic will be forwarded to. (default: "127.0.0.1:443") 24 | ["-4" | "--ipv4", addr] => { 25 | ipv4_fwd = addr.parse()?; 26 | } 27 | /// Address to which IPv6 traffic will be forwarded to. (default: "[::1]:443") 28 | ["-6" | "--ipv6", addr] => { 29 | ipv6_fwd = addr.parse()?; 30 | } 31 | /// Path to a file that contains allowed subnets of the proxy servers. 32 | ["-a" | "--allowed-subnets", path] => { 33 | let ret = util::parse_allowed_subnets(&path)?; 34 | allowed_subnets = if !ret.is_empty() { Some (ret) } else { None } 35 | } 36 | /// Number of seconds after which UDP socket will be cleaned up. (default: 60) 37 | ["-c" | "--close-after", n] => { 38 | close_after = Duration::from_secs(str::parse(&n)?); 39 | } 40 | /// Address the proxy listens on. (default: "0.0.0.0:8443") 41 | ["-l" | "--listen-addr", string] => { 42 | listen_addr = string.parse()?; 43 | } 44 | /// Number of listener sockets that will be opened for the listen address. (Linux 3.9+) (default: 1) 45 | ["--listeners", n] => { 46 | listeners = str::parse(&n)?; 47 | } 48 | /// Protocol that will be proxied: tcp, udp. (default: tcp) 49 | ["-p" | "--protocol", p] => { 50 | protocol = match &p.to_lowercase()[..] { 51 | "tcp" => Protocol::Tcp, 52 | "udp" => Protocol::Udp, 53 | _ => return Err(format!("invalid protocol value: {p}").into()), 54 | }; 55 | } 56 | /// The mark that will be set on outbound packets. (default: 0) 57 | ["-m" | "--mark", n] => { 58 | mark = str::parse::(&n)?; 59 | } 60 | } 61 | 62 | pub fn parse_args() -> Result { 63 | match Args::args() { 64 | Ok(args) => { 65 | if args.help { 66 | std::process::exit(1); 67 | } 68 | Ok(args) 69 | } 70 | Err(err) => Err(err), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/listener/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tcp; 2 | pub mod udp; 3 | -------------------------------------------------------------------------------- /src/listener/tcp.rs: -------------------------------------------------------------------------------- 1 | use simple_eyre::eyre::{Result, WrapErr}; 2 | 3 | use crate::{ 4 | args::Args, 5 | pipe::{splice, wouldblock, Pipe, PIPE_BUF_SIZE}, 6 | util, 7 | }; 8 | 9 | use std::{net::SocketAddr, os::fd::AsRawFd}; 10 | use tokio::{ 11 | io::{AsyncReadExt, AsyncWriteExt, Interest}, 12 | net::{ 13 | tcp::{ReadHalf, WriteHalf}, 14 | TcpSocket, TcpStream, 15 | }, 16 | }; 17 | 18 | pub async fn listen(args: Args) -> Result<()> { 19 | let socket = match args.listen_addr { 20 | SocketAddr::V4(_) => TcpSocket::new_v4(), 21 | SocketAddr::V6(_) => TcpSocket::new_v6(), 22 | }; 23 | let socket = socket.wrap_err("failed to create socket")?; 24 | 25 | socket 26 | .set_reuseport(args.listeners > 1) 27 | .wrap_err("failed to set reuseport")?; 28 | socket 29 | .set_reuseaddr(true) 30 | .wrap_err("failed to set reuseaddr")?; 31 | socket 32 | .bind(args.listen_addr) 33 | .wrap_err_with(|| format!("failed to bind to {}", args.listen_addr))?; 34 | 35 | let listener = socket 36 | .listen(args.listeners) 37 | .wrap_err("failed to start the listener")?; 38 | 39 | log::info!("listening on: {}", args.listen_addr); 40 | loop { 41 | let (conn, addr) = listener 42 | .accept() 43 | .await 44 | .wrap_err("failed to accept connection")?; 45 | 46 | if let Some(ref allowed_subnets) = args.allowed_subnets { 47 | let ip_addr = addr.ip(); 48 | 49 | if !util::check_origin_allowed(&ip_addr, allowed_subnets) { 50 | log::warn!("connection origin is not allowed: {ip_addr}"); 51 | continue; 52 | } 53 | } 54 | 55 | let mark = args.mark; 56 | let ipv4_fwd = args.ipv4_fwd; 57 | let ipv6_fwd = args.ipv6_fwd; 58 | 59 | tokio::spawn(async move { 60 | if let Err(err) = tcp_handle_connection(conn, addr, mark, ipv4_fwd, ipv6_fwd).await { 61 | log::error!("{err:#}"); 62 | } 63 | }); 64 | } 65 | } 66 | 67 | async fn tcp_handle_connection( 68 | mut src: TcpStream, 69 | addr: SocketAddr, 70 | mark: u32, 71 | ipv4_fwd: SocketAddr, 72 | ipv6_fwd: SocketAddr, 73 | ) -> Result<()> { 74 | src.set_nodelay(true) 75 | .wrap_err_with(|| format!("failed to set nodelay on {addr} socket"))?; 76 | 77 | let mut buffer = [0u8; u16::MAX as usize]; 78 | let read_bytes = src 79 | .read(&mut buffer) 80 | .await 81 | .wrap_err_with(|| format!("failed to read the initial proxy-protocol header on {addr}"))?; 82 | 83 | let (addr_pair, mut rest, _version) = util::parse_proxy_protocol_header(&buffer[..read_bytes]) 84 | .wrap_err("failed to parse the proxy protocol header")?; 85 | 86 | let src_addr = match addr_pair { 87 | Some((src, _dst)) => src, 88 | None => { 89 | log::debug!("unknown source, using the downstream connection address"); 90 | addr 91 | } 92 | }; 93 | let target_addr = match src_addr { 94 | SocketAddr::V4(_) => ipv4_fwd, 95 | SocketAddr::V6(_) => ipv6_fwd, 96 | }; 97 | log::info!("[new conn] [origin: {addr}] [src: {src_addr}]"); 98 | 99 | let mut dst = util::tcp_create_upstream_conn(src_addr, target_addr, mark).await?; 100 | tokio::io::copy_buf(&mut rest, &mut dst) 101 | .await 102 | .wrap_err("failed to re-transmit rest of the initial tcp packet")?; 103 | 104 | let (mut sr, mut sw) = src.split(); 105 | let (mut dr, mut dw) = dst.split(); 106 | 107 | let src_to_dst = async { 108 | splice_copy(&mut sr, &mut dw).await?; 109 | dw.shutdown() 110 | .await 111 | .wrap_err("failed to shutdown the dst writer") 112 | }; 113 | let dst_to_src = async { 114 | splice_copy(&mut dr, &mut sw).await?; 115 | sw.shutdown() 116 | .await 117 | .wrap_err("failed to shutdown the src writer") 118 | }; 119 | 120 | tokio::try_join!(src_to_dst, dst_to_src) 121 | // discard the `Ok(_)` value as it's useless 122 | .map(|_| ()) 123 | } 124 | 125 | // wait for src to be readable 126 | // splice from src to the pipe buffer 127 | // wait for dst to be writable 128 | // splice to dst from the pipe buffer 129 | async fn splice_copy(src: &mut ReadHalf<'_>, dst: &mut WriteHalf<'_>) -> Result<()> { 130 | use std::io::{Error, ErrorKind::WouldBlock}; 131 | 132 | let pipe = Pipe::new().wrap_err("failed to create pipe")?; 133 | // number of bytes that the pipe buffer is currently holding 134 | let mut size = 0; 135 | let mut done = false; 136 | 137 | let src = src.as_ref(); 138 | let dst = dst.as_ref(); 139 | let src_fd = src.as_raw_fd(); 140 | let dst_fd = dst.as_raw_fd(); 141 | 142 | while !done { 143 | src.readable() 144 | .await 145 | .wrap_err("awaiting on readable failed")?; 146 | let ret = src.try_io(Interest::READABLE, || { 147 | while size < PIPE_BUF_SIZE { 148 | match splice(src_fd, pipe.w, PIPE_BUF_SIZE - size) { 149 | r if r > 0 => size += r as usize, 150 | r if r == 0 => { 151 | done = true; 152 | break; 153 | } 154 | r if r < 0 && wouldblock() => { 155 | return Err(Error::new(WouldBlock, "EWOULDBLOCK")) 156 | } 157 | _ => return Err(Error::last_os_error()), 158 | } 159 | } 160 | Ok(()) 161 | }); 162 | if let Err(err) = ret { 163 | if err.kind() != WouldBlock { 164 | break; 165 | } 166 | } 167 | 168 | dst.writable() 169 | .await 170 | .wrap_err("awaiting on writable failed")?; 171 | let ret = dst.try_io(Interest::WRITABLE, || { 172 | while size > 0 { 173 | match splice(pipe.r, dst_fd, size) { 174 | r if r > 0 => size -= r as usize, 175 | r if r < 0 && wouldblock() => { 176 | return Err(Error::new(WouldBlock, "EWOULDBLOCK")) 177 | } 178 | _ => return Err(Error::last_os_error()), 179 | } 180 | } 181 | Ok(()) 182 | }); 183 | if let Err(err) = ret { 184 | if err.kind() != WouldBlock { 185 | break; 186 | } 187 | } 188 | } 189 | 190 | if done { 191 | Ok(()) 192 | } else { 193 | Err(Error::last_os_error().into()) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/listener/udp.rs: -------------------------------------------------------------------------------- 1 | use simple_eyre::eyre::{eyre, Result, WrapErr}; 2 | 3 | use crate::{args::Args, util}; 4 | use socket2::SockRef; 5 | use std::{ 6 | collections::HashMap, 7 | net::SocketAddr, 8 | sync::{ 9 | atomic::{AtomicU64, Ordering}, 10 | Arc, 11 | }, 12 | time::Duration, 13 | }; 14 | use tokio::{net::UdpSocket, sync::mpsc, task::JoinHandle}; 15 | 16 | const MAX_DGRAM_SIZE: usize = 65_507; 17 | type ConnectionsHashMap = HashMap, JoinHandle<()>)>; 18 | 19 | #[derive(Debug)] 20 | struct UdpProxyConn { 21 | pub sock: UdpSocket, 22 | pub last_activity: AtomicU64, 23 | } 24 | 25 | impl UdpProxyConn { 26 | fn new(sock: UdpSocket) -> Self { 27 | Self { 28 | sock, 29 | last_activity: AtomicU64::new(0), 30 | } 31 | } 32 | } 33 | 34 | pub async fn listen(args: Args) -> Result<()> { 35 | let socket = { 36 | let socket = UdpSocket::bind(args.listen_addr) 37 | .await 38 | .wrap_err_with(|| format!("failed to bind to {}", args.listen_addr))?; 39 | 40 | let sock_ref = SockRef::from(&socket); 41 | sock_ref 42 | .set_reuse_port(args.listeners > 1) 43 | .wrap_err("failed to set reuse port on listener socket")?; 44 | 45 | Arc::new(socket) 46 | }; 47 | 48 | let mut buffer = [0u8; MAX_DGRAM_SIZE]; 49 | let mut connections = ConnectionsHashMap::new(); 50 | let (tx, mut rx) = mpsc::channel::(128); 51 | 52 | log::info!("listening on: {}", args.listen_addr); 53 | loop { 54 | tokio::select! { 55 | // close inactive connections in this branch 56 | addr = rx.recv() => { 57 | if let Some(addr) = addr { 58 | if let Some((_conn, handle)) = connections.remove(&addr) { 59 | log::info!("closing {addr} due to inactivity"); 60 | handle.abort(); 61 | } 62 | } 63 | } 64 | // handle incoming DGRAM packets in this branch 65 | ret = socket.recv_from(&mut buffer) => { 66 | let (read, addr) = ret.wrap_err("failed to accept connection")?; 67 | 68 | if let Some(ref allowed_subnets) = args.allowed_subnets { 69 | let ip_addr = addr.ip(); 70 | 71 | if !util::check_origin_allowed(&ip_addr, allowed_subnets) { 72 | log::warn!("connection origin is not allowed: {ip_addr}"); 73 | continue; 74 | } 75 | } 76 | 77 | if let Err(why) = udp_handle_connection( 78 | &args, 79 | socket.clone(), 80 | addr, 81 | &mut buffer[..read], 82 | &mut connections, 83 | tx.clone(), 84 | ) 85 | .await 86 | { 87 | log::error!("{why:#}"); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | async fn udp_handle_connection( 95 | args: &Args, 96 | src: Arc, 97 | addr: SocketAddr, 98 | buffer: &mut [u8], 99 | connections: &mut ConnectionsHashMap, 100 | tx: mpsc::Sender, 101 | ) -> Result<()> { 102 | let (src_addr, rest, version) = match util::parse_proxy_protocol_header(buffer) { 103 | Ok((addr_pair, rest, version)) => match addr_pair { 104 | Some((src, _)) => (src, rest, version), 105 | None => (addr, rest, version), 106 | }, 107 | Err(err) => return Err(err).wrap_err("failed to parse proxy protocol header"), 108 | }; 109 | 110 | if version < 2 { 111 | return Err(eyre!( 112 | "proxy protocol version 1 doesn't support UDP connections" 113 | )); 114 | } 115 | let target_addr = match src_addr { 116 | SocketAddr::V4(_) => args.ipv4_fwd, 117 | SocketAddr::V6(_) => args.ipv6_fwd, 118 | }; 119 | 120 | let dst = match connections.get(&addr) { 121 | Some((dst, _handle)) => { 122 | dst.last_activity.fetch_add(1, Ordering::SeqCst); 123 | dst.clone() 124 | } 125 | // first time connecting 126 | None => { 127 | if src_addr == addr { 128 | log::debug!("unknown source, using the downstream connection address"); 129 | } 130 | log::info!("[new conn] [origin: {addr}] [src: {src_addr}]"); 131 | 132 | let dst = { 133 | let sock = util::udp_create_upstream_conn(src_addr, target_addr, args.mark).await?; 134 | Arc::new(UdpProxyConn::new(sock)) 135 | }; 136 | 137 | let src_clone = src.clone(); 138 | let dst_clone = dst.clone(); 139 | let handle = tokio::spawn(async move { 140 | if let Err(why) = udp_dst_to_src(addr, src_addr, src_clone, dst_clone).await { 141 | log::error!("{why:#}"); 142 | }; 143 | }); 144 | tokio::spawn(udp_close_after_inactivity( 145 | addr, 146 | args.close_after, 147 | tx.clone(), 148 | dst.clone(), 149 | )); 150 | 151 | connections.insert(addr, (dst.clone(), handle)); 152 | dst 153 | } 154 | }; 155 | 156 | match dst.sock.send(rest).await { 157 | Ok(size) => { 158 | log::debug!("from [{}] to [{}], size: {}", src_addr, addr, size); 159 | Ok(()) 160 | } 161 | Err(err) => Err(err).wrap_err("failed to write data to the upstream connection"), 162 | } 163 | } 164 | 165 | async fn udp_dst_to_src( 166 | addr: SocketAddr, 167 | src_addr: SocketAddr, 168 | src: Arc, 169 | dst: Arc, 170 | ) -> Result<()> { 171 | let mut buffer = [0u8; MAX_DGRAM_SIZE]; 172 | 173 | loop { 174 | let read_bytes = dst.sock.recv(&mut buffer).await?; 175 | let sent_bytes = src.send_to(&buffer[..read_bytes], addr).await?; 176 | if sent_bytes == 0 { 177 | return Err(eyre!("couldn't sent anything to downstream")); 178 | } 179 | log::debug!("from [{}] to [{}], size: {}", addr, src_addr, sent_bytes); 180 | 181 | dst.last_activity.fetch_add(1, Ordering::SeqCst); 182 | } 183 | } 184 | 185 | async fn udp_close_after_inactivity( 186 | addr: SocketAddr, 187 | close_after: Duration, 188 | tx: mpsc::Sender, 189 | dst: Arc, 190 | ) { 191 | let mut last_activity = dst.last_activity.load(Ordering::SeqCst); 192 | loop { 193 | tokio::time::sleep(close_after).await; 194 | if dst.last_activity.load(Ordering::SeqCst) == last_activity { 195 | break; 196 | } 197 | last_activity = dst.last_activity.load(Ordering::SeqCst); 198 | } 199 | 200 | if let Err(why) = tx.send(addr).await { 201 | log::error!("couldn't send the close command to conn channel: {why}"); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod listener; 3 | mod pipe; 4 | mod util; 5 | 6 | use env_logger::{Env, DEFAULT_FILTER_ENV}; 7 | use listener::{tcp, udp}; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | env_logger::init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); 12 | 13 | let args = match args::parse_args() { 14 | Ok(args) => args, 15 | Err(why) => { 16 | log::error!("{why}"); 17 | return; 18 | } 19 | }; 20 | 21 | let ret = match args.protocol { 22 | util::Protocol::Tcp => tcp::listen(args).await, 23 | util::Protocol::Udp => udp::listen(args).await, 24 | }; 25 | 26 | if let Err(why) = ret { 27 | log::error!("{why:#}"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/pipe.rs: -------------------------------------------------------------------------------- 1 | use std::{io, ptr::null_mut}; 2 | 3 | pub const PIPE_BUF_SIZE: usize = 1 << 20; 4 | 5 | #[derive(Debug)] 6 | pub struct Pipe { 7 | pub r: i32, 8 | pub w: i32, 9 | } 10 | 11 | impl Pipe { 12 | pub fn new() -> io::Result { 13 | let pipes = unsafe { 14 | let mut pipes = std::mem::MaybeUninit::<[libc::c_int; 2]>::uninit(); 15 | if libc::pipe2( 16 | pipes.as_mut_ptr().cast(), 17 | libc::O_NONBLOCK | libc::O_CLOEXEC, 18 | ) < 0 19 | { 20 | return Err(io::Error::last_os_error()); 21 | } 22 | pipes.assume_init() 23 | }; 24 | 25 | unsafe { 26 | if libc::fcntl(pipes[0], libc::F_SETPIPE_SZ, PIPE_BUF_SIZE) < 0 { 27 | libc::close(pipes[0]); 28 | libc::close(pipes[1]); 29 | 30 | return Err(io::Error::last_os_error()); 31 | } 32 | } 33 | 34 | Ok(Self { 35 | r: pipes[0], 36 | w: pipes[1], 37 | }) 38 | } 39 | } 40 | 41 | impl Drop for Pipe { 42 | fn drop(&mut self) { 43 | unsafe { 44 | libc::close(self.r); 45 | libc::close(self.w); 46 | } 47 | } 48 | } 49 | 50 | pub fn splice(r: i32, w: i32, n: usize) -> isize { 51 | unsafe { 52 | libc::splice( 53 | r, 54 | null_mut::(), 55 | w, 56 | null_mut::(), 57 | n, 58 | libc::SPLICE_F_MOVE | libc::SPLICE_F_NONBLOCK, 59 | ) 60 | } 61 | } 62 | 63 | pub fn wouldblock() -> bool { 64 | let errno = unsafe { *libc::__errno_location() }; 65 | errno == libc::EWOULDBLOCK || errno == libc::EAGAIN 66 | } 67 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use simple_eyre::eyre::{Result, WrapErr}; 2 | 3 | use std::{ 4 | fs::File, 5 | io::{self, Read}, 6 | net::{IpAddr, SocketAddr}, 7 | str::FromStr, 8 | }; 9 | 10 | use proxy_protocol::{version1 as v1, version2 as v2, ProxyHeader}; 11 | use socket2::{Domain, SockRef, Socket, Type}; 12 | use tokio::net::{TcpSocket, TcpStream, UdpSocket}; 13 | 14 | // this is returned from `util::parse_proxy_protocol_header` function 15 | pub type ProxyProtocolResult<'a> = io::Result<(Option<(SocketAddr, SocketAddr)>, &'a [u8], i32)>; 16 | 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 18 | pub enum Protocol { 19 | Tcp, 20 | Udp, 21 | } 22 | 23 | impl Default for Protocol { 24 | fn default() -> Self { 25 | Self::Tcp 26 | } 27 | } 28 | 29 | pub fn check_origin_allowed(addr: &IpAddr, subnets: &[cidr::IpCidr]) -> bool { 30 | for net in subnets.iter() { 31 | if net.contains(addr) { 32 | return true; 33 | } 34 | } 35 | 36 | false 37 | } 38 | 39 | pub fn parse_allowed_subnets(path: &str) -> io::Result> { 40 | let mut data = Vec::new(); 41 | let mut file = File::open(path)?; 42 | 43 | let mut contents = String::new(); 44 | file.read_to_string(&mut contents)?; 45 | 46 | for line in contents.lines() { 47 | match cidr::IpCidr::from_str(line) { 48 | Ok(cidr) => data.push(cidr), 49 | Err(why) => { 50 | return Err(io::Error::new(io::ErrorKind::Other, why)); 51 | } 52 | } 53 | } 54 | 55 | Ok(data) 56 | } 57 | 58 | fn setup_socket(socket_ref: &SockRef, src: SocketAddr, mark: u32) -> Result<()> { 59 | // needs CAP_NET_ADMIN 60 | socket_ref 61 | .set_ip_transparent(true) 62 | .wrap_err("failed to set ip transparent on the upstream socket")?; 63 | socket_ref 64 | .set_nonblocking(true) 65 | .wrap_err("failed to set nonblocking on the upstream socket")?; 66 | socket_ref 67 | .set_reuse_address(true) 68 | .wrap_err("failed to set reuse address on the upstream socket")?; 69 | socket_ref 70 | .set_mark(mark) 71 | .wrap_err("failed to set mark on the upstream socket")?; 72 | socket_ref 73 | .bind(&src.into()) 74 | .wrap_err("failed to set source address for the upstream socket")?; 75 | 76 | Ok(()) 77 | } 78 | 79 | pub async fn tcp_create_upstream_conn( 80 | src: SocketAddr, 81 | target: SocketAddr, 82 | mark: u32, 83 | ) -> Result { 84 | let socket = match src { 85 | SocketAddr::V4(_) => TcpSocket::new_v4(), 86 | SocketAddr::V6(_) => TcpSocket::new_v6(), 87 | }; 88 | let socket = socket.wrap_err("failed to create the upstream socket")?; 89 | let socket_ref = SockRef::from(&socket); 90 | 91 | socket_ref 92 | .set_nodelay(true) 93 | .wrap_err("failed to set nodelay on the upstream socket")?; 94 | setup_socket(&socket_ref, src, mark)?; 95 | 96 | socket 97 | .connect(target) 98 | .await 99 | .wrap_err("failed to connect to the upstream server") 100 | } 101 | 102 | pub async fn udp_create_upstream_conn( 103 | src: SocketAddr, 104 | target: SocketAddr, 105 | mark: u32, 106 | ) -> Result { 107 | let socket = match src { 108 | SocketAddr::V4(_) => Socket::new(Domain::IPV4, Type::DGRAM, None), 109 | SocketAddr::V6(_) => Socket::new(Domain::IPV6, Type::DGRAM, None), 110 | }; 111 | let socket = socket.wrap_err("failed to create upstream socket")?; 112 | 113 | setup_socket(&SockRef::from(&socket), src, mark)?; 114 | let udp_socket = UdpSocket::from_std(socket.into()) 115 | .wrap_err("failed to cast socket2 socket to tokio socket")?; 116 | 117 | udp_socket 118 | .connect(target) 119 | .await 120 | .wrap_err("failed to connecto to the upstream server")?; 121 | 122 | Ok(udp_socket) 123 | } 124 | 125 | // TODO: revise this 126 | pub fn parse_proxy_protocol_header(mut buffer: &[u8]) -> ProxyProtocolResult { 127 | match proxy_protocol::parse(&mut buffer) { 128 | Ok(result) => match result { 129 | ProxyHeader::Version1 { addresses } => match addresses { 130 | v1::ProxyAddresses::Unknown => Ok((None, buffer, 1)), 131 | v1::ProxyAddresses::Ipv4 { 132 | source, 133 | destination, 134 | } => Ok(( 135 | Some((SocketAddr::V4(source), SocketAddr::V4(destination))), 136 | buffer, 137 | 1, 138 | )), 139 | v1::ProxyAddresses::Ipv6 { 140 | source, 141 | destination, 142 | } => Ok(( 143 | Some((SocketAddr::V6(source), SocketAddr::V6(destination))), 144 | buffer, 145 | 1, 146 | )), 147 | }, 148 | ProxyHeader::Version2 { addresses, .. } => match addresses { 149 | v2::ProxyAddresses::Unspec => Ok((None, buffer, 2)), 150 | v2::ProxyAddresses::Ipv4 { 151 | source, 152 | destination, 153 | } => Ok(( 154 | Some((SocketAddr::V4(source), SocketAddr::V4(destination))), 155 | buffer, 156 | 2, 157 | )), 158 | v2::ProxyAddresses::Ipv6 { 159 | source, 160 | destination, 161 | } => Ok(( 162 | Some((SocketAddr::V6(source), SocketAddr::V6(destination))), 163 | buffer, 164 | 2, 165 | )), 166 | v2::ProxyAddresses::Unix { .. } => Err(io::Error::new( 167 | io::ErrorKind::Other, 168 | "unix sockets are not supported", 169 | )), 170 | }, 171 | _ => unreachable!(), 172 | }, 173 | Err(err) => Err(io::Error::new(io::ErrorKind::Other, err)), 174 | } 175 | } 176 | --------------------------------------------------------------------------------