Repository: xnuter/http-tunnel Branch: main Commit: 1a61b0d4229d Files: 24 Total size: 114.1 KB Directory structure: gitextract_osepvf9y/ ├── .github/ │ ├── actions-rs/ │ │ └── grcov.yml │ └── workflows/ │ ├── clippy.yml │ ├── grcov.yml │ └── tests.yml ├── .gitignore ├── COPYRIGHT ├── Cargo.toml ├── LICENSE ├── README.md ├── config/ │ ├── README.md │ ├── config-browser.yaml │ ├── config.yaml │ ├── domain.crt │ ├── domain.key │ ├── domain.pfx │ └── log4rs.yaml ├── misc/ │ └── diagrams/ │ ├── components-high-level.puml │ └── components.puml └── src/ ├── configuration.rs ├── http_tunnel_codec.rs ├── main.rs ├── proxy_target.rs ├── relay.rs └── tunnel.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/actions-rs/grcov.yml ================================================ output-type: lcov output-path: ./lcov.info source-dir: ./src ignore-dir: - "*.cargo/*" ignore: - "*.cargo*" - "*rust*" - "*configuration.rs" - "*main.rs" - "*proxy_target.rs" ignore-not-existing: true llvm: true excl-start: (.*)begin-ignore-line(.*) excl-stop: (.*)end-ignore-line(.*) ================================================ FILE: .github/workflows/clippy.yml ================================================ on: [push, pull_request] name: Clippy/Fmt jobs: clippy: name: Clippy runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install nightly toolchain with clippy available uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: nightly override: true components: clippy - name: Run cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: -- -D warnings rustfmt: name: Format runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install nightly toolchain with rustfmt available uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: nightly override: true components: rustfmt - name: Run cargo fmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check ================================================ FILE: .github/workflows/grcov.yml ================================================ on: [push, pull_request] name: Code coverage with grcov jobs: grcov: runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install toolchain uses: actions-rs/toolchain@v1 with: toolchain: nightly override: true profile: minimal - name: Execute tests uses: actions-rs/cargo@v1 with: command: test args: --features plain_text env: CARGO_INCREMENTAL: 0 RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" - name: Gather coverage data id: coverage uses: actions-rs/grcov@v0.1 - name: Pre-installing rust-covfix uses: actions-rs/install@v0.1 with: crate: rust-covfix use-tool-cache: true - name: Fix coverage data id: fix-coverage continue-on-error: true run: rust-covfix lcov.info -o lcov.info - name: Coveralls upload uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true path-to-lcov: lcov.info grcov_finalize: runs-on: ubuntu-latest needs: grcov steps: - name: Coveralls finalization uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true ================================================ FILE: .github/workflows/tests.yml ================================================ on: [push, pull_request] name: Tests env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose - name: Run tests for plain_text run: cargo test --verbose --features plain_text ================================================ FILE: .gitignore ================================================ /target /.idea Cargo.lock /log/*.log ================================================ FILE: COPYRIGHT ================================================ Copyrights in the http-tunnel project are retained by their contributors. No copyright assignment is required to contribute to the http-tunnel project. For full authorship information, see the version control history. Except as otherwise noted (below and/or in individual files), http-tunnel is licensed under the Apache License, Version 2.0 or or the MIT license or , at your option. ================================================ FILE: Cargo.toml ================================================ [package] name = "http-tunnel" version = "0.1.12" authors = ["Eugene Retunsky"] license = "MIT OR Apache-2.0" edition = "2021" publish = true readme = "README.md" repository = "https://github.com/xnuter/http-tunnel" homepage = "https://github.com/xnuter/http-tunnel" description = """ HTTP Tunnel/TCP Proxy example written in Rust. """ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] tokio-native-tls = "0.3" native-tls = "0.2" clap = { version = "3.1.6", features = ["derive"] } regex = "1.3" rand = "0.8" yaml-rust = "0.4" serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" serde_yaml = "0.8" serde_json = "1.0" serde_regex = "1.1" humantime-serde = "1.0" async-trait = "0.1" strum = "0.19" strum_macros = "0.19" derive_builder = "0.9" log = "0.4" log4rs = "1.0.0-alpha-1" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.6", features = ["full"] } bytes = "1" futures = "0.3" time = "0.1" [dev-dependencies] tokio-test = "0.4" [features] # For legacy software you can enable plain_text tunnelling default = [] plain_text = [] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ [![Crate](https://img.shields.io/crates/v/http-tunnel.svg)](https://crates.io/crates/http-tunnel) ![Clippy/Fmt](https://github.com/xnuter/http-tunnel/workflows/Clippy/Fmt/badge.svg) ![Tests](https://github.com/xnuter/http-tunnel/workflows/Tests/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/xnuter/http-tunnel/badge.svg?branch=main)](https://coveralls.io/github/xnuter/http-tunnel?branch=main) ### Overview An implementation of [HTTP Tunnel](https://en.wikipedia.org/wiki/HTTP_tunnel) in Rust, which can also function as a TCP proxy. The core code is entirely abstract from the tunnel protocol or transport protocols. In this example, it supports both `HTTP` and `HTTPS` with minimal additional code. *Please note*, this tunnel doesn't allow tunneling of plain text over HTTP tunnels (only HTTPS connections can be tunneled). If you need this functionality you need to build the `http-tunnel` with the `plain_text` feature: ```bash cargo build --release --features plain_text ``` E.g. it can be extended to run the tunnel over `QUIC+HTTP/3` or connect to another tunnel (as long as `AsyncRead + AsyncWrite` is satisfied for the implementation). You can check [benchmarks](https://github.com/xnuter/perf-gauge/wiki/Benchmarking-TCP-Proxies-written-in-different-languages:-C,-CPP,-Rust,-Golang,-Java,-Python). [Read more](https://medium.com/@xnuter/writing-a-modern-http-s-tunnel-in-rust-56e70d898700) about the design. ### Quick overview of source files * `configuration.rs` - contains configuration structures + a basic CLI * see `config/` with configuration files/TLS materials * `http_tunnel_codec.rs` - a codec to process the initial HTTP request and encode a corresponding response. * `proxy_target.rs` - an abstraction + basic TCP implementation to connect target servers. * contains a DNS resolver with a basic caching strategy (cache for a given `TTL`) * `relay.rs` - relaying data from one stream to another, `tunnel = upstream_relay + downstream_relay` * also, contains basic `relay_policy` * `tunnel.rs` - a tunnel. It's built from: * a tunnel handshake codec (e.g. `HttpTunnelCodec`) * a target connector * client connection as a stream * `main.rs` - application. May start `HTTP` or `HTTPS` tunnel (based on the command line parameters). * emits log to `logs/application.log` (`log/` contains the actual output of the app from the browser session) * metrics to `logs/metrics.log` - very basic, to demonstrate the concept.` ### Run demo Install via `cargo`: ``` cargo install http-tunnel ``` Now you can start it without any configuration: ``` $ http-tunnel --bind 0.0.0.0:8080 http ``` There are three modes. * `HTTPS`: ``` $ http-tunnel --config ./config/config.yaml \ --bind 0.0.0.0:8443 \ https --pk "./config/domain.pfx" --password "6B9mZ*1hJ#xk" ``` * `HTTP`: ``` $ http-tunnel --config ./config/config-browser.yaml --bind 0.0.0.0:8080 http ``` * `TCP Proxy`: ``` $ http-tunnel --config ./config/config-browser.yaml --bind 0.0.0.0:8080 tcp --destination $REMOTE_HOST:$REMOTE_PORT ``` ### Testing with a browser (HTTP) In Firefox, you can set the HTTP proxy to `localhost:8080`. Make sure you run it with the right configuration: https://support.mozilla.org/en-US/kb/connection-settings-firefox (use HTTP Proxy and check "use this proxy for FTP and HTTPS") ``` $ ./target/release/http-tunnel --config ./config/config-browser.yaml --bind 0.0.0.0:8080 http ``` ### Testing with cURL (HTTPS) This proxy can be tested with `cURL`: Add `simple.rust-http-tunnel.org'` to `/etc/hosts`: ``` $ echo '127.0.0.1 simple.rust-http-tunnel.org' | sudo tee -a /etc/hosts ``` Then try access-listed targets (see `./config/config.yaml`), e.g: ``` curl -vp --proxy https://simple.rust-http-tunnel.org:8443 --proxy-cacert ./config/domain.crt https://www.wikipedia.org ``` You can also play around with targets that are not allowed. ### Privacy The application cannot see the plaintext data. The application doesn't log any information that may help identify clients (such as IP, auth tokens). Only general information (events, errors, data sizes) is logged for monitoring purposes. #### DDoS protection * `Slowloris` attack (opening tons of slow connections) * Sending requests resulting in large responses Some of them can be solved by introducing rate/age limits and inactivity timeouts. ### Build Install `cargo` - [follow these instructions](https://doc.rust-lang.org/cargo/getting-started/installation.html) On `Debian` to fix [OpenSSL build issue](https://docs.rs/openssl/0.10.30/openssl/): ``` sudo apt-get install pkg-config libssl-dev ``` ### Installation On MacOS: ``` curl https://sh.rustup.rs -sSf | sh cargo install http-tunnel http-tunnel --bind 0.0.0.0:8080 http ``` On Debian based Linux: ``` curl https://sh.rustup.rs -sSf | sh sudo apt-get -y install gcc pkg-config libssl-dev cargo install http-tunnel http-tunnel --bind 0.0.0.0:8080 http ``` ================================================ FILE: config/README.md ================================================ ### PCKS12 generation In case you want to generate your own TLS materials. These self-signed certs are generated for `simple.rust-http-tunnel.org` domain. Generate cert/pk: ``` openssl req \ -newkey rsa:2048 -nodes -keyout domain.key \ -x509 -days 365 -out domain.crt ``` Create a `pkcs12` file: ``` openssl pkcs12 \ -inkey domain.key \ -in domain.crt \ -export -out domain.pfx ``` ================================================ FILE: config/config-browser.yaml ================================================ # a configuration to test it with your browser # set `localhost:8080` as HTTP/HTTPS proxy (run it in HTTP mode) client_connection: initiation_timeout: 100s relay_policy: idle_timeout: 300s min_rate_bpm: 0 max_rate_bps: 10000000 target_connection: dns_cache_ttl: 60s allowed_targets: ".*" # anything connect_timeout: 100s relay_policy: idle_timeout: 100s min_rate_bpm: 0 max_rate_bps: 10000000 ================================================ FILE: config/config.yaml ================================================ client_connection: initiation_timeout: 10s # we want to make sure connections are not under-utilized or over-utilized relay_policy: idle_timeout: 30s min_rate_bpm: 1000 max_rate_bps: 10000 target_connection: dns_cache_ttl: 60s allowed_targets: "^(?i)([a-z]+)\\.(wikipedia|rust-lang)\\.org:443$" connect_timeout: 10s relay_policy: idle_timeout: 10s min_rate_bpm: 1000 max_rate_bps: 10000 # Other traffic policies may go here # max_lifetime: 100s # max_total_payload: 10mb # we can extend this with TCP policies, if necessary. E.g. # tcp_policies: # keep_idle: 60s # linger, nodelay, .... ================================================ FILE: config/domain.crt ================================================ -----BEGIN CERTIFICATE----- MIID0DCCArgCCQC39O6tC7bK5DANBgkqhkiG9w0BAQsFADCBqTELMAkGA1UEBhMC VVMxEzARBgNVBAgMCldhc2hpbmd0b24xETAPBgNVBAcMCEJlbGxldnVlMQ8wDQYD VQQKDAZ4bnV0ZXIxGDAWBgNVBAsMD0V1Z2VuZSBSZXR1bnNreTEkMCIGA1UEAwwb c2ltcGxlLnJ1c3QtaHR0cC10dW5uZWwub3JnMSEwHwYJKoZIhvcNAQkBFhJyZXR1 bnNreUBnbWFpbC5jb20wHhcNMjAwOTEzMDAwNjI2WhcNMzAwOTExMDAwNjI2WjCB qTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xETAPBgNVBAcMCEJl bGxldnVlMQ8wDQYDVQQKDAZ4bnV0ZXIxGDAWBgNVBAsMD0V1Z2VuZSBSZXR1bnNr eTEkMCIGA1UEAwwbc2ltcGxlLnJ1c3QtaHR0cC10dW5uZWwub3JnMSEwHwYJKoZI hvcNAQkBFhJyZXR1bnNreUBnbWFpbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB DwAwggEKAoIBAQCiyxVy9mmbJVRjAK6sOkcU21lmZRUQEa8LZx09tuU89K81+CvZ JLA/QjBPQq3pbIxWYIcb+pjro1kPqGw/z3gEcpTF6v5b//DbSCi/BGAHTRyaGqJ4 GHrh8kMmnEcxrzX5hIONwgnOP1b5s5Ih+0Psiyh+cwItJWuVemwa/BWmIxZEqpC/ m4vKzRTV5a50PTH3LCW9bHKzuj7OIU+dfTiR0CVJIY2tJjZldflXdhmzWxFAb5jf QFNXKqMfc4lCGYz37dyDZf4m56FKEa5c41r32dMlq0PIlqWA41gtCr7zhDUB9yk9 zU61MmGRTk7qAFB1SakwMVQl5bFn8N3U4Em5AgMBAAEwDQYJKoZIhvcNAQELBQAD ggEBAJasp7TuZcv+vDljgm/TMr8+x+8qY9v7hfefqNz+p1LNqvpRsC4m9immTYOM aw0FUUv20Syb4thxAE52GImpboqVwiYNK0huX62Htvs8DnxsiOs7y/rC29MxQMiR FkbYVZFsUOVVRCru8ZORRT1io7NnlueQN3s7/NOCl5XYssoH1868/+TqPxGG9rQw mk+KqDRDk2CNsixxu7MYpRFWd5+mglYwS9EJRLXfMgjJIYBQ+ID3jNP39qK7lnxa jb1rKBK0g04XtBUkKH3Y8V11JRD+I/Ylnn1qkg8ZJAoMTQV4TMgh4hQWFYOAgHWC eGIwbeixjlk3N6pdYCaGLMfZkhk= -----END CERTIFICATE----- ================================================ FILE: config/domain.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCiyxVy9mmbJVRj AK6sOkcU21lmZRUQEa8LZx09tuU89K81+CvZJLA/QjBPQq3pbIxWYIcb+pjro1kP qGw/z3gEcpTF6v5b//DbSCi/BGAHTRyaGqJ4GHrh8kMmnEcxrzX5hIONwgnOP1b5 s5Ih+0Psiyh+cwItJWuVemwa/BWmIxZEqpC/m4vKzRTV5a50PTH3LCW9bHKzuj7O IU+dfTiR0CVJIY2tJjZldflXdhmzWxFAb5jfQFNXKqMfc4lCGYz37dyDZf4m56FK Ea5c41r32dMlq0PIlqWA41gtCr7zhDUB9yk9zU61MmGRTk7qAFB1SakwMVQl5bFn 8N3U4Em5AgMBAAECggEADLSjAO0Agw5fzrZP67tErvkLujUrdqyap94tZxKuQ5qp TiIHchQt+VH2KUl//1bsgRVZljJx2vpNyi4P/M75hAdZWzUjExUfvE2eeIIj6I35 LIHlqk/mz1m0KgBKgjM1mDridZ7uWv2QkT6VqjdNLtoRmATr55AjHHCInXaNTgER LiSYJdxOubllLmXUuRZk/7/R1hDajmcR06QGJs994+/9ZyWzow82VuCJLUzFRSF3 kj3Cqb1p9ShpL2e/DN+QpgNq1gs7b3YDNl2fgix6c7V2LxM/G6qfT+i4rGAUhONN 9Jx49cjTA7TsjD+NvoFY0GvVt8Gn4igaviVZA9jWAQKBgQDPNKxQd3PLDWujsHIk bepG6Bm4S3/mCmsVnSvqaIsEXJQgAdMXt60J5vo0pNFrxIcrQAp+7JJPtdMm/uOB TFOosV2Y8oDwhPXboTI/QHtnS6624ziUJbF2qfVRGbIPdu76OVv/7PCxZsUhDBjI F0ZN9iFP5y7cLA7e2JHrDDQrwQKBgQDJIQVkvqXyYvx/hUtYB6tZqOGZvnbaeXsx Q3vv5omRE8H8AzwaiBL0PBMp2tXMhIl9nTtD6femtf5UpdihFB8z7KwmEGp1pka4 rZXrJGofZ+fzPh9LpJxDSHBgNTEaJHSn2CZeZ6UR/mbVQcnFlNyT2aneSRdOmEtk MJLkHBx7+QKBgQCYLvGYMAOl0PeLw94xj2EQLwwk5Z7MUD6SI1vL0Hi5/Vz1nSFz O/4lVbXS0HLXmgJE68ZJrmtPjBXHgFGL94lCTvKVkRbOkHkalGwZNLzuAxIRVRWL CZwrsWxx4lN7NDkVIufFMjsdsIN8YCwbWazTOcEBtKQgJWPOnHWfktkGgQKBgQC9 ytY/GhSYZMYmQ480k5AjPFUe8ndPdIFGnIrQd/hqmX1dJWRLGQrhw+rFfUZxBsSD b6KkVJ0oiOZl1FZWshk7s2NDTAxZ1r03uj4VNTibSD6972oyxDPc3feFIcyjAbG/ TR3vydgf4bQCG2Gee/ml3ykHpGtE9Dt4YMnMTaanaQKBgQC7kiA/r0Ob05OCI5iz LyhLLBJneA4OeSoEq64HHnjOfKxsfe6d/SDmUAvwcvB+2GO5B7XubMu5nrg47izp ny/eAVVmMYRzCY7mrCt5CC/1DBtdkAgdjmXXFlGZ74wjl6O65ZKsgIbkn41dGR2N j+2D+93dPvODA2LMD1VaRFBqQQ== -----END PRIVATE KEY----- ================================================ FILE: config/log4rs.yaml ================================================ refresh_rate: 30 seconds appenders: stdout: kind: console metrics: kind: rolling_file path: log/metrics.log policy: kind: compound roller: kind: fixed_window count: 10 base: 1 pattern: log/metrics.{}.log trigger: kind: size limit: 10mb encoder: pattern: "{d} - {m}{n}" application: kind: rolling_file path: log/application.log policy: kind: compound roller: kind: fixed_window count: 10 base: 1 pattern: log/application.{}.log trigger: kind: size limit: 10mb encoder: pattern: "{d} - [{l}] {f}:{L} - {m}{n}" root: level: info appenders: - application loggers: metrics: level: info appenders: - metrics additive: false ================================================ FILE: misc/diagrams/components-high-level.puml ================================================ @startuml Client <--> Tunnel: L4 handshake Client -> Tunnel: Negotiate Target activate Tunnel Tunnel <-> Target: L4 Handshake activate Target Tunnel -> Client: Success group Full-Duplex, endless loop Client --> Tunnel: upstream Tunnel --> Target: upstream Tunnel <-- Target: downstream Client <-- Tunnel: downstream end deactivate Tunnel deactivate Target @enduml ================================================ FILE: misc/diagrams/components.puml ================================================ @startuml Client <--> Tunnel: TCP handshake group HTTPS Tunnel only Client <--> Tunnel: TLS handshake end Client -> Tunnel: HTTP CONNECT (target) activate Tunnel Tunnel <-> Target: TCP Handshake activate Target Tunnel -> Client: 200 OK group Full-Duplex, endless loop Client --> Tunnel: upstream Tunnel --> Target: upstream Tunnel <-- Target: downstream Client <-- Tunnel: downstream end deactivate Tunnel deactivate Target @enduml ================================================ FILE: src/configuration.rs ================================================ /// Copyright 2020 Developers of the http-tunnel project. /// /// Licensed under the Apache License, Version 2.0 or the MIT license /// , at your /// option. This file may not be copied, modified, or distributed /// except according to those terms. use crate::relay::{RelayPolicy, NO_BANDWIDTH_LIMIT, NO_TIMEOUT}; use clap::Args; use clap::Parser; use clap::Subcommand; use log::{error, info}; use native_tls::Identity; use regex::Regex; use std::fs::File; use std::io::{Error, ErrorKind, Read}; use std::time::Duration; use tokio::io; #[derive(Deserialize, Clone)] pub struct ClientConnectionConfig { #[serde(with = "humantime_serde")] pub initiation_timeout: Duration, pub relay_policy: RelayPolicy, } #[derive(Deserialize, Clone)] pub struct TargetConnectionConfig { #[serde(with = "humantime_serde")] pub dns_cache_ttl: Duration, #[serde(with = "serde_regex")] pub allowed_targets: Regex, #[serde(with = "humantime_serde")] pub connect_timeout: Duration, pub relay_policy: RelayPolicy, } #[derive(Deserialize, Clone)] pub struct TunnelConfig { pub client_connection: ClientConnectionConfig, pub target_connection: TargetConnectionConfig, } #[derive(Clone)] pub enum ProxyMode { Http, Https(Identity), Tcp(String), } #[derive(Clone, Builder)] pub struct ProxyConfiguration { pub mode: ProxyMode, pub bind_address: String, pub tunnel_config: TunnelConfig, } #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] struct Cli { /// Configuration file. #[clap(long)] config: Option, /// Bind address, e.g. 0.0.0.0:8443. #[clap(long)] bind: String, #[clap(subcommand)] command: Commands, } #[derive(Subcommand, Debug)] enum Commands { Http(HttpOptions), Https(HttpsOptions), Tcp(TcpOptions), } #[derive(Args, Debug)] #[clap(about = "Run the tunnel in HTTP mode", long_about = None)] #[clap(author, version, long_about = None)] #[clap(propagate_version = true)] struct HttpOptions {} #[derive(Args, Debug)] #[clap(about = "Run the tunnel in HTTPS mode", long_about = None)] #[clap(author, version, long_about = None)] #[clap(propagate_version = true)] struct HttpsOptions { /// pkcs12 filename. #[clap(long)] pk: String, /// Password for the pkcs12 file. #[clap(long)] password: String, } #[derive(Args, Debug)] #[clap(about = "Run the tunnel in TCP proxy mode", long_about = None)] #[clap(author, version, long_about = None)] #[clap(propagate_version = true)] struct TcpOptions { /// Destination address, e.g. 10.0.0.2:8443. #[clap(short, long)] destination: String, } impl Default for TunnelConfig { fn default() -> Self { // by default no restrictions Self { client_connection: ClientConnectionConfig { initiation_timeout: NO_TIMEOUT, relay_policy: RelayPolicy { idle_timeout: NO_TIMEOUT, min_rate_bpm: 0, max_rate_bps: NO_BANDWIDTH_LIMIT, }, }, target_connection: TargetConnectionConfig { dns_cache_ttl: NO_TIMEOUT, allowed_targets: Regex::new(".*").expect("Bug: bad default regexp"), connect_timeout: NO_TIMEOUT, relay_policy: RelayPolicy { idle_timeout: NO_TIMEOUT, min_rate_bpm: 0, max_rate_bps: NO_BANDWIDTH_LIMIT, }, }, } } } impl ProxyConfiguration { /// For this demo the app reads the key/certs from the disk. /// In production more secure approaches should be used (at least encryption with regularly /// rotated keys, storing sensitive data on RAM disk only, etc.) pub fn from_command_line() -> io::Result { let cli: Cli = Cli::parse(); let config = cli.config; let bind_address = cli.bind; let mode = match cli.command { Commands::Http(_) => { info!( "Starting in HTTP mode: bind: {}, configuration: {:?}", bind_address, config ); ProxyMode::Http } Commands::Https(https) => { let pkcs12_file = https.pk.as_str(); let password = https.password.as_str(); let identity = ProxyConfiguration::tls_identity_from_file(pkcs12_file, password)?; info!( "Starting in HTTPS mode: pkcs12: {}, password: {}, bind: {}, configuration: {:?}", pkcs12_file, !password.is_empty(), bind_address, config ); ProxyMode::Https(identity) } Commands::Tcp(tcp) => { let destination = tcp.destination; info!( "Starting in TCP mode: destination: {}, configuration: {:?}", destination, config ); ProxyMode::Tcp(destination) } }; let tunnel_config = match config { None => TunnelConfig::default(), Some(config) => ProxyConfiguration::read_tunnel_config(config.as_str())?, }; Ok(ProxyConfigurationBuilder::default() .bind_address(bind_address) .mode(mode) .tunnel_config(tunnel_config) .build() .expect("ProxyConfigurationBuilder failed")) } fn tls_identity_from_file(filename: &str, password: &str) -> io::Result { let mut file = File::open(filename).map_err(|e| { error!("Error opening PKSC12 file {}: {}", filename, e); e })?; let mut identity = vec![]; file.read_to_end(&mut identity).map_err(|e| { error!("Error reading file {}: {}", filename, e); e })?; Identity::from_pkcs12(&identity, password).map_err(|e| { error!("Cannot process PKCS12 file {}: {}", filename, e); Error::from(ErrorKind::InvalidInput) }) } fn read_tunnel_config(filename: &str) -> io::Result { let mut file = File::open(filename).map_err(|e| { error!("Error opening config file {}: {}", filename, e); e })?; let mut yaml = vec![]; file.read_to_end(&mut yaml).map_err(|e| { error!("Error reading file {}: {}", filename, e); e })?; let result: TunnelConfig = serde_yaml::from_slice(&yaml).map_err(|e| { error!("Error parsing yaml {}: {}", filename, e); Error::from(ErrorKind::InvalidInput) })?; Ok(result) } } ================================================ FILE: src/http_tunnel_codec.rs ================================================ /// Copyright 2020 Developers of the http-tunnel project. /// /// Licensed under the Apache License, Version 2.0 or the MIT license /// , at your /// option. This file may not be copied, modified, or distributed /// except according to those terms. use std::fmt::Write; use async_trait::async_trait; use bytes::BytesMut; use log::debug; use regex::Regex; use tokio::io::{Error, ErrorKind}; use tokio_util::codec::{Decoder, Encoder}; use crate::proxy_target::Nugget; use crate::tunnel::{EstablishTunnelResult, TunnelCtx, TunnelTarget}; use core::fmt; use std::str::Split; const REQUEST_END_MARKER: &[u8] = b"\r\n\r\n"; /// A reasonable value to limit possible header size. const MAX_HTTP_REQUEST_SIZE: usize = 16384; /// HTTP/1.1 request representation /// Supports only `CONNECT` method, unless the `plain_text` feature is enabled struct HttpConnectRequest { uri: String, nugget: Option, // out of scope of this demo, but let's put it here for extensibility // e.g. Authorization/Policies headers // headers: Vec<(String, String)>, } #[derive(Builder, Eq, PartialEq, Debug, Clone)] pub struct HttpTunnelTarget { pub target: String, pub nugget: Option, // easily can be extended with something like // policies: Vec } /// Codec to extract `HTTP/1.1 CONNECT` requests and build a corresponding `HTTP` response. #[derive(Clone, Builder)] pub struct HttpTunnelCodec { tunnel_ctx: TunnelCtx, enabled_targets: Regex, } impl Decoder for HttpTunnelCodec { type Item = HttpTunnelTarget; type Error = EstablishTunnelResult; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { if !got_http_request(src) { return Ok(None); } match HttpConnectRequest::parse(src) { Ok(parsed_request) => { if !self.enabled_targets.is_match(&parsed_request.uri) { debug!( "Target `{}` is not allowed. Allowed: `{}`, CTX={}", parsed_request.uri, self.enabled_targets, self.tunnel_ctx ); Err(EstablishTunnelResult::Forbidden) } else { Ok(Some( HttpTunnelTargetBuilder::default() .target(parsed_request.uri) .nugget(parsed_request.nugget) .build() .expect("HttpTunnelTargetBuilder failed"), )) } } Err(e) => Err(e), } } } impl Encoder for HttpTunnelCodec { type Error = std::io::Error; fn encode( &mut self, item: EstablishTunnelResult, dst: &mut BytesMut, ) -> Result<(), Self::Error> { let (code, message) = match item { EstablishTunnelResult::Ok => (200, "OK"), EstablishTunnelResult::OkWithNugget => { // do nothing, the upstream should respond instead return Ok(()); } EstablishTunnelResult::BadRequest => (400, "BAD_REQUEST"), EstablishTunnelResult::Forbidden => (403, "FORBIDDEN"), EstablishTunnelResult::OperationNotAllowed => (405, "NOT_ALLOWED"), EstablishTunnelResult::RequestTimeout => (408, "TIMEOUT"), EstablishTunnelResult::TooManyRequests => (429, "TOO_MANY_REQUESTS"), EstablishTunnelResult::ServerError => (500, "SERVER_ERROR"), EstablishTunnelResult::BadGateway => (502, "BAD_GATEWAY"), EstablishTunnelResult::GatewayTimeout => (504, "GATEWAY_TIMEOUT"), }; dst.write_fmt(format_args!("HTTP/1.1 {} {}\r\n\r\n", code as u32, message)) .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) } } #[async_trait] impl TunnelTarget for HttpTunnelTarget { type Addr = String; fn target_addr(&self) -> Self::Addr { self.target.clone() } fn has_nugget(&self) -> bool { self.nugget.is_some() } fn nugget(&self) -> &Nugget { self.nugget .as_ref() .expect("Cannot use this method without checking `has_nugget`") } } // cov:begin-ignore-line impl fmt::Display for HttpTunnelTarget { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.target) } } // cov:end-ignore-line #[cfg(not(feature = "plain_text"))] fn got_http_request(buffer: &BytesMut) -> bool { buffer.len() >= MAX_HTTP_REQUEST_SIZE || buffer.ends_with(REQUEST_END_MARKER) } #[cfg(feature = "plain_text")] fn got_http_request(buffer: &BytesMut) -> bool { buffer.len() >= MAX_HTTP_REQUEST_SIZE || buffer .windows(REQUEST_END_MARKER.len()) .find(|w| *w == REQUEST_END_MARKER) .is_some() } impl From for EstablishTunnelResult { fn from(e: Error) -> Self { match e.kind() { ErrorKind::TimedOut => EstablishTunnelResult::GatewayTimeout, _ => EstablishTunnelResult::BadGateway, } } } /// Basic HTTP Request parser which only purpose is to parse `CONNECT` requests. impl HttpConnectRequest { pub fn parse(http_request: &[u8]) -> Result { HttpConnectRequest::precondition_size(http_request)?; HttpConnectRequest::precondition_legal_characters(http_request)?; let http_request_as_string = String::from_utf8(http_request.to_vec()).expect("Contains only ASCII"); let mut lines = http_request_as_string.split("\r\n"); let request_line = HttpConnectRequest::parse_request_line( lines .next() .expect("At least a single line is present at this point"), )?; let has_nugget = request_line.3; if has_nugget { Ok(Self { uri: HttpConnectRequest::extract_destination_host(&mut lines, request_line.1) .unwrap_or_else(|| request_line.1.to_string()), nugget: Some(Nugget::new(http_request)), }) } else { Ok(Self { uri: request_line.1.to_string(), nugget: None, }) } } fn extract_destination_host(lines: &mut Split<&str>, endpoint: &str) -> Option { const HOST_HEADER: &str = "host:"; lines .find(|line| line.to_ascii_lowercase().starts_with(HOST_HEADER)) .map(|line| line[HOST_HEADER.len()..].trim()) .map(|host| { let mut host = String::from(host); if host.rfind(':').is_none() { let default_port = if endpoint.to_ascii_lowercase().starts_with("https://") { ":443" } else { ":80" }; host.push_str(default_port); } host }) } fn parse_request_line( request_line: &str, ) -> Result<(&str, &str, &str, bool), EstablishTunnelResult> { let request_line_items = request_line.split(' ').collect::>(); HttpConnectRequest::precondition_well_formed(request_line, &request_line_items)?; let method = request_line_items[0]; let uri = request_line_items[1]; let version = request_line_items[2]; let has_nugget = HttpConnectRequest::check_method(method)?; HttpConnectRequest::check_version(version)?; Ok((method, uri, version, has_nugget)) } fn precondition_well_formed( request_line: &str, request_line_items: &[&str], ) -> Result<(), EstablishTunnelResult> { if request_line_items.len() != 3 { debug!("Bad request line: `{:?}`", request_line,); Err(EstablishTunnelResult::BadRequest) } else { Ok(()) } } fn check_version(version: &str) -> Result<(), EstablishTunnelResult> { if version != "HTTP/1.1" { debug!("Bad version {}", version); Err(EstablishTunnelResult::BadRequest) } else { Ok(()) } } #[cfg(not(feature = "plain_text"))] fn check_method(method: &str) -> Result { if method != "CONNECT" { debug!("Not allowed method {}", method); Err(EstablishTunnelResult::OperationNotAllowed) } else { Ok(false) } } #[cfg(feature = "plain_text")] fn check_method(method: &str) -> Result { Ok(method != "CONNECT") } fn precondition_legal_characters(http_request: &[u8]) -> Result<(), EstablishTunnelResult> { for b in http_request { match b { // non-ascii characters don't make sense in this context 32..=126 | 9 | 10 | 13 => {} _ => { debug!("Bad request header. Illegal character: {:#04x}", b); return Err(EstablishTunnelResult::BadRequest); } } } Ok(()) } fn precondition_size(http_request: &[u8]) -> Result<(), EstablishTunnelResult> { if http_request.len() >= MAX_HTTP_REQUEST_SIZE { debug!( "Bad request header. Size {} exceeds limit {}", http_request.len(), MAX_HTTP_REQUEST_SIZE ); Err(EstablishTunnelResult::BadRequest) } else { Ok(()) } } } #[cfg(test)] mod tests { use bytes::{BufMut, BytesMut}; use regex::Regex; use tokio_util::codec::{Decoder, Encoder}; use crate::http_tunnel_codec::{ EstablishTunnelResult, HttpTunnelCodec, HttpTunnelCodecBuilder, HttpTunnelTargetBuilder, MAX_HTTP_REQUEST_SIZE, REQUEST_END_MARKER, }; #[cfg(feature = "plain_text")] use crate::proxy_target::Nugget; #[cfg(feature = "plain_text")] use crate::tunnel::EstablishTunnelResult::Forbidden; use crate::tunnel::TunnelCtxBuilder; #[test] fn test_got_http_request_partial() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); let result = codec.decode(&mut buffer); assert_eq!(result, Ok(None)); buffer.put_slice(b"CONNECT foo.bar.com:443 HTTP/1.1"); let result = codec.decode(&mut buffer); assert_eq!(result, Ok(None)); } #[test] fn test_got_http_request_full() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"CONNECT foo.bar.com:443 HTTP/1.1"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert_eq!( result, Ok(Some( HttpTunnelTargetBuilder::default() .target("foo.bar.com:443".to_string()) .nugget(None) .build() .unwrap(), )) ); } #[test] fn test_got_http_request_exceeding() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); while buffer.len() <= MAX_HTTP_REQUEST_SIZE { buffer.put_slice(b"CONNECT foo.bar.com:443 HTTP/1.1\r\n"); } let result = codec.decode(&mut buffer); assert_eq!(result, Err(EstablishTunnelResult::BadRequest)); } #[test] fn test_parse_valid() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"CONNECT foo.bar.com:443 HTTP/1.1"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert!(result.is_ok()); } #[test] fn test_parse_valid_with_headers() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice( b"CONNECT foo.bar.com:443 HTTP/1.1\r\n\ Host: ignored\r\n\ Auithorization: ignored", ); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert!(result.is_ok()); } #[test] #[cfg(not(feature = "plain_text"))] fn test_parse_not_allowed_method() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"GET foo.bar.com:443 HTTP/1.1"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert_eq!(result, Err(EstablishTunnelResult::OperationNotAllowed)); } #[test] #[cfg(feature = "plain_text")] fn test_parse_plain_text_method() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"GET https://foo.bar.com:443/get HTTP/1.1\r\n"); buffer.put_slice(b"connection: keep-alive\r\n"); buffer.put_slice(b"Host: \tfoo.bar.com:443 \t\r\n"); buffer.put_slice(b"User-Agent: whatever"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert!(result.is_ok()); let result = result.unwrap(); assert!(result.is_some()); assert_eq!(result.unwrap().target, "foo.bar.com:443"); } #[test] #[cfg(feature = "plain_text")] fn test_parse_plain_text_default_https_port() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"GET https://foo.bar.com/get HTTP/1.1\r\n"); buffer.put_slice(b"connection: keep-alive\r\n"); buffer.put_slice(b"Host: \tfoo.bar.com \t\r\n"); buffer.put_slice(b"User-Agent: whatever"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert!(result.is_ok()); let result = result.unwrap(); assert!(result.is_some()); assert_eq!(result.unwrap().target, "foo.bar.com:443"); } #[test] #[cfg(feature = "plain_text")] fn test_parse_plain_text_default_http_port() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"GET http://foo.bar.com/get HTTP/1.1\r\n"); buffer.put_slice(b"connection: keep-alive\r\n"); buffer.put_slice(b"Host: \tfoo.bar.com \t\r\n"); buffer.put_slice(b"User-Agent: whatever"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert!(result.is_ok()); let result = result.unwrap(); assert!(result.is_some()); assert_eq!(result.unwrap().target, "foo.bar.com:80"); } #[test] #[cfg(feature = "plain_text")] fn test_parse_plain_text_nugget() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"GET https://foo.bar.com:443/get HTTP/1.1\r\n"); buffer.put_slice(b"connection: keep-alive\r\n"); buffer.put_slice(b"Host: \tfoo.bar.com:443 \t\r\n"); buffer.put_slice(b"User-Agent: whatever"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert!(result.is_ok()); let result = result.unwrap(); assert!(result.is_some()); let result = result.unwrap(); assert!(result.nugget.is_some()); let nugget = result.nugget.unwrap(); assert_eq!(nugget, Nugget::new(buffer.to_vec())); } #[test] #[cfg(feature = "plain_text")] fn test_parse_plain_text_with_body() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"POST https://foo.bar.com:443/get HTTP/1.1\r\n"); buffer.put_slice(b"connection: keep-alive\r\n"); buffer.put_slice(b"Host: \tfoo.bar.com:443 \t\r\n"); buffer.put_slice(b"User-Agent: whatever"); buffer.put_slice(REQUEST_END_MARKER); buffer.put_slice(b"{body: 'some json body'}"); let result = codec.decode(&mut buffer); assert!(result.is_ok()); let result = result.unwrap(); assert!(result.is_some()); let result = result.unwrap(); assert!(result.nugget.is_some()); let nugget = result.nugget.unwrap(); assert_eq!(nugget, Nugget::new(buffer.to_vec())); } #[test] #[cfg(feature = "plain_text")] fn test_parse_plain_text_method_forbidden_domain() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"GET https://foo.bar.com:443/get HTTP/1.1\r\n"); buffer.put_slice(b"connection: keep-alive\r\n"); buffer.put_slice(b"Host: \tsome.uknown.site.com:443 \t\r\n"); buffer.put_slice(b"User-Agent: whatever"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert_eq!(result, Err(Forbidden)); } #[test] fn test_parse_bad_version() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(b"CONNECT foo.bar.com:443 HTTP/1.0"); buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert!(result.is_err()); let code = result.err().unwrap(); assert_eq!(code, EstablishTunnelResult::BadRequest); } #[test] fn test_parse_bad_requests() { let bad_requests = [ "bad request\r\n\r\n", // 2 tokens "yet another bad request\r\n\r\n", // 4 tokens "CONNECT foo.bar.cøm:443 HTTP/1.1\r\n\r\n", // non-ascii "CONNECT foo.bar.com:443 HTTP/1.1\r\n\r\n", // double-space "CONNECT foo.bar.com:443\tHTTP/1.1\r\n\r\n", // CTL ]; bad_requests.iter().for_each(|r| { let mut codec = build_codec(); let mut buffer = BytesMut::new(); buffer.put_slice(r.as_bytes()); let result = codec.decode(&mut buffer); assert_eq!( result, Err(EstablishTunnelResult::BadRequest), "Didn't reject {r}" ); }); } #[test] fn test_parse_request_exceeds_size() { let mut codec = build_codec(); let mut buffer = BytesMut::new(); while !buffer.len() <= MAX_HTTP_REQUEST_SIZE { buffer.put_slice(b"CONNECT foo.bar.com:443 HTTP/1.1\r\n"); } buffer.put_slice(REQUEST_END_MARKER); let result = codec.decode(&mut buffer); assert_eq!(result, Err(EstablishTunnelResult::BadRequest)); } #[test] fn test_http_tunnel_encoder() { let mut codec = build_codec(); let pattern = Regex::new(r"^HTTP/1\.1 ([2-5][\d]{2}) [A-Z_]{2,20}\r\n\r\n").unwrap(); for code in &[ EstablishTunnelResult::Ok, EstablishTunnelResult::BadGateway, EstablishTunnelResult::Forbidden, EstablishTunnelResult::GatewayTimeout, EstablishTunnelResult::OperationNotAllowed, EstablishTunnelResult::RequestTimeout, EstablishTunnelResult::ServerError, EstablishTunnelResult::TooManyRequests, ] { let mut buffer = BytesMut::new(); let encoded = codec.encode(code.clone(), &mut buffer); assert!(encoded.is_ok()); let str = String::from_utf8(Vec::from(&buffer[..])).expect("Must be valid ASCII"); assert!(pattern.is_match(&str), "Malformed response `{code:?}`"); } } fn build_codec() -> HttpTunnelCodec { let ctx = TunnelCtxBuilder::default().id(1).build().unwrap(); HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar\.com:(443|80)").unwrap()) .build() .unwrap() } } ================================================ FILE: src/main.rs ================================================ /// Copyright 2020 Developers of the http-tunnel project. /// /// Licensed under the Apache License, Version 2.0 or the MIT license /// , at your /// option. This file may not be copied, modified, or distributed /// except according to those terms. #[macro_use] extern crate derive_builder; #[macro_use] extern crate serde_derive; use log::{error, info, LevelFilter}; use rand::{thread_rng, Rng}; use tokio::io; use tokio::net::{TcpListener, TcpStream}; use tokio::time::timeout; use tokio_native_tls::TlsAcceptor; use crate::configuration::{ProxyConfiguration, ProxyMode}; use crate::http_tunnel_codec::{HttpTunnelCodec, HttpTunnelCodecBuilder, HttpTunnelTarget}; use crate::proxy_target::{SimpleCachingDnsResolver, SimpleTcpConnector, TargetConnector}; use crate::tunnel::{ relay_connections, ConnectionTunnel, TunnelCtx, TunnelCtxBuilder, TunnelStats, }; use log4rs::append::console::ConsoleAppender; use log4rs::config::{Appender, Root}; use log4rs::Config; use std::io::{Error, ErrorKind}; use tokio::io::{AsyncRead, AsyncWrite}; mod configuration; mod http_tunnel_codec; mod proxy_target; mod relay; mod tunnel; type DnsResolver = SimpleCachingDnsResolver; #[tokio::main] async fn main() -> io::Result<()> { init_logger(); let proxy_configuration = ProxyConfiguration::from_command_line().map_err(|e| { println!("Failed to process parameters. See ./log/application.log for details"); e })?; info!("Starting listener on: {}", proxy_configuration.bind_address); let dns_resolver = SimpleCachingDnsResolver::new( proxy_configuration .tunnel_config .target_connection .dns_cache_ttl, ); match &proxy_configuration.mode { ProxyMode::Http => { serve_plain_text(proxy_configuration, dns_resolver).await?; } ProxyMode::Https(tls_identity) => { let acceptor = native_tls::TlsAcceptor::new(tls_identity.clone()).map_err(|e| { error!("Error setting up TLS {}", e); Error::from(ErrorKind::InvalidInput) })?; let tls_acceptor = TlsAcceptor::from(acceptor); serve_tls(proxy_configuration, tls_acceptor, dns_resolver).await?; } ProxyMode::Tcp(d) => { let destination = d.clone(); serve_tcp(proxy_configuration, dns_resolver, destination).await?; } }; info!("Proxy stopped"); Ok(()) } async fn start_listening_tcp(config: &ProxyConfiguration) -> Result { let bind_address = &config.bind_address; match TcpListener::bind(bind_address).await { Ok(s) => { info!("Serving requests on: {bind_address}"); Ok(s) } Err(e) => { error!("Error binding TCP socket {bind_address}: {e}"); Err(e) } } } async fn serve_tls( config: ProxyConfiguration, tls_acceptor: TlsAcceptor, dns_resolver: DnsResolver, ) -> io::Result<()> { let listener = start_listening_tcp(&config).await?; loop { // Asynchronously wait for an inbound socket. let socket = listener.accept().await; let dns_resolver_ref = dns_resolver.clone(); match socket { Ok((stream, _)) => { stream.nodelay().unwrap_or_default(); let stream_tls_acceptor = tls_acceptor.clone(); let config = config.clone(); // handle accepted connections asynchronously tokio::spawn(async move { handle_client_tls_connection( config, stream_tls_acceptor, stream, dns_resolver_ref, ) .await }); } Err(e) => error!("Failed TCP handshake {}", e), } } } async fn serve_plain_text(config: ProxyConfiguration, dns_resolver: DnsResolver) -> io::Result<()> { let listener = start_listening_tcp(&config).await?; loop { // Asynchronously wait for an inbound socket. let socket = listener.accept().await; let dns_resolver_ref = dns_resolver.clone(); match socket { Ok((stream, _)) => { stream.nodelay().unwrap_or_default(); let config = config.clone(); // handle accepted connections asynchronously tokio::spawn(async move { tunnel_stream(&config, stream, dns_resolver_ref).await }); } Err(e) => error!("Failed TCP handshake {}", e), } } } async fn serve_tcp( config: ProxyConfiguration, dns_resolver: DnsResolver, destination: String, ) -> io::Result<()> { let listener = start_listening_tcp(&config).await?; loop { // Asynchronously wait for an inbound socket. let socket = listener.accept().await; let dns_resolver_ref = dns_resolver.clone(); let destination_copy = destination.clone(); let config_copy = config.clone(); match socket { Ok((stream, _)) => { let config = config.clone(); stream.nodelay().unwrap_or_default(); // handle accepted connections asynchronously tokio::spawn(async move { let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let mut connector: SimpleTcpConnector = SimpleTcpConnector::new( dns_resolver_ref, config.tunnel_config.target_connection.connect_timeout, ctx, ); match connector .connect(&HttpTunnelTarget { target: destination_copy, nugget: None, }) .await { Ok(destination) => { let stats = relay_connections( stream, destination, ctx, config_copy.tunnel_config.client_connection.relay_policy, config_copy.tunnel_config.target_connection.relay_policy, ) .await; report_tunnel_metrics(ctx, stats); } Err(e) => error!("Failed to establish TCP upstream connection {:?}", e), } }); } Err(e) => error!("Failed TCP handshake {}", e), } } } async fn handle_client_tls_connection( config: ProxyConfiguration, tls_acceptor: TlsAcceptor, stream: TcpStream, dns_resolver: DnsResolver, ) -> io::Result<()> { let timed_tls_handshake = timeout( config.tunnel_config.client_connection.initiation_timeout, tls_acceptor.accept(stream), ) .await; if let Ok(tls_result) = timed_tls_handshake { match tls_result { Ok(downstream) => { tunnel_stream(&config, downstream, dns_resolver).await?; } Err(e) => { error!( "Client opened a TCP connection but TLS handshake failed: {}.", e ); } } } else { error!( "Client opened TCP connection but didn't complete TLS handshake in time: {:?}.", config.tunnel_config.client_connection.initiation_timeout ); } Ok(()) } /// Tunnel via a client connection. /// This method constructs `HttpTunnelCodec` and `SimpleTcpConnector` /// to create an `HTTP` tunnel. async fn tunnel_stream( config: &ProxyConfiguration, client: C, dns_resolver: DnsResolver, ) -> io::Result<()> { let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); // here it can be any codec. let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets( config .tunnel_config .target_connection .allowed_targets .clone(), ) .build() .expect("HttpTunnelCodecBuilder failed"); // any `TargetConnector` would do. let connector: SimpleTcpConnector = SimpleTcpConnector::new( dns_resolver, config.tunnel_config.target_connection.connect_timeout, ctx, ); let stats = ConnectionTunnel::new(codec, connector, client, config.tunnel_config.clone(), ctx) .start() .await; report_tunnel_metrics(ctx, stats); Ok(()) } /// Placeholder for proper metrics emission. /// Here we just write to a file without any aggregation. fn report_tunnel_metrics(ctx: TunnelCtx, stats: io::Result) { match stats { Ok(s) => { info!(target: "metrics", "{}", serde_json::to_string(&s).expect("JSON serialization failed")); } Err(_) => error!("Failed to get stats for TID={}", ctx), } } fn init_logger() { let logger_configuration = "./config/log4rs.yaml"; if let Err(e) = log4rs::init_file(logger_configuration, Default::default()) { println!( "Cannot initialize logger from {logger_configuration}, error=[{e}]. Logging to the console."); let config = Config::builder() .appender( Appender::builder() .build("application", Box::new(ConsoleAppender::builder().build())), ) .build( Root::builder() .appender("application") .build(LevelFilter::Info), ) .unwrap(); log4rs::init_config(config).expect("Bug: bad default config"); } } ================================================ FILE: src/proxy_target.rs ================================================ /// Copyright 2020 Developers of the http-tunnel project. /// /// Licensed under the Apache License, Version 2.0 or the MIT license /// , at your /// option. This file may not be copied, modified, or distributed /// except according to those terms. use crate::tunnel::{TunnelCtx, TunnelTarget}; use async_trait::async_trait; use log::{debug, error, info}; use rand::prelude::thread_rng; use rand::Rng; use std::collections::HashMap; use std::marker::PhantomData; use std::net::SocketAddr; use std::sync::Arc; use std::time::Instant; use tokio::io; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, Error, ErrorKind}; use tokio::net::TcpStream; use tokio::sync::RwLock; use tokio::time::timeout; use tokio::time::Duration; #[async_trait] pub trait TargetConnector { type Target: TunnelTarget + Send + Sync + Sized; type Stream: AsyncRead + AsyncWrite + Send + Sized + 'static; async fn connect(&mut self, target: &Self::Target) -> io::Result; } #[async_trait] pub trait DnsResolver { async fn resolve(&mut self, target: &str) -> io::Result; } #[derive(Clone, Builder)] pub struct SimpleTcpConnector { connect_timeout: Duration, tunnel_ctx: TunnelCtx, dns_resolver: R, #[builder(setter(skip))] _phantom_target: PhantomData, } #[derive(Eq, PartialEq, Debug, Clone)] pub struct Nugget { data: Arc>, } type CachedSocketAddrs = (Vec, u128); /// Caching DNS resolution to minimize DNS look-ups. /// The cache has relaxed consistency, it allows concurrent DNS look-ups of the same key, /// without any guarantees which result is going to be cached. /// /// Given it's used for DNS look-ups this trade-off seems to be reasonable. #[derive(Clone)] pub struct SimpleCachingDnsResolver { // mostly reads, occasional writes cache: Arc>>, ttl: Duration, start_time: Instant, } #[async_trait] impl TargetConnector for SimpleTcpConnector where D: TunnelTarget + Send + Sync + Sized, R: DnsResolver + Send + Sync + 'static, { type Target = D; type Stream = TcpStream; async fn connect(&mut self, target: &Self::Target) -> io::Result { let target_addr = &target.target_addr(); let addr = self.dns_resolver.resolve(target_addr).await?; if let Ok(tcp_stream) = timeout(self.connect_timeout, TcpStream::connect(addr)).await { let mut stream = tcp_stream?; stream.nodelay()?; if target.has_nugget() { if let Ok(written_successfully) = timeout( self.connect_timeout, stream.write_all(&target.nugget().data()), ) .await { written_successfully?; } else { error!( "Timeout sending nugget to {}, {}, CTX={}", addr, target_addr, self.tunnel_ctx ); return Err(Error::from(ErrorKind::TimedOut)); } } Ok(stream) } else { error!( "Timeout connecting to {}, {}, CTX={}", addr, target_addr, self.tunnel_ctx ); Err(Error::from(ErrorKind::TimedOut)) } } } #[async_trait] impl DnsResolver for SimpleCachingDnsResolver { async fn resolve(&mut self, target: &str) -> io::Result { match self.try_find(target).await { Some(a) => Ok(a), _ => Ok(self.resolve_and_cache(target).await?), } } } impl SimpleTcpConnector where R: DnsResolver, { pub fn new(dns_resolver: R, connect_timeout: Duration, tunnel_ctx: TunnelCtx) -> Self { Self { dns_resolver, connect_timeout, tunnel_ctx, _phantom_target: PhantomData, } } } impl SimpleCachingDnsResolver { pub fn new(ttl: Duration) -> Self { Self { cache: Arc::new(RwLock::new(HashMap::new())), ttl, start_time: Instant::now(), } } fn pick(&self, addrs: &[SocketAddr]) -> SocketAddr { addrs[thread_rng().gen::() % addrs.len()] } async fn try_find(&mut self, target: &str) -> Option { let map = self.cache.read().await; let addr = match map.get(target) { None => None, Some((cached, expiration)) => { // expiration with jitter to avoid expiration "waves" let expiration_jitter = *expiration + thread_rng().gen_range(0..5_000); if Instant::now().duration_since(self.start_time).as_millis() < expiration_jitter { Some(self.pick(cached)) } else { None } } }; addr } async fn resolve_and_cache(&mut self, target: &str) -> io::Result { let resolved = SimpleCachingDnsResolver::resolve(target).await?; let mut map = self.cache.write().await; map.insert( target.to_string(), ( resolved.clone(), Instant::now().duration_since(self.start_time).as_millis() + self.ttl.as_millis(), ), ); Ok(self.pick(&resolved)) } async fn resolve(target: &str) -> io::Result> { debug!("Resolving DNS {}", target,); let resolved: Vec = tokio::net::lookup_host(target).await?.collect(); info!("Resolved DNS {} to {:?}", target, resolved); if resolved.is_empty() { error!("Cannot resolve DNS {}", target,); return Err(Error::from(ErrorKind::AddrNotAvailable)); } Ok(resolved) } } impl Nugget { pub fn new>>(v: T) -> Self { Self { data: Arc::new(v.into()), } } pub fn data(&self) -> Arc> { self.data.clone() } } ================================================ FILE: src/relay.rs ================================================ /// Copyright 2020 Developers of the http-tunnel project. /// /// Licensed under the Apache License, Version 2.0 or the MIT license /// , at your /// option. This file may not be copied, modified, or distributed /// except according to those terms. use core::fmt; use std::future::Future; use std::time::{Duration, Instant}; use crate::tunnel::TunnelCtx; use log::{debug, error, info}; use tokio::io; use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}; use tokio::time::timeout; pub const NO_TIMEOUT: Duration = Duration::from_secs(300); pub const NO_BANDWIDTH_LIMIT: u64 = 1_000_000_000_000_u64; const BUFFER_SIZE: usize = 16 * 1024; #[derive(Debug, Clone, Eq, PartialEq, Serialize)] pub enum RelayShutdownReasons { /// If a reader connection was gracefully closed GracefulShutdown, ReadError, WriteError, ReaderTimeout, WriterTimeout, TooSlow, TooFast, } /// Relays traffic from one stream to another in a single direction. /// To relay two sockets in full-duplex mode you need to create two `Relays` in both directions. /// It doesn't really matter what is the protocol, as it only requires `AsyncReadExt` /// and `AsyncWriteExt` traits from the source and the target. #[derive(Builder, Clone)] pub struct Relay { name: &'static str, relay_policy: RelayPolicy, tunnel_ctx: TunnelCtx, } /// Stats after the relay is closed. Can be used for telemetry/monitoring. #[derive(Builder, Clone, Debug, Serialize)] pub struct RelayStats { pub shutdown_reason: RelayShutdownReasons, pub total_bytes: usize, pub event_count: usize, pub duration: Duration, } /// Relay policy is meant to protect targets and proxy servers from /// different sorts of abuse. Currently it only checks too slow or too fast connections, /// which may lead to different capacity issues. #[derive(Builder, Deserialize, Clone)] pub struct RelayPolicy { #[serde(with = "humantime_serde")] pub idle_timeout: Duration, /// Min bytes-per-minute (bpm) pub min_rate_bpm: u64, // Max bytes-per-second (bps) pub max_rate_bps: u64, } impl Relay { /// Relays data in a single direction. E.g. /// ```ignore /// let upstream = tokio::spawn(async move { /// upstream_relay.relay_data(client_recv, target_send).await /// }); /// let downstream = tokio::spawn(async move { /// downstream_relay.relay_data(target_recv, client_send).await /// }); /// let downstream_stats = downstream.await??; /// let upstream_stats = upstream.await??; /// ``` pub async fn relay_data( self, mut source: ReadHalf, mut dest: WriteHalf, ) -> io::Result { let mut buffer = [0; BUFFER_SIZE]; let mut total_bytes = 0; let mut event_count = 0; let start_time = Instant::now(); let shutdown_reason; loop { let read_result = self .relay_policy .timed_operation(source.read(&mut buffer)) .await; if read_result.is_err() { shutdown_reason = RelayShutdownReasons::ReaderTimeout; break; } let n = match read_result.unwrap() { Ok(0) => { shutdown_reason = RelayShutdownReasons::GracefulShutdown; break; } Ok(n) => n, Err(e) => { error!( "{} failed to read. Err = {:?}, CTX={}", self.name, e, self.tunnel_ctx ); shutdown_reason = RelayShutdownReasons::ReadError; break; } }; let write_result = self .relay_policy .timed_operation(dest.write_all(&buffer[..n])) .await; if write_result.is_err() { shutdown_reason = RelayShutdownReasons::WriterTimeout; break; } if let Err(e) = write_result.unwrap() { error!( "{} failed to write {} bytes. Err = {:?}, CTX={}", self.name, n, e, self.tunnel_ctx ); shutdown_reason = RelayShutdownReasons::WriteError; break; } total_bytes += n; event_count += 1; if let Err(rate_violation) = self .relay_policy .check_transmission_rates(&start_time, total_bytes) { shutdown_reason = rate_violation; break; } } self.shutdown(&mut dest, &shutdown_reason).await; let duration = Instant::now().duration_since(start_time); let stats = RelayStatsBuilder::default() .shutdown_reason(shutdown_reason) .total_bytes(total_bytes) .event_count(event_count) .duration(duration) .build() .expect("RelayStatsBuilder failed"); info!("{} closed: {}, CTX={}", self.name, stats, self.tunnel_ctx); Ok(stats) } async fn shutdown( &self, dest: &mut WriteHalf, reason: &RelayShutdownReasons, ) { match dest.shutdown().await { Ok(_) => { debug!( "{} shutdown due do {:?}, CTX={}", self.name, reason, self.tunnel_ctx ); } Err(e) => { error!( "{} failed to shutdown. Err = {:?}, CTX={}", self.name, e, self.tunnel_ctx ); } } } } impl RelayPolicy { /// Basic rate limiting. Placeholder for more sophisticated policy handling, /// e.g. sliding windows, detecting heavy hitters, etc. pub fn check_transmission_rates( &self, start: &Instant, total_bytes: usize, ) -> Result<(), RelayShutdownReasons> { if self.min_rate_bpm == 0 && self.max_rate_bps >= NO_BANDWIDTH_LIMIT { return Ok(()); } let elapsed = Instant::now().duration_since(*start); if elapsed.as_secs_f32() > 5. && total_bytes as u64 / elapsed.as_secs() > self.max_rate_bps { // prevent bandwidth abuse Err(RelayShutdownReasons::TooFast) } else if elapsed.as_secs_f32() >= 30. && total_bytes as f64 / elapsed.as_secs_f64() / 60. < self.min_rate_bpm as f64 { // prevent slowloris: https://en.wikipedia.org/wiki/Slowloris_(computer_security) Err(RelayShutdownReasons::TooSlow) } else { Ok(()) } } /// Each async operation must be time-bound. pub async fn timed_operation(&self, f: T) -> Result<::Output, ()> { if self.idle_timeout >= NO_TIMEOUT { return Ok(f.await); } let result = timeout(self.idle_timeout, f).await; if let Ok(r) = result { Ok(r) } else { Err(()) } } } // cov:begin-ignore-line impl fmt::Display for RelayStats { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "shutdown_reason={:?}, bytes={}, event_count={}, duration={:?}, rate_kbps={:.3}", self.shutdown_reason, self.total_bytes, self.event_count, self.duration, self.total_bytes as f64 / 1024. / self.duration.as_secs_f64() ) } } // cov:end-ignore-line #[cfg(test)] mod test_relay_policy { extern crate tokio; use std::ops::Sub; use std::time::{Duration, Instant}; use tokio_test::io::Builder; use tokio_test::io::Mock; use crate::relay::{RelayPolicy, RelayPolicyBuilder, RelayShutdownReasons}; use self::tokio::io::{AsyncReadExt, Error, ErrorKind}; #[test] fn test_enforce_policy_ok() { let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(1)) .build() .unwrap(); let start = Instant::now().sub(Duration::from_secs(10)); // 100k in 10 second is OK let result = relay_policy.check_transmission_rates(&start, 100_000); assert!(result.is_ok()); } #[test] fn test_enforce_policy_too_fast() { let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(1)) .build() .unwrap(); let start = Instant::now().sub(Duration::from_secs(10)); // 10m in 10 second is way too fast let result = relay_policy.check_transmission_rates(&start, 10_000_000); assert!(result.is_err()); assert_eq!(RelayShutdownReasons::TooFast, result.unwrap_err()); } #[test] fn test_enforce_policy_too_slow() { let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(1)) .build() .unwrap(); // 100 bytes in 40 seconds is too slow let start = Instant::now().sub(Duration::from_secs(40)); let result = relay_policy.check_transmission_rates(&start, 100); assert!(result.is_err()); assert_eq!(RelayShutdownReasons::TooSlow, result.unwrap_err()); } #[tokio::test] async fn test_timed_operation_ok() { let data = b"data on the wire"; let mut mock_connection: Mock = Builder::new().read(data).build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(5)) .build() .unwrap(); let mut buf = [0; 1024]; let timed_future = relay_policy .timed_operation(mock_connection.read(&mut buf)) .await; assert!(timed_future.is_ok()); assert_eq!(data, &buf[..timed_future.unwrap().unwrap()]) } #[tokio::test] async fn test_timed_operation_failed_io() { let mut mock_connection: Mock = Builder::new() .read_error(Error::from(ErrorKind::BrokenPipe)) .build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(5)) .build() .unwrap(); let mut buf = [0; 1024]; let timed_future = relay_policy .timed_operation(mock_connection.read(&mut buf)) .await; assert!(timed_future.is_ok()); // no timeout assert!(timed_future.unwrap().is_err()); // but io-error } #[tokio::test] async fn test_timed_operation_timeout() { let time_duration = 1; let mut mock_connection: Mock = Builder::new() .wait(Duration::from_secs(time_duration * 2)) .build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(time_duration)) .build() .unwrap(); let mut buf = [0; 1024]; let timed_future = relay_policy .timed_operation(mock_connection.read(&mut buf)) .await; assert!(timed_future.is_err()); } } #[cfg(test)] mod test_relay { extern crate tokio; use std::time::Duration; use tokio::io; use tokio_test::io::Builder; use tokio_test::io::Mock; use crate::relay::{ Relay, RelayBuilder, RelayPolicy, RelayPolicyBuilder, RelayShutdownReasons, }; use self::tokio::io::{Error, ErrorKind}; use crate::tunnel::{TunnelCtx, TunnelCtxBuilder}; #[tokio::test] async fn test_relay_ok() { let data = b"data on the wire"; let reader: Mock = Builder::new().read(data).read(data).read(data).build(); let writer: Mock = Builder::new().write(data).write(data).write(data).build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(5)) .build() .unwrap(); let relay: Relay = build_relay(relay_policy); let (client_recv, _) = io::split(reader); let (_, target_send) = io::split(writer); let result = relay.relay_data(client_recv, target_send).await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!( RelayShutdownReasons::GracefulShutdown, stats.shutdown_reason ); assert_eq!(data.len() * 3, stats.total_bytes); assert_eq!(3, stats.event_count); } #[tokio::test] async fn test_relay_reader_error() { let data = b"data on the wire"; let reader: Mock = Builder::new() .read(data) .read_error(Error::from(ErrorKind::BrokenPipe)) .build(); let writer: Mock = Builder::new().write(data).build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(5)) .build() .unwrap(); let relay: Relay = build_relay(relay_policy); let (client_recv, _) = io::split(reader); let (_, target_send) = io::split(writer); let result = relay.relay_data(client_recv, target_send).await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(RelayShutdownReasons::ReadError, stats.shutdown_reason); } #[tokio::test] async fn test_relay_reader_timeout() { let data = b"data on the wire"; let reader: Mock = Builder::new() .read(data) .wait(Duration::from_secs(3)) .build(); let writer: Mock = Builder::new().write(data).build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(1)) .build() .unwrap(); let relay: Relay = build_relay(relay_policy); let (client_recv, _) = io::split(reader); let (_, target_send) = io::split(writer); let result = relay.relay_data(client_recv, target_send).await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(RelayShutdownReasons::ReaderTimeout, stats.shutdown_reason); } #[tokio::test] async fn test_relay_writer_error() { let data = b"data on the wire"; let reader: Mock = Builder::new().read(data).read(data).build(); let writer: Mock = Builder::new() .write(data) .write_error(Error::from(ErrorKind::BrokenPipe)) .build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(5)) .build() .unwrap(); let relay: Relay = build_relay(relay_policy); let (client_recv, _) = io::split(reader); let (_, target_send) = io::split(writer); let result = relay.relay_data(client_recv, target_send).await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(RelayShutdownReasons::WriteError, stats.shutdown_reason); } #[tokio::test] async fn test_relay_writer_timeout() { let data = b"data on the wire"; let reader: Mock = Builder::new().read(data).build(); let writer: Mock = Builder::new().wait(Duration::from_secs(3)).build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1000) .max_rate_bps(100_000) .idle_timeout(Duration::from_secs(1)) .build() .unwrap(); let relay: Relay = build_relay(relay_policy); let (client_recv, _) = io::split(reader); let (_, target_send) = io::split(writer); let result = relay.relay_data(client_recv, target_send).await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(RelayShutdownReasons::WriterTimeout, stats.shutdown_reason); } #[tokio::test] async fn test_relay_reader_violates_rate_limits() { let data = b"waaaay too much data on the wire"; let reader: Mock = Builder::new() .read(data) .wait(Duration::from_secs_f32(5.5)) .read(data) .build(); let writer: Mock = Builder::new().write(data).write(data).build(); let relay_policy: RelayPolicy = RelayPolicyBuilder::default() .min_rate_bpm(1) .max_rate_bps(1) // ok, let's be like unreasonably restrictive .idle_timeout(Duration::from_secs(10)) .build() .unwrap(); let relay: Relay = build_relay(relay_policy); let (client_recv, _) = io::split(reader); let (_, target_send) = io::split(writer); let result = relay.relay_data(client_recv, target_send).await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(RelayShutdownReasons::TooFast, stats.shutdown_reason); assert_eq!(data.len() * 2, stats.total_bytes); assert_eq!(2, stats.event_count); } fn build_relay(relay_policy: RelayPolicy) -> Relay { let ctx: TunnelCtx = TunnelCtxBuilder::default().id(1).build().unwrap(); RelayBuilder::default() .relay_policy(relay_policy) .tunnel_ctx(ctx) .name("Test") .build() .unwrap() } } ================================================ FILE: src/tunnel.rs ================================================ /// Copyright 2020 Developers of the http-tunnel project. /// /// Licensed under the Apache License, Version 2.0 or the MIT license /// , at your /// option. This file may not be copied, modified, or distributed /// except according to those terms. use async_trait::async_trait; use futures::{SinkExt, StreamExt}; use log::{debug, error}; use tokio::io; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::timeout; use tokio_util::codec::{Decoder, Encoder, Framed}; use crate::configuration::TunnelConfig; use crate::proxy_target::{Nugget, TargetConnector}; use crate::relay::{Relay, RelayBuilder, RelayPolicy, RelayStats}; use core::fmt; use futures::stream::SplitStream; use std::fmt::Display; use std::time::Duration; #[derive(Eq, PartialEq, Debug, Clone, Serialize)] #[allow(dead_code)] pub enum EstablishTunnelResult { /// Successfully connected to target. Ok, /// Successfully connected to target but has a nugget to send after connection establishment. OkWithNugget, /// Malformed request BadRequest, /// Target is not allowed Forbidden, /// Unsupported operation, however valid for the protocol. OperationNotAllowed, /// The client failed to send a tunnel request timely. RequestTimeout, /// Cannot connect to target. BadGateway, /// Connection attempt timed out. GatewayTimeout, /// Busy. Try again later. TooManyRequests, /// Any other error. E.g. an abrupt I/O error. ServerError, } /// A connection tunnel. /// /// # Parameters /// * `` - proxy handshake codec for initiating a tunnel. /// It extracts the request message, which contains the target, and, potentially policies. /// It also takes care of encoding a response. /// * `` - a connection from from client. /// * `` - target connector. It takes result produced by the codec and establishes a connection /// to a target. /// /// Once the target connection is established, it relays data until any connection is closed or an /// error happens. pub struct ConnectionTunnel { tunnel_request_codec: Option, tunnel_ctx: TunnelCtx, target_connector: T, client: Option, tunnel_config: TunnelConfig, } #[async_trait] pub trait TunnelTarget { type Addr; fn target_addr(&self) -> Self::Addr; fn has_nugget(&self) -> bool; fn nugget(&self) -> &Nugget; } /// We need to be able to trace events in logs/metrics. #[derive(Builder, Copy, Clone, Default, Serialize)] pub struct TunnelCtx { /// We can easily extend it, if necessary. For now just a random u128. id: u128, } /// Statistics. No sensitive information. #[derive(Serialize, Builder)] pub struct TunnelStats { tunnel_ctx: TunnelCtx, result: EstablishTunnelResult, upstream_stats: Option, downstream_stats: Option, } impl ConnectionTunnel where H: Decoder + Encoder, H::Item: TunnelTarget + Sized + Display + Send + Sync, C: AsyncRead + AsyncWrite + Sized + Send + Unpin + 'static, T: TargetConnector, { pub fn new( handshake_codec: H, target_connector: T, client: C, tunnel_config: TunnelConfig, tunnel_ctx: TunnelCtx, ) -> Self { Self { tunnel_request_codec: Some(handshake_codec), target_connector, tunnel_ctx, client: Some(client), tunnel_config, } } /// Once the client connected we wait for a tunnel establishment handshake. /// For instance, an `HTTP/1.1 CONNECT` for HTTP tunnels. /// /// During handshake we obtained the target target, and if we were able to connect to it, /// a message indicating success is sent back to client (or an error response otherwise). /// /// At that point we start relaying data in full-duplex mode. /// /// # Note /// This method consumes `self` and thus can be called only once. pub async fn start(mut self) -> io::Result { let stream = self.client.take().expect("downstream can be taken once"); let tunnel_result = self .establish_tunnel(stream, self.tunnel_config.clone()) .await; if let Err(error) = tunnel_result { return Ok(TunnelStats { tunnel_ctx: self.tunnel_ctx, result: error, upstream_stats: None, downstream_stats: None, }); } let (client, target) = tunnel_result.unwrap(); relay_connections( client, target, self.tunnel_ctx, self.tunnel_config.client_connection.relay_policy, self.tunnel_config.target_connection.relay_policy, ) .await } async fn establish_tunnel( &mut self, stream: C, configuration: TunnelConfig, ) -> Result<(C, T::Stream), EstablishTunnelResult> { debug!("Accepting HTTP tunnel request: CTX={}", self.tunnel_ctx); let (mut write, mut read) = self .tunnel_request_codec .take() .expect("establish_tunnel can be called only once") .framed(stream) .split(); let (response, target) = self.process_tunnel_request(&configuration, &mut read).await; let response_sent = match response { EstablishTunnelResult::OkWithNugget => true, _ => timeout( configuration.client_connection.initiation_timeout, write.send(response.clone()), ) .await .is_ok(), }; if response_sent { match target { None => Err(response), Some(u) => { // lets take the original stream to either relay data, or to drop it on error let framed = write.reunite(read).expect("Uniting previously split parts"); let original_stream = framed.into_inner(); Ok((original_stream, u)) } } } else { Err(EstablishTunnelResult::RequestTimeout) } } async fn process_tunnel_request( &mut self, configuration: &TunnelConfig, read: &mut SplitStream>, ) -> ( EstablishTunnelResult, Option<::Stream>, ) { let connect_request = timeout( configuration.client_connection.initiation_timeout, read.next(), ) .await; let response; let mut target = None; if connect_request.is_err() { error!("Client established TLS connection but failed to send an HTTP request within {:?}, CTX={}", configuration.client_connection.initiation_timeout, self.tunnel_ctx); response = EstablishTunnelResult::RequestTimeout; } else if let Ok(Some(event)) = connect_request { match event { Ok(decoded_target) => { let has_nugget = decoded_target.has_nugget(); response = match self .connect_to_target( decoded_target, configuration.target_connection.connect_timeout, ) .await { Ok(t) => { target = Some(t); if has_nugget { EstablishTunnelResult::OkWithNugget } else { EstablishTunnelResult::Ok } } Err(e) => e, } } Err(e) => { response = e; } } } else { response = EstablishTunnelResult::BadRequest; } (response, target) } async fn connect_to_target( &mut self, target: T::Target, connect_timeout: Duration, ) -> Result { debug!( "Establishing HTTP tunnel target connection: {}, CTX={}", target, self.tunnel_ctx, ); let timed_connection_result = timeout(connect_timeout, self.target_connector.connect(&target)).await; if let Ok(timed_connection_result) = timed_connection_result { match timed_connection_result { Ok(tcp_stream) => Ok(tcp_stream), Err(e) => Err(EstablishTunnelResult::from(e)), } } else { Err(EstablishTunnelResult::GatewayTimeout) } } } pub async fn relay_connections< D: AsyncRead + AsyncWrite + Sized + Send + Unpin + 'static, U: AsyncRead + AsyncWrite + Sized + Send + 'static, >( client: D, target: U, ctx: TunnelCtx, downstream_relay_policy: RelayPolicy, upstream_relay_policy: RelayPolicy, ) -> io::Result { let (client_recv, client_send) = io::split(client); let (target_recv, target_send) = io::split(target); let downstream_relay: Relay = RelayBuilder::default() .name("Downstream") .tunnel_ctx(ctx) .relay_policy(downstream_relay_policy) .build() .expect("RelayBuilder failed"); let upstream_relay: Relay = RelayBuilder::default() .name("Upstream") .tunnel_ctx(ctx) .relay_policy(upstream_relay_policy) .build() .expect("RelayBuilder failed"); let upstream_task = tokio::spawn(async move { downstream_relay.relay_data(client_recv, target_send).await }); let downstream_task = tokio::spawn(async move { upstream_relay.relay_data(target_recv, client_send).await }); let downstream_stats = downstream_task.await??; let upstream_stats = upstream_task.await??; Ok(TunnelStats { tunnel_ctx: ctx, result: EstablishTunnelResult::Ok, upstream_stats: Some(upstream_stats), downstream_stats: Some(downstream_stats), }) } // cov:begin-ignore-line impl fmt::Display for TunnelCtx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.id) } } // cov:end-ignore-line #[cfg(test)] mod test { extern crate tokio; use async_trait::async_trait; use std::time::Duration; use tokio::io; use tokio_test::io::Builder; use tokio_test::io::Mock; use crate::relay::RelayPolicy; use self::tokio::io::{AsyncWriteExt, Error, ErrorKind}; use crate::configuration::{ClientConnectionConfig, TargetConnectionConfig, TunnelConfig}; use crate::http_tunnel_codec::{HttpTunnelCodec, HttpTunnelCodecBuilder, HttpTunnelTarget}; use crate::proxy_target::TargetConnector; use crate::tunnel::{ConnectionTunnel, EstablishTunnelResult, TunnelCtxBuilder, TunnelTarget}; use rand::{thread_rng, Rng}; use regex::Regex; #[tokio::test] async fn test_tunnel_ok() { let handshake_request = b"CONNECT foo.bar:80 HTTP/1.1\r\n\r\n"; let handshake_response = b"HTTP/1.1 200 OK\r\n\r\n"; let tunneled_request = b"0: Some arbibrary request"; let tunneled_response = b"1: Some arbibrary response"; let client: Mock = Builder::new() .read(handshake_request) .write(handshake_response) .read(tunneled_request) .write(tunneled_response) .build(); let target: Mock = Builder::new() .write(tunneled_request) .read(tunneled_response) .build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("ConnectRequestCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: Some(target), delay: None, error: None, }; let default_timeout = Duration::from_secs(5); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::Ok); assert!(stats.upstream_stats.is_some()); assert!(stats.downstream_stats.is_some()); let upstream_stats = stats.upstream_stats.unwrap(); let downstream_stats = stats.downstream_stats.unwrap(); assert_eq!(upstream_stats.total_bytes, tunneled_request.len()); assert_eq!(downstream_stats.total_bytes, tunneled_response.len()); } #[tokio::test] #[cfg(feature = "plain_text")] async fn test_tunnel_plain_text_ok() { let handshake_request = b"GET https://foo.bar/index.html HTTP/1.1\r\nHost: foo.bar:443\r\n\r\n"; let tunneled_response = b"HTTP/1.1 200 OK\r\n\r\n"; let client: Mock = Builder::new() .read(handshake_request) .write(tunneled_response) .build(); let target: Mock = Builder::new() .write(handshake_request) .read(tunneled_response) .build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:443").unwrap()) .build() .expect("ConnectRequestCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:443".to_string(), mock: Some(target), delay: None, error: None, }; let default_timeout = Duration::from_secs(5); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::Ok); assert!(stats.upstream_stats.is_some()); assert!(stats.downstream_stats.is_some()); let upstream_stats = stats.upstream_stats.unwrap(); let downstream_stats = stats.downstream_stats.unwrap(); assert_eq!(upstream_stats.total_bytes, 0); assert_eq!(downstream_stats.total_bytes, tunneled_response.len()); } #[tokio::test] async fn test_tunnel_request_timeout() { let handshake_response = b"HTTP/1.1 408 TIMEOUT\r\n\r\n"; let client: Mock = Builder::new() .wait(Duration::from_secs(2)) .write(handshake_response) .build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("HttpTunnelCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: None, delay: None, error: None, }; let default_timeout = Duration::from_secs(1); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::RequestTimeout); assert!(stats.upstream_stats.is_none()); assert!(stats.downstream_stats.is_none()); } #[tokio::test] async fn test_tunnel_response_timeout() { let handshake_request = b"CONNECT foo.bar:80 HTTP/1.1\r\n\r\n"; let client: Mock = Builder::new() .read(handshake_request) .wait(Duration::from_secs(2)) .build(); let target: Mock = Builder::new().build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("HttpTunnelCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: Some(target), delay: None, error: None, }; let default_timeout = Duration::from_secs(1); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::RequestTimeout); assert!(stats.upstream_stats.is_none()); assert!(stats.downstream_stats.is_none()); } #[tokio::test] async fn test_tunnel_upstream_timeout() { let handshake_request = b"CONNECT foo.bar:80 HTTP/1.1\r\n\r\n"; let handshake_response = b"HTTP/1.1 504 GATEWAY_TIMEOUT\r\n\r\n"; let client: Mock = Builder::new() .read(handshake_request) .write(handshake_response) .build(); let target: Mock = Builder::new().build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("HttpTunnelCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: Some(target), delay: Some(Duration::from_secs(3)), error: None, }; let default_timeout = Duration::from_secs(1); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::GatewayTimeout); assert!(stats.upstream_stats.is_none()); assert!(stats.downstream_stats.is_none()); } #[tokio::test] async fn test_tunnel_bad_target() { let handshake_request = b"CONNECT disallowed.com:80 HTTP/1.1\r\n\r\n"; let handshake_response = b"HTTP/1.1 403 FORBIDDEN\r\n\r\n"; let client: Mock = Builder::new() .read(handshake_request) .write(handshake_response) .build(); let target: Mock = Builder::new().build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("HttpTunnelCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: Some(target), delay: None, error: None, }; let default_timeout = Duration::from_secs(1); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::Forbidden); assert!(stats.upstream_stats.is_none()); assert!(stats.downstream_stats.is_none()); } #[tokio::test] async fn test_tunnel_bad_gateway() { let handshake_request = b"CONNECT foo.bar:80 HTTP/1.1\r\n\r\n"; let handshake_response = b"HTTP/1.1 502 BAD_GATEWAY\r\n\r\n"; let client: Mock = Builder::new() .read(handshake_request) .write(handshake_response) .build(); let _target: Mock = Builder::new().build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("HttpTunnelCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: None, delay: None, error: Some(ErrorKind::BrokenPipe), }; let default_timeout = Duration::from_secs(1); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::BadGateway); assert!(stats.upstream_stats.is_none()); assert!(stats.downstream_stats.is_none()); } #[tokio::test] async fn test_tunnel_bad_request() { let handshake_request = b"CONNECT\tfoo.bar:80\tHTTP/1.1\r\n\r\n"; let handshake_response = b"HTTP/1.1 400 BAD_REQUEST\r\n\r\n"; let client: Mock = Builder::new() .read(handshake_request) .write(handshake_response) .build(); let _target: Mock = Builder::new().build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("HttpTunnelCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: None, delay: None, error: Some(ErrorKind::BrokenPipe), }; let default_timeout = Duration::from_secs(1); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::BadRequest); assert!(stats.upstream_stats.is_none()); assert!(stats.downstream_stats.is_none()); } #[tokio::test] #[cfg(not(feature = "plain_text"))] async fn test_tunnel_not_allowed() { let handshake_request = b"GET foo.bar:80 HTTP/1.1\r\n\r\n"; let handshake_response = b"HTTP/1.1 405 NOT_ALLOWED\r\n\r\n"; let client: Mock = Builder::new() .read(handshake_request) .write(handshake_response) .build(); let _target: Mock = Builder::new().build(); let ctx = TunnelCtxBuilder::default() .id(thread_rng().gen::()) .build() .expect("TunnelCtxBuilder failed"); let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default() .tunnel_ctx(ctx) .enabled_targets(Regex::new(r"foo\.bar:80").unwrap()) .build() .expect("HttpTunnelCodecBuilder failed"); let connector = MockTargetConnector { target: "foo.bar:80".to_string(), mock: None, delay: None, error: Some(ErrorKind::BrokenPipe), }; let default_timeout = Duration::from_secs(1); let config = build_config(default_timeout); let result = ConnectionTunnel::new(codec, connector, client, config, ctx) .start() .await; assert!(result.is_ok()); let stats = result.unwrap(); assert_eq!(stats.result, EstablishTunnelResult::OperationNotAllowed); assert!(stats.upstream_stats.is_none()); assert!(stats.downstream_stats.is_none()); } fn build_config(default_timeout: Duration) -> TunnelConfig { TunnelConfig { client_connection: ClientConnectionConfig { initiation_timeout: default_timeout, relay_policy: RelayPolicy { idle_timeout: default_timeout, min_rate_bpm: 0, max_rate_bps: 120410065, }, }, target_connection: TargetConnectionConfig { dns_cache_ttl: default_timeout, allowed_targets: Regex::new(r"foo\.bar:80").unwrap(), connect_timeout: default_timeout, relay_policy: RelayPolicy { idle_timeout: default_timeout, min_rate_bpm: 0, max_rate_bps: 170310180, }, }, } } struct MockTargetConnector { target: String, mock: Option, delay: Option, error: Option, } #[async_trait] impl TargetConnector for MockTargetConnector { type Target = HttpTunnelTarget; type Stream = Mock; async fn connect(&mut self, target: &Self::Target) -> io::Result { let target_addr = &target.target_addr(); assert_eq!(&self.target, target_addr); if let Some(d) = self.delay { tokio::time::sleep(d).await; } match self.error { None => { let mut stream = self.mock.take().unwrap(); if target.has_nugget() { stream.write_all(&target.nugget().data()).await?; } Ok(stream) } Some(e) => Err(Error::from(e)), } } } }