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 <LICENSE-APACHE> or
<http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
<LICENSE-MIT> or <http://opensource.org/licenses/MIT>, 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
================================================
[](https://crates.io/crates/http-tunnel)


[](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 <LICENSE-APACHE or
/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<String>,
/// 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<ProxyConfiguration> {
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<Identity> {
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<TunnelConfig> {
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 <LICENSE-APACHE or
/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<Nugget>,
// 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<Nugget>,
// easily can be extended with something like
// policies: Vec<TunnelPolicy>
}
/// 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<Option<Self::Item>, 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<EstablishTunnelResult> 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<Error> 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<Self, EstablishTunnelResult> {
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<String> {
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::<Vec<&str>>();
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<bool, EstablishTunnelResult> {
if method != "CONNECT" {
debug!("Not allowed method {}", method);
Err(EstablishTunnelResult::OperationNotAllowed)
} else {
Ok(false)
}
}
#[cfg(feature = "plain_text")]
fn check_method(method: &str) -> Result<bool, EstablishTunnelResult> {
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 <LICENSE-APACHE or
/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<TcpListener, Error> {
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::<u128>())
.build()
.expect("TunnelCtxBuilder failed");
let mut connector: SimpleTcpConnector<HttpTunnelTarget, DnsResolver> =
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<C: AsyncRead + AsyncWrite + Send + Unpin + 'static>(
config: &ProxyConfiguration,
client: C,
dns_resolver: DnsResolver,
) -> io::Result<()> {
let ctx = TunnelCtxBuilder::default()
.id(thread_rng().gen::<u128>())
.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<HttpTunnelTarget, DnsResolver> = 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<TunnelStats>) {
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 <LICENSE-APACHE or
/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<Self::Stream>;
}
#[async_trait]
pub trait DnsResolver {
async fn resolve(&mut self, target: &str) -> io::Result<SocketAddr>;
}
#[derive(Clone, Builder)]
pub struct SimpleTcpConnector<D, R: DnsResolver> {
connect_timeout: Duration,
tunnel_ctx: TunnelCtx,
dns_resolver: R,
#[builder(setter(skip))]
_phantom_target: PhantomData<D>,
}
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct Nugget {
data: Arc<Vec<u8>>,
}
type CachedSocketAddrs = (Vec<SocketAddr>, 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<RwLock<HashMap<String, CachedSocketAddrs>>>,
ttl: Duration,
start_time: Instant,
}
#[async_trait]
impl<D, R> TargetConnector for SimpleTcpConnector<D, R>
where
D: TunnelTarget<Addr = String> + Send + Sync + Sized,
R: DnsResolver + Send + Sync + 'static,
{
type Target = D;
type Stream = TcpStream;
async fn connect(&mut self, target: &Self::Target) -> io::Result<Self::Stream> {
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<SocketAddr> {
match self.try_find(target).await {
Some(a) => Ok(a),
_ => Ok(self.resolve_and_cache(target).await?),
}
}
}
impl<D, R> SimpleTcpConnector<D, R>
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::<usize>() % addrs.len()]
}
async fn try_find(&mut self, target: &str) -> Option<SocketAddr> {
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<SocketAddr> {
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<Vec<SocketAddr>> {
debug!("Resolving DNS {}", target,);
let resolved: Vec<SocketAddr> = 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<T: Into<Vec<u8>>>(v: T) -> Self {
Self {
data: Arc::new(v.into()),
}
}
pub fn data(&self) -> Arc<Vec<u8>> {
self.data.clone()
}
}
================================================
FILE: src/relay.rs
================================================
/// Copyright 2020 Developers of the http-tunnel project.
///
/// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<R: AsyncReadExt + Sized, W: AsyncWriteExt + Sized>(
self,
mut source: ReadHalf<R>,
mut dest: WriteHalf<W>,
) -> io::Result<RelayStats> {
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<W: AsyncWriteExt + Sized>(
&self,
dest: &mut WriteHalf<W>,
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<T: Future>(&self, f: T) -> Result<<T as Future>::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 <LICENSE-APACHE or
/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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
/// * `<H>` - 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.
/// * `<C>` - a connection from from client.
/// * `<T>` - 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<H, C, T> {
tunnel_request_codec: Option<H>,
tunnel_ctx: TunnelCtx,
target_connector: T,
client: Option<C>,
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<RelayStats>,
downstream_stats: Option<RelayStats>,
}
impl<H, C, T> ConnectionTunnel<H, C, T>
where
H: Decoder<Error = EstablishTunnelResult> + Encoder<EstablishTunnelResult>,
H::Item: TunnelTarget + Sized + Display + Send + Sync,
C: AsyncRead + AsyncWrite + Sized + Send + Unpin + 'static,
T: TargetConnector<Target = H::Item>,
{
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<TunnelStats> {
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<Framed<C, H>>,
) -> (
EstablishTunnelResult,
Option<<T as TargetConnector>::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<T::Stream, EstablishTunnelResult> {
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<TunnelStats> {
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::<u128>())
.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::<u128>())
.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::<u128>())
.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::<u128>())
.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::<u128>())
.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::<u128>())
.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::<u128>())
.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::<u128>())
.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::<u128>())
.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<Mock>,
delay: Option<Duration>,
error: Option<ErrorKind>,
}
#[async_trait]
impl TargetConnector for MockTargetConnector {
type Target = HttpTunnelTarget;
type Stream = Mock;
async fn connect(&mut self, target: &Self::Target) -> io::Result<Self::Stream> {
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)),
}
}
}
}
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
SYMBOL INDEX (142 symbols across 6 files)
FILE: src/configuration.rs
type ClientConnectionConfig (line 21) | pub struct ClientConnectionConfig {
type TargetConnectionConfig (line 28) | pub struct TargetConnectionConfig {
type TunnelConfig (line 39) | pub struct TunnelConfig {
type ProxyMode (line 45) | pub enum ProxyMode {
type ProxyConfiguration (line 52) | pub struct ProxyConfiguration {
method from_command_line (line 138) | pub fn from_command_line() -> io::Result<ProxyConfiguration> {
method tls_identity_from_file (line 189) | fn tls_identity_from_file(filename: &str, password: &str) -> io::Resul...
method read_tunnel_config (line 208) | fn read_tunnel_config(filename: &str) -> io::Result<TunnelConfig> {
type Cli (line 61) | struct Cli {
type Commands (line 73) | enum Commands {
type HttpOptions (line 83) | struct HttpOptions {}
type HttpsOptions (line 89) | struct HttpsOptions {
type TcpOptions (line 102) | struct TcpOptions {
method default (line 109) | fn default() -> Self {
FILE: src/http_tunnel_codec.rs
constant REQUEST_END_MARKER (line 22) | const REQUEST_END_MARKER: &[u8] = b"\r\n\r\n";
constant MAX_HTTP_REQUEST_SIZE (line 24) | const MAX_HTTP_REQUEST_SIZE: usize = 16384;
type HttpConnectRequest (line 28) | struct HttpConnectRequest {
method parse (line 164) | pub fn parse(http_request: &[u8]) -> Result<Self, EstablishTunnelResul...
method extract_destination_host (line 195) | fn extract_destination_host(lines: &mut Split<&str>, endpoint: &str) -...
method parse_request_line (line 215) | fn parse_request_line(
method precondition_well_formed (line 231) | fn precondition_well_formed(
method check_version (line 243) | fn check_version(version: &str) -> Result<(), EstablishTunnelResult> {
method check_method (line 253) | fn check_method(method: &str) -> Result<bool, EstablishTunnelResult> {
method check_method (line 263) | fn check_method(method: &str) -> Result<bool, EstablishTunnelResult> {
method precondition_legal_characters (line 267) | fn precondition_legal_characters(http_request: &[u8]) -> Result<(), Es...
method precondition_size (line 281) | fn precondition_size(http_request: &[u8]) -> Result<(), EstablishTunne...
type HttpTunnelTarget (line 37) | pub struct HttpTunnelTarget {
method fmt (line 133) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type HttpTunnelCodec (line 46) | pub struct HttpTunnelCodec {
type Error (line 84) | type Error = std::io::Error;
method encode (line 86) | fn encode(
type Item (line 52) | type Item = HttpTunnelTarget;
type Error (line 53) | type Error = EstablishTunnelResult;
method decode (line 55) | fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, S...
type Addr (line 114) | type Addr = String;
method target_addr (line 116) | fn target_addr(&self) -> Self::Addr {
method has_nugget (line 120) | fn has_nugget(&self) -> bool {
method nugget (line 124) | fn nugget(&self) -> &Nugget {
function got_http_request (line 140) | fn got_http_request(buffer: &BytesMut) -> bool {
function got_http_request (line 145) | fn got_http_request(buffer: &BytesMut) -> bool {
method from (line 154) | fn from(e: Error) -> Self {
function test_got_http_request_partial (line 312) | fn test_got_http_request_partial() {
function test_got_http_request_full (line 326) | fn test_got_http_request_full() {
function test_got_http_request_exceeding (line 346) | fn test_got_http_request_exceeding() {
function test_parse_valid (line 358) | fn test_parse_valid() {
function test_parse_valid_with_headers (line 368) | fn test_parse_valid_with_headers() {
function test_parse_not_allowed_method (line 383) | fn test_parse_not_allowed_method() {
function test_parse_plain_text_method (line 395) | fn test_parse_plain_text_method() {
function test_parse_plain_text_default_https_port (line 413) | fn test_parse_plain_text_default_https_port() {
function test_parse_plain_text_default_http_port (line 431) | fn test_parse_plain_text_default_http_port() {
function test_parse_plain_text_nugget (line 449) | fn test_parse_plain_text_nugget() {
function test_parse_plain_text_with_body (line 470) | fn test_parse_plain_text_with_body() {
function test_parse_plain_text_method_forbidden_domain (line 492) | fn test_parse_plain_text_method_forbidden_domain() {
function test_parse_bad_version (line 506) | fn test_parse_bad_version() {
function test_parse_bad_requests (line 519) | fn test_parse_bad_requests() {
function test_parse_request_exceeds_size (line 543) | fn test_parse_request_exceeds_size() {
function test_http_tunnel_encoder (line 557) | fn test_http_tunnel_encoder() {
function build_codec (line 582) | fn build_codec() -> HttpTunnelCodec {
FILE: src/main.rs
type DnsResolver (line 39) | type DnsResolver = SimpleCachingDnsResolver;
function main (line 42) | async fn main() -> io::Result<()> {
function start_listening_tcp (line 84) | async fn start_listening_tcp(config: &ProxyConfiguration) -> Result<TcpL...
function serve_tls (line 99) | async fn serve_tls(
function serve_plain_text (line 133) | async fn serve_plain_text(config: ProxyConfiguration, dns_resolver: DnsR...
function serve_tcp (line 154) | async fn serve_tcp(
function handle_client_tls_connection (line 215) | async fn handle_client_tls_connection(
function tunnel_stream (line 251) | async fn tunnel_stream<C: AsyncRead + AsyncWrite + Send + Unpin + 'static>(
function report_tunnel_metrics (line 292) | fn report_tunnel_metrics(ctx: TunnelCtx, stats: io::Result<TunnelStats>) {
function init_logger (line 301) | fn init_logger() {
FILE: src/proxy_target.rs
type TargetConnector (line 26) | pub trait TargetConnector {
method connect (line 30) | async fn connect(&mut self, target: &Self::Target) -> io::Result<Self:...
type Target (line 73) | type Target = D;
type Stream (line 74) | type Stream = TcpStream;
method connect (line 76) | async fn connect(&mut self, target: &Self::Target) -> io::Result<Self:...
type DnsResolver (line 34) | pub trait DnsResolver {
method resolve (line 35) | async fn resolve(&mut self, target: &str) -> io::Result<SocketAddr>;
method resolve (line 113) | async fn resolve(&mut self, target: &str) -> io::Result<SocketAddr> {
type SimpleTcpConnector (line 39) | pub struct SimpleTcpConnector<D, R: DnsResolver> {
type Nugget (line 48) | pub struct Nugget {
method new (line 197) | pub fn new<T: Into<Vec<u8>>>(v: T) -> Self {
method data (line 203) | pub fn data(&self) -> Arc<Vec<u8>> {
type CachedSocketAddrs (line 52) | type CachedSocketAddrs = (Vec<SocketAddr>, u128);
type SimpleCachingDnsResolver (line 60) | pub struct SimpleCachingDnsResolver {
method new (line 136) | pub fn new(ttl: Duration) -> Self {
method pick (line 144) | fn pick(&self, addrs: &[SocketAddr]) -> SocketAddr {
method try_find (line 148) | async fn try_find(&mut self, target: &str) -> Option<SocketAddr> {
method resolve_and_cache (line 167) | async fn resolve_and_cache(&mut self, target: &str) -> io::Result<Sock...
method resolve (line 182) | async fn resolve(target: &str) -> io::Result<Vec<SocketAddr>> {
function new (line 125) | pub fn new(dns_resolver: R, connect_timeout: Duration, tunnel_ctx: Tunne...
FILE: src/relay.rs
constant NO_TIMEOUT (line 18) | pub const NO_TIMEOUT: Duration = Duration::from_secs(300);
constant NO_BANDWIDTH_LIMIT (line 19) | pub const NO_BANDWIDTH_LIMIT: u64 = 1_000_000_000_000_u64;
constant BUFFER_SIZE (line 20) | const BUFFER_SIZE: usize = 16 * 1024;
type RelayShutdownReasons (line 23) | pub enum RelayShutdownReasons {
type Relay (line 39) | pub struct Relay {
method relay_data (line 79) | pub async fn relay_data<R: AsyncReadExt + Sized, W: AsyncWriteExt + Si...
method shutdown (line 166) | async fn shutdown<W: AsyncWriteExt + Sized>(
type RelayStats (line 47) | pub struct RelayStats {
method fmt (line 231) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type RelayPolicy (line 58) | pub struct RelayPolicy {
method check_transmission_rates (line 191) | pub fn check_transmission_rates(
method timed_operation (line 215) | pub async fn timed_operation<T: Future>(&self, f: T) -> Result<<T as F...
function test_enforce_policy_ok (line 260) | fn test_enforce_policy_ok() {
function test_enforce_policy_too_fast (line 274) | fn test_enforce_policy_too_fast() {
function test_enforce_policy_too_slow (line 289) | fn test_enforce_policy_too_slow() {
function test_timed_operation_ok (line 304) | async fn test_timed_operation_ok() {
function test_timed_operation_failed_io (line 324) | async fn test_timed_operation_failed_io() {
function test_timed_operation_timeout (line 345) | async fn test_timed_operation_timeout() {
function test_relay_ok (line 384) | async fn test_relay_ok() {
function test_relay_reader_error (line 416) | async fn test_relay_reader_error() {
function test_relay_reader_timeout (line 445) | async fn test_relay_reader_timeout() {
function test_relay_writer_error (line 474) | async fn test_relay_writer_error() {
function test_relay_writer_timeout (line 503) | async fn test_relay_writer_timeout() {
function test_relay_reader_violates_rate_limits (line 529) | async fn test_relay_reader_violates_rate_limits() {
function build_relay (line 561) | fn build_relay(relay_policy: RelayPolicy) -> Relay {
FILE: src/tunnel.rs
type EstablishTunnelResult (line 26) | pub enum EstablishTunnelResult {
type ConnectionTunnel (line 61) | pub struct ConnectionTunnel<H, C, T> {
type TunnelTarget (line 70) | pub trait TunnelTarget {
method target_addr (line 72) | fn target_addr(&self) -> Self::Addr;
method has_nugget (line 73) | fn has_nugget(&self) -> bool;
method nugget (line 74) | fn nugget(&self) -> &Nugget;
type TunnelCtx (line 79) | pub struct TunnelCtx {
method fmt (line 320) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type TunnelStats (line 86) | pub struct TunnelStats {
function new (line 100) | pub fn new(
function start (line 126) | pub async fn start(mut self) -> io::Result<TunnelStats> {
function establish_tunnel (line 153) | async fn establish_tunnel(
function process_tunnel_request (line 195) | async fn process_tunnel_request(
function connect_to_target (line 250) | async fn connect_to_target(
function relay_connections (line 274) | pub async fn relay_connections<
function test_tunnel_ok (line 348) | async fn test_tunnel_ok() {
function test_tunnel_plain_text_ok (line 406) | async fn test_tunnel_plain_text_ok() {
function test_tunnel_request_timeout (line 460) | async fn test_tunnel_request_timeout() {
function test_tunnel_response_timeout (line 501) | async fn test_tunnel_response_timeout() {
function test_tunnel_upstream_timeout (line 544) | async fn test_tunnel_upstream_timeout() {
function test_tunnel_bad_target (line 588) | async fn test_tunnel_bad_target() {
function test_tunnel_bad_gateway (line 632) | async fn test_tunnel_bad_gateway() {
function test_tunnel_bad_request (line 676) | async fn test_tunnel_bad_request() {
function test_tunnel_not_allowed (line 721) | async fn test_tunnel_not_allowed() {
function build_config (line 764) | fn build_config(default_timeout: Duration) -> TunnelConfig {
type MockTargetConnector (line 787) | struct MockTargetConnector {
type Target (line 796) | type Target = HttpTunnelTarget;
type Stream (line 797) | type Stream = Mock;
method connect (line 799) | async fn connect(&mut self, target: &Self::Target) -> io::Result<Self::S...
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (122K chars).
[
{
"path": ".github/actions-rs/grcov.yml",
"chars": 297,
"preview": "output-type: lcov\noutput-path: ./lcov.info\nsource-dir: ./src\nignore-dir:\n - \"*.cargo/*\"\nignore:\n - \"*.cargo*\"\n - \"*ru"
},
{
"path": ".github/workflows/clippy.yml",
"chars": 1032,
"preview": "on: [push, pull_request]\nname: Clippy/Fmt\njobs:\n clippy:\n name: Clippy\n runs-on: ubuntu-latest\n steps:\n -"
},
{
"path": ".github/workflows/grcov.yml",
"chars": 1558,
"preview": "on: [push, pull_request]\n\nname: Code coverage with grcov\n\njobs:\n grcov:\n runs-on: ${{ matrix.os }}\n strategy:\n "
},
{
"path": ".github/workflows/tests.yml",
"chars": 355,
"preview": "on: [push, pull_request]\nname: Tests\n\nenv:\n CARGO_TERM_COLOR: always\n\njobs:\n build:\n\n runs-on: ubuntu-latest\n\n s"
},
{
"path": ".gitignore",
"chars": 37,
"preview": "/target\n/.idea\nCargo.lock\n/log/*.log\n"
},
{
"path": "COPYRIGHT",
"chars": 500,
"preview": "Copyrights in the http-tunnel project are retained by their contributors. No\ncopyright assignment is required to contrib"
},
{
"path": "Cargo.toml",
"chars": 1143,
"preview": "[package]\nname = \"http-tunnel\"\nversion = \"0.1.12\"\nauthors = [\"Eugene Retunsky\"]\nlicense = \"MIT OR Apache-2.0\"\nedition = "
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 4985,
"preview": "[](https://crates.io/crates/http-tunnel)\n\n\nclient_"
},
{
"path": "config/config.yaml",
"chars": 651,
"preview": "client_connection:\n initiation_timeout: 10s\n # we want to make sure connections are not under-utilized or over-utilize"
},
{
"path": "config/domain.crt",
"chars": 1383,
"preview": "-----BEGIN CERTIFICATE-----\nMIID0DCCArgCCQC39O6tC7bK5DANBgkqhkiG9w0BAQsFADCBqTELMAkGA1UEBhMC\nVVMxEzARBgNVBAgMCldhc2hpbmd"
},
{
"path": "config/domain.key",
"chars": 1708,
"preview": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCiyxVy9mmbJVRj\nAK6sOkcU21lmZRUQEa8LZx09tuU"
},
{
"path": "config/log4rs.yaml",
"chars": 836,
"preview": "refresh_rate: 30 seconds\n\nappenders:\n stdout:\n kind: console\n\n metrics:\n kind: rolling_file\n path: log/metric"
},
{
"path": "misc/diagrams/components-high-level.puml",
"chars": 363,
"preview": "@startuml\nClient <--> Tunnel: L4 handshake\nClient -> Tunnel: Negotiate Target\nactivate Tunnel\nTunnel <-> Target: L4 Hand"
},
{
"path": "misc/diagrams/components.puml",
"chars": 431,
"preview": "@startuml\nClient <--> Tunnel: TCP handshake\ngroup HTTPS Tunnel only\nClient <--> Tunnel: TLS handshake\nend\nClient -> Tunn"
},
{
"path": "src/configuration.rs",
"chars": 7006,
"preview": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENS"
},
{
"path": "src/http_tunnel_codec.rs",
"chars": 20130,
"preview": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENS"
},
{
"path": "src/main.rs",
"chars": 10444,
"preview": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENS"
},
{
"path": "src/proxy_target.rs",
"chars": 6242,
"preview": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENS"
},
{
"path": "src/relay.rs",
"chars": 18383,
"preview": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENS"
},
{
"path": "src/tunnel.rs",
"chars": 27168,
"preview": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENS"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the xnuter/http-tunnel GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (114.1 KB), approximately 28.4k tokens, and a symbol index with 142 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.