Compare commits

...

77 Commits
v0.3.0 ... main

Author SHA1 Message Date
Datong Sun
869c79422f chore(cargo): add Cargo.lock. Closes #138
Some checks failed
Docker image build / build (push) Has been cancelled
Rust / build (push) Has been cancelled
2025-03-14 22:46:01 -07:00
Datong Sun
201da45ee8 style(fake-tcp): fix warnings 2025-03-14 22:45:24 -07:00
SH Weng
333c6dd059
fix(deps): support newer version of rand crate
Closes #186.
2025-03-15 15:38:13 +08:00
Datong Sun
62f0278c1a fix(phantun): fix tokio-tun incompatiable API change
Some checks failed
Docker image build / build (push) Has been cancelled
Rust / build (push) Has been cancelled
2025-01-02 21:52:56 +08:00
Meng Zhuo
f436325d23
docs(README): fix typo
Some checks failed
Rust / build (push) Has been cancelled
2024-12-29 19:01:26 +08:00
Datong Sun
028a32d197
docs(readme): latest release is now v0.7.0
Some checks failed
Docker image build / build (push) Has been cancelled
Rust / build (push) Has been cancelled
2024-11-22 01:31:25 +08:00
Datong Sun
e452e40edd chore(release): remove MIPS targets due to being downgraded to Tier 3
support by Rust
2024-11-21 09:27:19 -08:00
dependabot[bot]
253ce54554 chore(deps): bump docker/build-push-action from 5 to 6
Some checks are pending
Docker image build / build (push) Waiting to run
Rust / build (push) Waiting to run
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-20 23:12:10 +08:00
Datong Sun
34d2350d1c chore(cargo): bump fake-tcp version to 0.6.0 and phantun to
`0.7.0`
2024-11-20 23:06:36 +08:00
Datong Sun
6955a1eb4c chore(deps): bump dependencies to latest 2024-11-20 23:06:36 +08:00
Datong Sun
e86c5c5c50
fix(fake-tcp): when connect()-ing, attempt to get ephemeral port using algorithm similar to Linux (#162)
* fix(fake-tcp): when `connect()`-ing, attempt to get ephemeral port 5 times to reduce the chance
of panicking

Fixes #79
2024-11-20 22:57:11 +08:00
Datong Sun
60f24d2563 Revert "docs(readme): update README.md to include incoming interface (`-i t…"
Some checks failed
Rust / build (push) Has been cancelled
This reverts commit 8c7f4e98b39518e10bf5cbd69987e4ccf702015b.
2024-08-27 23:19:09 +08:00
Ricardo Pallas
8c7f4e98b3
docs(readme): update README.md to include incoming interface (-i tun0) in client NAT commands example (#163)
Some checks are pending
Rust / build (push) Waiting to run
2024-08-26 19:50:07 +08:00
Datong Sun
90b93370ce chore(docs): update license year to 2024 2024-04-19 09:15:33 -07:00
dependabot[bot]
792ee46ec4 chore(deps): bump softprops/action-gh-release from 1 to 2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-26 23:41:37 +08:00
dependabot[bot]
f5cb4b1220 chore(deps): update nix requirement from 0.27 to 0.28
Updates the requirements on [nix](https://github.com/nix-rust/nix) to permit the latest version.
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: nix
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-26 23:41:27 +08:00
dependabot[bot]
8a0ec729e2 chore(deps): update tokio-tun requirement from 0.9 to 0.11
Updates the requirements on [tokio-tun](https://github.com/yaa110/tokio-tun) to permit the latest version.
- [Commits](https://github.com/yaa110/tokio-tun/compare/0.9.0...0.11.2)

---
updated-dependencies:
- dependency-name: tokio-tun
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-30 22:45:51 +08:00
Datong Sun
78dd7c13b1 style(phantun): fix Clippy warnings 2023-09-19 00:25:52 +08:00
dependabot[bot]
b58d58956b chore(deps): update nix requirement from 0.26 to 0.27
Updates the requirements on [nix](https://github.com/nix-rust/nix) to permit the latest version.
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: nix
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-19 00:15:35 +08:00
dependabot[bot]
310bb17516 chore(deps): update pnet requirement from 0.33 to 0.34
Updates the requirements on [pnet](https://github.com/libpnet/libpnet) to permit the latest version.
- [Release notes](https://github.com/libpnet/libpnet/releases)
- [Commits](https://github.com/libpnet/libpnet/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: pnet
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-19 00:07:50 +08:00
dependabot[bot]
632132b75a chore(deps): bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 22:09:47 +08:00
dependabot[bot]
48a0399f59 chore(deps): bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 22:09:34 +08:00
dependabot[bot]
590a048b5b chore(deps): bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 22:09:16 +08:00
dependabot[bot]
3fa8f86379 chore(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 22:08:56 +08:00
dependabot[bot]
805bf80cd8 chore(deps): update tokio-tun requirement from 0.7 to 0.9
Updates the requirements on [tokio-tun](https://github.com/yaa110/tokio-tun) to permit the latest version.
- [Commits](https://github.com/yaa110/tokio-tun/compare/0.7.0...0.9.0)

---
updated-dependencies:
- dependency-name: tokio-tun
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-27 23:38:42 +08:00
dependabot[bot]
aec3bcdeda chore(deps): update flume requirement from 0.10 to 0.11
Updates the requirements on [flume](https://github.com/zesterer/flume) to permit the latest version.
- [Changelog](https://github.com/zesterer/flume/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zesterer/flume/commits)

---
updated-dependencies:
- dependency-name: flume
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-27 12:58:08 +08:00
dependabot[bot]
9b1d3c0124 chore(deps): update pretty_env_logger requirement from 0.4 to 0.5
Updates the requirements on [pretty_env_logger](https://github.com/seanmonstar/pretty-env-logger) to permit the latest version.
- [Commits](https://github.com/seanmonstar/pretty-env-logger/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: pretty_env_logger
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-23 02:39:00 +08:00
dependabot[bot]
6c42f02b28 chore(deps): update socket2 requirement from 0.4 to 0.5
Updates the requirements on [socket2](https://github.com/rust-lang/socket2) to permit the latest version.
- [Release notes](https://github.com/rust-lang/socket2/releases)
- [Changelog](https://github.com/rust-lang/socket2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/socket2/compare/v0.4.0...v0.5.1)

---
updated-dependencies:
- dependency-name: socket2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-21 00:35:39 +08:00
Datong Sun
ee0bce0a96
chore(docker): use Debian as base image (#108)
Alpine has worse performance and musl does not work well with Rust `libc` version `3`,
use Debian instead.
2023-03-21 00:30:39 +08:00
dependabot[bot]
1f11d618e0 chore(deps): bump docker/build-push-action from 3 to 4
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-14 18:56:42 +08:00
Sing Yu Chan
d7913c1407
chore(docker): add Docker image build. Thanks @pexcn for the contributions!
Co-authored-by: Datong Sun <dndx@idndx.com>
2023-02-13 23:29:59 +08:00
dependabot[bot]
ee7ee5d5f9 chore(deps): update pnet requirement from 0.31 to 0.33
Updates the requirements on [pnet](https://github.com/libpnet/libpnet) to permit the latest version.
- [Release notes](https://github.com/libpnet/libpnet/releases)
- [Commits](https://github.com/libpnet/libpnet/compare/v0.31.0...v0.33.0)

---
updated-dependencies:
- dependency-name: pnet
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-13 23:16:04 +08:00
Datong Sun
af3a9061a1
docs(README): fix build status link 2022-12-29 10:41:32 +08:00
dependabot[bot]
7c98012a67 chore(deps): update nix requirement from 0.25 to 0.26
Updates the requirements on [nix](https://github.com/nix-rust/nix) to permit the latest version.
- [Release notes](https://github.com/nix-rust/nix/releases)
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.25.0...v0.26.1)

---
updated-dependencies:
- dependency-name: nix
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-29 16:31:23 +08:00
Datong Sun
b674268863
docs(README): bumped latest release version to v0.6.0 2022-10-25 23:54:08 +08:00
Datong Sun
b40ca10cc1 chore(phantun): bump fake-tcp dependency to v0.5.0 2022-10-25 08:36:42 -07:00
Datong Sun
30f0a1118b chore(fake-tcp): bump to v0.5.0 2022-10-25 08:35:49 -07:00
Datong Sun
fd607bc72a Revert "chore(release) temporary disable the MIPS musl target until new versions"
This reverts commit 9ff691d063afb1cc7c475e0298c818dd3fe0a514.
2022-10-25 08:25:30 -07:00
Datong Sun
939e4aa94e chore(phantun): bump to v0.6.0 2022-10-25 08:22:55 -07:00
Datong Sun
7bcfada87b refactor(phantun): work with clap 4.0. 2022-10-25 23:21:58 +08:00
dependabot[bot]
fe18a49d40 chore(deps): update clap requirement from 3.0 to 4.0
Updates the requirements on [clap](https://github.com/clap-rs/clap) to permit the latest version.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v3.0.0...v4.0.2)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-25 23:21:58 +08:00
dependabot[bot]
b707c5bd12 chore(deps): update tokio-tun requirement from 0.6 to 0.7
Updates the requirements on [tokio-tun](https://github.com/yaa110/tokio-tun) to permit the latest version.
- [Release notes](https://github.com/yaa110/tokio-tun/releases)
- [Commits](https://github.com/yaa110/tokio-tun/compare/0.6.0...0.7.0)

---
updated-dependencies:
- dependency-name: tokio-tun
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-20 00:04:20 -07:00
Paolo Barbolini
6af7757456
perf(fake-tcp) let the memory allocator zero initialize the BytesMut memory instead of resizing immediately after allocation
Co-authored-by: Datong Sun <dndx@idndx.com>
2022-08-18 19:00:09 -07:00
dependabot[bot]
f374ac8081 chore(deps): update nix requirement from 0.24 to 0.25
Updates the requirements on [nix](https://github.com/nix-rust/nix) to permit the latest version.
- [Release notes](https://github.com/nix-rust/nix/releases)
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: nix
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-18 18:54:57 -07:00
dependabot[bot]
50346c1ba0 chore(deps): update tokio-tun requirement from 0.5 to 0.6
Updates the requirements on [tokio-tun](https://github.com/yaa110/tokio-tun) to permit the latest version.
- [Release notes](https://github.com/yaa110/tokio-tun/releases)
- [Commits](https://github.com/yaa110/tokio-tun/compare/0.5.1...0.6.0)

---
updated-dependencies:
- dependency-name: tokio-tun
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-18 18:54:47 -07:00
dependabot[bot]
f649c79656 chore(deps): update pnet requirement from 0.30 to 0.31
Updates the requirements on [pnet](https://github.com/libpnet/libpnet) to permit the latest version.
- [Release notes](https://github.com/libpnet/libpnet/releases)
- [Commits](https://github.com/libpnet/libpnet/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: pnet
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-25 02:57:39 +08:00
Datong Sun
c91bda7e6a
docs(readme) add blog post link about performance optimizations 2022-05-30 14:10:41 +08:00
Datong Sun
00a308a005 docs(readme) update latest release version to v0.5.0 2022-05-13 08:52:21 -07:00
Datong Sun
9ff691d063 chore(release) temporary disable the MIPS musl target until new versions
of `libc` is released with `libc::sock_txtime`
2022-05-13 08:38:11 -07:00
Datong Sun
b5e79653f0 chore(phantun) bump to v0.5.0 2022-05-13 08:34:31 -07:00
Datong Sun
f496a7919b feat(phantun) new option --handshake-packet that allows additional
packet to be sent to the other end after TCP connection establishment
2022-05-13 23:14:05 +08:00
dependabot[bot]
bf6b9bc2ff chore(deps): update pnet requirement from 0.29 to 0.30
Updates the requirements on [pnet](https://github.com/libpnet/libpnet) to permit the latest version.
- [Release notes](https://github.com/libpnet/libpnet/releases)
- [Commits](https://github.com/libpnet/libpnet/compare/v0.29.0...v0.29.0)

---
updated-dependencies:
- dependency-name: pnet
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-13 23:06:03 +08:00
dependabot[bot]
47b9037968 chore(deps): update nix requirement from 0.23 to 0.24
Updates the requirements on [nix](https://github.com/nix-rust/nix) to permit the latest version.
- [Release notes](https://github.com/nix-rust/nix/releases)
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/commits)

---
updated-dependencies:
- dependency-name: nix
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-21 20:07:52 +08:00
Datong Sun
c2341b6662 docs(README) bump the release to v0.4.2 and fixed default site-local address in doc 2022-04-16 09:58:07 -07:00
Datong Sun
a3eff42453 chore(phantun) release v0.4.2 2022-04-16 09:55:40 -07:00
Datong Sun
87a42a1e23 fix(phantun) do not use the deprecated fec0::/10 block for default ULA
address
2022-04-17 00:55:16 +08:00
Datong Sun
851750b13d docs(README) bump release version to v0.4.1 and add end-to-end IPv6 documentation 2022-04-16 19:45:31 +08:00
Datong Sun
b89b683bb2 chore(phantun) bump to v0.4.1 2022-04-16 03:54:47 -07:00
Datong Sun
838cfa6738 style(phantun) slight fix on client usage 2022-04-16 03:53:27 -07:00
Datong Sun
827530f62c chore(phantun) release v0.4.0 and bump fake-tcp dependency to v0.4 2022-04-16 03:48:07 -07:00
Datong Sun
245cb9c7f4 chore(fake-tcp) release v0.4.0 2022-04-16 03:47:02 -07:00
Datong Sun
85555f2a34 feat(*) IPv6 support 2022-04-16 18:43:55 +08:00
Datong Sun
74183071f1 style(phantun) remove unnecessary tokio::select call 2022-04-15 23:01:44 +08:00
Datong Sun
2f4eaafccd
docs(README) fixed a typo 2022-04-10 23:40:44 +08:00
Datong Sun
1e3b632413 docs(README) add benchmarking results based on v0.3.2 2022-04-10 21:21:09 +08:00
Datong Sun
99bff568f6 chore(phantun) release v0.3.2 2022-04-10 06:10:57 -07:00
Datong Sun
91ad2c03a1 chore(fake-tcp) release v0.3.1 2022-04-10 06:09:48 -07:00
Datong Sun
581d80d08c perf(fake-tcp) use flume to avoid locking in receiver, improved single
connection performance by 300%
2022-04-10 21:07:12 +08:00
Datong Sun
55da4d6a62 docs(README) style improvements 2022-04-10 18:47:22 +08:00
Datong Sun
bb859be6b6
docs(README) add build status and docs.rs badge 2022-04-10 18:37:06 +08:00
Datong Sun
8d315ea4e7 docs(README) add packet header diagram 2022-04-10 18:26:28 +08:00
Datong Sun
21eabe8b82 docs(README) add description for safe Rust and bump latest release to
`v0.3.1`
2022-04-10 01:44:33 -07:00
Datong Sun
8a74b31c6e chore(phantun) bump fake-tcp dependency to v0.3.0 and release
`v0.3.1`
2022-04-10 01:37:59 -07:00
Datong Sun
ca14ba457f chore(fake-tcp) bump to v0.3.0 2022-04-10 01:36:45 -07:00
Datong Sun
33a0cfe567 docs(README) updated benchmarking results 2022-04-10 01:35:07 -07:00
Datong Sun
95dfd8ab54 fix(fake-tcp) fix an issue where RST generated is not following
the proper RFC requirement.

Send ACK every 128MB in lieu of data packets.
2022-04-10 16:33:53 +08:00
Datong Sun
1c35635091 docs(README) bump latest release version to v0.3.0 2022-04-09 08:49:29 -07:00
27 changed files with 2129 additions and 379 deletions

30
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Docker image build
on:
push:
paths-ignore:
- '**.md'
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
tags: phantun
platforms: linux/amd64

View File

@ -25,13 +25,9 @@ jobs:
- arm-unknown-linux-musleabihf
- aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- mips-unknown-linux-gnu
- mips-unknown-linux-musl
- mipsel-unknown-linux-gnu
- mipsel-unknown-linux-musl
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@ -50,7 +46,7 @@ jobs:
zip phantun_${{ matrix.target }}.zip phantun_client phantun_server
- name: Upload Github Assets
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: target/${{ matrix.target }}/release/*.zip
prerelease: ${{ contains(github.ref, '-') }}

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

1
.gitignore vendored
View File

@ -1,2 +1 @@
/target
Cargo.lock

1101
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
[workspace]
resolver = "2"
members = [
"fake-tcp",
"phantun",

View File

@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
Copyright (c) 2021-2024 Datong Sun (dndx@idndx.com)
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated

116
README.md
View File

@ -2,6 +2,9 @@
A lightweight and fast UDP to TCP obfuscator.
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dndx/phantun/rust.yml)
![docs.rs](https://img.shields.io/docsrs/fake-tcp)
Table of Contents
=================
@ -32,7 +35,7 @@ Table of Contents
# Latest release
[v0.2.5](https://github.com/dndx/phantun/releases/tag/v0.2.5)
[v0.7.0](https://github.com/dndx/phantun/releases/tag/v0.7.0)
# Overview
@ -52,6 +55,11 @@ connection from the perspective of firewalls/NAT devices.
Phantun means Phantom TUN, as it is an obfuscator for UDP traffic that does just enough work
to make it pass through stateful firewall/NATs as TCP packets.
Phantun is written in 100% safe Rust. It has been optimized extensively to scale well on multi-core
systems and has no issue saturating all available CPU resources on a fast connection.
See the [Performance](#performance) section for benchmarking results.
![Phantun benchmark results](images/phantun-vs-udp2raw-benchmark-result.png)
![Traffic flow diagram](images/traffic-flow.png)
# Usage
@ -64,30 +72,32 @@ It is also assumed that **Phantun Client** listens for incoming UDP packets at
`127.0.0.1:1234` (the `--local` option for client) and connects to Phantun Server at `10.0.0.1:4567`
(the `--remote` option for client).
Phantun creates TUN interface for both the Client and Server. For Client, Phantun assigns itself the IP address
`192.168.200.2` by default and for Server, it assigns `192.168.201.2` by default. Therefore, your Kernel must have
`net.ipv4.ip_forward` enabled and setup appropriate iptables rules for NAT between your physical
NIC address and Phantun's TUN interface address.
Phantun creates TUN interface for both the Client and Server. For **Client**, Phantun assigns itself the IP address
`192.168.200.2` and `fcc8::2` by default.
For **Server**, it assigns `192.168.201.2` and `fcc9::2` by default. Therefore, your Kernel must have
IPv4/IPv6 forwarding enabled and setup appropriate iptables/nftables rules for NAT between your physical
NIC address and Phantun's Tun interface address.
You may customize the name of Tun interface created by Phantun and the assigned addresses. Please
run the executable with `-h` options to see how to change them.
Another way to help understand this network topology (please see the diagram above for an illustration of this topology):
Phantun Client is like a machine with private IP address (`192.168.200.2`) behind a router.
Phantun Client is like a machine with private IP address (`192.168.200.2`/`fcc8::2`) behind a router.
In order for it to reach the Internet, you will need to SNAT the private IP address before it's traffic
leaves the NIC.
Phantun Server is like a server with private IP address (`192.168.201.2`) behind a router.
Phantun Server is like a server with private IP address (`192.168.201.2`/`fcc9::2`) behind a router.
In order to access it from the Internet, you need to `DNAT` it's listening port on the router
and change the destination IP address to where the server is listening for incoming connections.
In those cases, the machine/iptables running Phantun acts as the "router" that allows Phantun
to communicate with outside using it's private IP addresses.
As of Phantun v0.2.2, IPv6 support for UDP endpoints has been added, however Fake TCP IPv6 support
has not been finished yet. To specify an IPv6 address, use the following format: `[::1]:1234` with
the command line options.
As of Phantun v0.4.1, IPv6 is fully supported for both TCP and UDP sides.
To specify an IPv6 address, use the following format: `[::1]:1234` with
the command line options. Resolving AAAA record is also supported. Please run the program
with `-h` to see detailed options on how to control the IPv6 behavior.
[Back to TOC](#table-of-contents)
@ -95,6 +105,12 @@ the command line options.
Edit `/etc/sysctl.conf`, add `net.ipv4.ip_forward=1` and run `sudo sysctl -p /etc/sysctl.conf`.
<details>
<summary>IPv6 specific config</summary>
`net.ipv6.conf.all.forwarding=1` will need to be set as well.
</details>
[Back to TOC](#table-of-contents)
## 2. Add required firewall rules
@ -120,12 +136,16 @@ table inet nat {
}
```
Note: The above rule uses `inet` as the table family type, so it is compatible with
both IPv4 and IPv6 usage.
[Back to TOC](#table-of-contents)
#### Using iptables
```
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
```
[Back to TOC](#table-of-contents)
@ -142,10 +162,11 @@ actual TCP port number used by Phantun server
#### Using nftables
```
table ip nat {
table inet nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif eth0 tcp dport 4567 dnat to 192.168.201.2
iif eth0 tcp dport 4567 dnat ip to 192.168.201.2
iif eth0 tcp dport 4567 dnat ip6 to fcc9::2
}
}
```
@ -156,6 +177,7 @@ table ip nat {
```
iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2
ip6tables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination fcc9::2
```
[Back to TOC](#table-of-contents)
@ -194,6 +216,10 @@ Or use host name with `--remote`:
RUST_LOG=info /usr/local/bin/phantun_server --local 4567 --remote example.com:1234
```
Note: Server by default assigns both IPv4 and IPv6 private address to the Tun interface.
If you do not wish to use IPv6, you can simply skip creating the IPv6 DNAT rule above and
the presence of IPv6 address on the Tun interface should have no side effect to the server.
[Back to TOC](#table-of-contents)
### Client
@ -211,25 +237,37 @@ Or use host name with `--remote`:
RUST_LOG=info /usr/local/bin/phantun_client --local 127.0.0.1:1234 --remote example.com:4567
```
<details>
<summary>IPv6 specific config</summary>
```
RUST_LOG=info /usr/local/bin/phantun_client --local 127.0.0.1:1234 --remote [fdxx::1234]:4567
```
Domain name with AAAA record is also supported.
</details>
[Back to TOC](#table-of-contents)
# MTU overhead
Phantun aims to keep tunneling overhead to the minimum. The overhead compared to a plain UDP packet
is the following:
is the following (using IPv4 below as an example):
Standard UDP packet: 20 byte IP header + 8 byte UDP header = 28 bytes
**Standard UDP packet:** `20 byte IP header + 8 byte UDP header = 28 bytes`
Phantun obfuscated UDP packet: 20 byte IP header + 20 byte TCP header = 40 bytes
**Obfuscated packet:** `20 byte IP header + 20 byte TCP header = 40 bytes`
Note that Phantun does not add any additional header other than IP and TCP headers in order to pass through
stateful packet inspection!
Phantun's additional overhead: 12 bytes. I other words, when using Phantun, the usable payload for
Phantun's additional overhead: `12 bytes`. In other words, when using Phantun, the usable payload for
UDP packet is reduced by 12 bytes. This is the minimum overhead possible when doing such kind
of obfuscation.
![Packet header diagram](images/packet-headers.png)
[Back to TOC](#table-of-contents)
## MTU calculation for WireGuard
@ -237,14 +275,25 @@ of obfuscation.
For people who use Phantun to tunnel [WireGuard®](https://www.wireguard.com) UDP packets, here are some guidelines on figuring
out the correct MTU to use for your WireGuard interface.
WireGuard MTU = Interface MTU - IP header (20 bytes) - TCP header (20 bytes) - WireGuard overhead (32 bytes)
```
WireGuard MTU = Interface MTU - IPv4 header (20 bytes) - TCP header (20 bytes) - WireGuard overhead (32 bytes)
```
or
```
WireGuard MTU = Interface MTU - IPv6 header (40 bytes) - TCP header (20 bytes) - WireGuard overhead (32 bytes)
```
For example, for a Ethernet interface with 1500 bytes MTU, the WireGuard interface MTU should be set as:
1500 - 20 - 20 - 32 = 1428 bytes
IPv4: `1500 - 20 - 20 - 32 = 1428 bytes`
IPv6: `1500 - 40 - 20 - 32 = 1408 bytes`
The resulted Phantun TCP data packet will be 1500 bytes which does not exceed the
interface MTU of 1500.
interface MTU of 1500. Please note it is strongly recommended to use the same interface
MTU for both ends of a WireGuard tunnel, or unexpected packet loss may occur and these issues are
generally very hard to troubleshoot.
[Back to TOC](#table-of-contents)
@ -264,19 +313,28 @@ For users who wish to use `fake-tcp` library inside their own project, refer to
# Performance
Performance was tested on AWS t3.xlarge instance with 4 vCPUs and 5 Gb/s NIC. WireGuard was used
for tunneling TCP/UDP traffic between two test instances and MTU has been tuned to avoid fragmentation.
Performance was tested on 2 AWS `t4g.xlarge` instances with 4 vCPUs and 5 Gb/s NIC over LAN. `nftables` was used to redirect
UDP stream of `iperf3` to go through the Phantun/udp2raw tunnel between two test instances and MTU has been tuned to avoid fragmentation.
| | WireGuard | WireGuard + Phantun | WireGuard + udp2raw (cipher-mode=none auth-mode=none disable-anti-replay) |
|-----------------|-------------|---------------------|---------------------------------------------------------------------------|
| iperf3 -c IP -R | 1.56 Gbit/s | 540 Mbit/s | 369 Mbit/s |
| iperf3 -c IP | 1.71 Gbit/s | 519 Mbit/s | 312 Mbit/s |
Phantun `v0.3.2` and `udp2raw_arm_asm_aes` `20200818.0` was used. These were the latest release of both projects as of Apr 2022.
Test command: `iperf3 -c <IP> -p <PORT> -R -u -l 1400 -b 1000m -t 30 -P 5`
| Mode | Send Speed | Receive Speed | Overall CPU Usage |
|---------------------------------------------------------------------------------|----------------|----------------|-----------------------------------------------------|
| Direct (1 stream) | 3.00 Gbits/sec | 2.37 Gbits/sec | 25% (1 core at 100%) |
| Phantun (1 stream) | 1.30 Gbits/sec | 1.20 Gbits/sec | 60% (1 core at 100%, 3 cores at 50%) |
| udp2raw (`cipher-mode=none` `auth-mode=none` `disable-anti-replay`) (1 stream) | 1.30 Gbits/sec | 715 Mbits/sec | 40% (1 core at 100%, 1 core at 50%, 2 cores idling) |
| Direct connection (5 streams) | 5.00 Gbits/sec | 3.64 Gbits/sec | 25% (1 core at 100%) |
| Phantun (5 streams) | 5.00 Gbits/sec | 2.38 Gbits/sec | 95% (all cores utilized) |
| udp2raw (`cipher-mode=none` `auth-mode=none` `disable-anti-replay`) (5 streams) | 5.00 Gbits/sec | 770 Mbits/sec | 50% (2 cores at 100%) |
Writeup on some of the techniques used in Phantun to achieve this performance result: [Writing Highly Efficient UDP Server in Rust](https://idndx.com/writing-highly-efficient-udp-server-in-rust/).
[Back to TOC](#table-of-contents)
# Future plans
* IPv6 support for fake-tcp
* Load balancing a single UDP stream into multiple TCP streams
* Integration tests
* Auto insertion/removal of required firewall rules
@ -300,17 +358,17 @@ Here is a quick overview of comparison between those two to help you choose:
| UDP over UDP obfuscation | ❌ | ✅ |
| Multi-threaded | ✅ | ❌ |
| Throughput | Better | Good |
| Raw IP mode | TUN interface | Raw sockets + BPF |
| Layer 3 mode | TUN interface | Raw sockets + BPF |
| Tunneling MTU overhead | 12 bytes | 44 bytes |
| Seprate TCP connections for each UDP connection | Client/Server | Server only |
| Anti-replay, encryption | ❌ | ✅ |
| IPv6 | UDP only | ✅ |
| IPv6 | ✅ | ✅ |
[Back to TOC](#table-of-contents)
# License
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)> or the MIT license

2
docker/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
README.md
docker-compose.yml

33
docker/Dockerfile Normal file
View File

@ -0,0 +1,33 @@
#
# Dockerfile for phantun
#
#
# Build stage
#
FROM rust:latest AS builder
COPY . /phantun
RUN cd phantun \
&& cargo build --release \
&& strip target/release/server target/release/client \
&& install target/release/server /usr/local/bin/phantun-server \
&& install target/release/client /usr/local/bin/phantun-client \
&& cd - \
&& rm -r phantun
#
# Runtime stage
#
FROM debian:latest
COPY --from=builder /usr/local/bin/phantun-server /usr/local/bin/
COPY --from=builder /usr/local/bin/phantun-client /usr/local/bin/
COPY docker/phantun.sh /usr/local/bin/
ENV USE_IPTABLES_NFT_BACKEND=0
ENV RUST_LOG=INFO
ENTRYPOINT ["phantun.sh"]
CMD ["phantun-server", "--help"]

11
docker/README.md Normal file
View File

@ -0,0 +1,11 @@
# phantun (docker)
## Build
```sh
docker build -t phantun -f docker/Dockerfile .
```
## Usage
It is recommended to use docker-compose, see [docker-compose.yml](docker-compose.yml) for details.

26
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,26 @@
version: '3.9'
services:
phantun-server:
image: phantun
container_name: phantun-server
restart: unless-stopped
network_mode: host
privileged: true
environment:
USE_IPTABLES_NFT_BACKEND: 0
RUST_LOG: INFO
command: >
phantun-server --local 1985 --remote 127.0.0.1:1984 --ipv4-only
phantun-client:
image: phantun
container_name: phantun-client
restart: unless-stopped
network_mode: host
privileged: true
environment:
USE_IPTABLES_NFT_BACKEND: 0
RUST_LOG: INFO
command: >
phantun-client --local 127.0.0.1:1984 --remote 11.22.33.44:1985 --ipv4-only

209
docker/phantun.sh Executable file
View File

@ -0,0 +1,209 @@
#!/bin/sh
# alias settings must be global, and must be defined before the function being called with the alias
if [ "$USE_IPTABLES_NFT_BACKEND" = 1 ]; then
alias iptables=iptables-nft
alias iptables-save=iptables-nft-save
alias ip6tables=ip6tables-nft
alias ip6tables-save=ip6tables-nft-save
fi
info() {
local green='\e[0;32m'
local clear='\e[0m'
local time=$(date '+%Y-%m-%d %T')
printf "${green}[${time}] [INFO]: ${clear}%s\n" "$*"
}
warn() {
local yellow='\e[1;33m'
local clear='\e[0m'
local time=$(date '+%Y-%m-%d %T')
printf "${yellow}[${time}] [WARN]: ${clear}%s\n" "$*" >&2
}
error() {
local red='\e[0;31m'
local clear='\e[0m'
local time=$(date '+%Y-%m-%d %T')
printf "${red}[${time}] [ERROR]: ${clear}%s\n" "$*" >&2
}
_get_default_iface() {
ip -4 route show default | awk -F 'dev' '{print $2}' | awk '{print $1}'
}
_get_default6_iface() {
ip -6 route show default | awk -F 'dev' '{print $2}' | awk '{print $1}'
}
_get_addr_by_iface() {
ip -4 addr show dev "$1" | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | head -1
}
_get_addr6_by_iface() {
ip -6 addr show dev "$1" | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | head -1
}
_check_rule_by_comment() {
iptables-save | grep -q "$1"
}
_check_rule6_by_comment() {
ip6tables-save | grep -q "$1"
}
_is_server_mode() {
[ "$1" = "phantun-server" ]
}
_is_ipv4_only() {
case "$@" in
*-4*|*--ipv4-only*)
return 0
;;
*\ -4*|*\ --ipv4-only*)
return 0
;;
esac
return 1
}
_get_tun_from_args() {
local tun=$(echo "$@" | awk -F '--tun' '{print $2}' | awk '{print $1}')
echo ${tun:=tun0}
}
_get_peer_from_args() {
local peer=$(echo "$@" | awk -F '--tun-peer' '{print $2}' | awk '{print $1}')
_is_server_mode "$1" && echo ${peer:=192.168.201.2} || echo ${peer:=192.168.200.2}
}
_get_peer6_from_args() {
local peer=$(echo "$@" | awk -F '--tun-peer6' '{print $2}' | awk '{print $1}')
_is_server_mode "$1" && echo ${peer:=fcc9::2} || echo ${peer:=fcc8::2}
}
_get_port_from_args() {
local value=$(echo "$@" | awk -F '-l|--local' '{print $2}' | awk '{print $1}')
_is_server_mode "$1" && echo $value || echo $value | awk -F ':' '{print $2}'
}
_iptables() {
iptables -w 10 "$@"
}
_ip6tables() {
ip6tables -w 10 "$@"
}
apply_sysctl() {
info "apply sysctl: $(sysctl -w net.ipv4.ip_forward=1)"
! _is_ipv4_only "$@" || return
info "apply sysctl: $(sysctl -w net.ipv6.conf.all.forwarding=1)"
}
apply_iptables() {
local interface=$(_get_default_iface)
local address=$(_get_addr_by_iface "${interface}")
local tun=$(_get_tun_from_args "$@")
local peer=$(_get_peer_from_args "$@")
local port=$(_get_port_from_args "$@")
local comment="phantun_${tun}_${port}"
if _check_rule_by_comment "${comment}"; then
warn "iptables rules already exist, maybe needs to check."
else
_iptables -A FORWARD -i $tun -j ACCEPT -m comment --comment "${comment}" || error "iptables filter rule add failed."
_iptables -A FORWARD -o $tun -j ACCEPT -m comment --comment "${comment}" || error "iptables filter rule add failed."
if _is_server_mode "$1"; then
info "iptables DNAT rule added: [${comment}]: ${interface} -> ${tun}, ${address} -> ${peer}"
_iptables -t nat -A PREROUTING -p tcp -i $interface --dport $port -j DNAT --to-destination $peer \
-m comment --comment "${comment}" || error "iptables DNAT rule add failed."
else
info "iptables SNAT rule added: [${comment}]: ${tun} -> ${interface}, ${peer} -> ${address}"
_iptables -t nat -A POSTROUTING -s $peer -o $interface -j SNAT --to-source $address \
-m comment --comment "${comment}" || error "iptables SNAT rule add failed."
fi
fi
}
apply_ip6tables() {
! _is_ipv4_only "$@" || return
local interface=$(_get_default6_iface)
local address=$(_get_addr6_by_iface "${interface}")
local tun=$(_get_tun_from_args "$@")
local peer=$(_get_peer6_from_args "$@")
local port=$(_get_port_from_args "$@")
local comment="phantun_${tun}_${port}"
if _check_rule6_by_comment "${comment}"; then
warn "ip6tables rules already exist, maybe needs to check."
else
_ip6tables -A FORWARD -i $tun -j ACCEPT -m comment --comment "${comment}" || error "ip6tables filter rule add failed."
_ip6tables -A FORWARD -o $tun -j ACCEPT -m comment --comment "${comment}" || error "ip6tables filter rule add failed."
if _is_server_mode "$1"; then
info "ip6tables DNAT rule added: [${comment}]: ${interface} -> ${tun}, ${address} -> ${peer}"
_ip6tables -t nat -A PREROUTING -p tcp -i $interface --dport $port -j DNAT --to-destination $peer \
-m comment --comment "${comment}" || error "ip6tables DNAT rule add failed."
else
info "ip6tables SNAT rule added: [${comment}]: ${tun} -> ${interface}, ${peer} -> ${address}"
_ip6tables -t nat -A POSTROUTING -s $peer -o $interface -j SNAT --to-source $address \
-m comment --comment "${comment}" || error "ip6tables SNAT rule add failed."
fi
fi
}
stop_process() {
kill $(pidof phantun-server phantun-client)
info "terminate phantun process."
}
revoke_iptables() {
local tun=$(_get_tun_from_args "$@")
local port=$(_get_port_from_args "$@")
local comment="phantun_${tun}_${port}"
iptables-save -t filter | grep "${comment}" | while read rule; do
_iptables -t filter ${rule/-A/-D} || error "iptables filter rule remove failed."
done
iptables-save -t nat | grep "${comment}" | while read rule; do
_iptables -t nat ${rule/-A/-D} || error "iptables nat rule remove failed."
done
info "iptables rule: [${comment}] removed."
}
revoke_ip6tables() {
! _is_ipv4_only "$@" || return
local tun=$(_get_tun_from_args "$@")
local port=$(_get_port_from_args "$@")
local comment="phantun_${tun}_${port}"
ip6tables-save -t filter | grep "${comment}" | while read rule; do
_ip6tables -t filter ${rule/-A/-D} || error "ip6tables filter rule remove failed."
done
ip6tables-save -t nat | grep "${comment}" | while read rule; do
_ip6tables -t nat ${rule/-A/-D} || error "ip6tables nat rule remove failed."
done
info "ip6tables rule: [${comment}] removed."
}
graceful_stop() {
warn "caught SIGTERM or SIGINT signal, graceful stopping..."
stop_process
revoke_iptables "$@"
revoke_ip6tables "$@"
}
start_phantun() {
trap 'graceful_stop "$@"' SIGTERM SIGINT
apply_sysctl "$@"
apply_iptables "$@"
apply_ip6tables "$@"
"$@" &
wait
}
start_phantun "$@"

View File

@ -1,6 +1,6 @@
[package]
name = "fake-tcp"
version = "0.2.4"
version = "0.6.0"
edition = "2021"
authors = ["Datong Sun <dndx@idndx.com>"]
license = "MIT OR Apache-2.0"
@ -16,9 +16,10 @@ benchmark = []
[dependencies]
bytes = "1"
pnet = "0.29"
tokio = { version = "1.14", features = ["full"] }
rand = { version = "0.8", features = ["small_rng"] }
log = "0.4"
internet-checksum = "0.2"
tokio-tun = "0.5"
pnet = "0"
tokio = { version = "1", features = ["full"] }
rand = { version = "0", features = ["small_rng"] }
log = "0"
internet-checksum = "0"
tokio-tun = "0"
flume = "0"

View File

@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
Copyright (c) 2021-2024 Datong Sun (dndx@idndx.com)
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated

View File

@ -49,27 +49,30 @@ use pnet::packet::{tcp, Packet};
use rand::prelude::*;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, RwLock};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::{
atomic::{AtomicU32, Ordering},
Arc, RwLock,
};
use tokio::sync::broadcast;
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::sync::Mutex as AsyncMutex;
use tokio::sync::mpsc;
use tokio::time;
use tokio_tun::Tun;
const TIMEOUT: time::Duration = time::Duration::from_secs(1);
const RETRIES: usize = 6;
const MPSC_BUFFER_LEN: usize = 512;
const MPMC_BUFFER_LEN: usize = 512;
const MPSC_BUFFER_LEN: usize = 128;
const MAX_UNACKED_LEN: u32 = 128 * 1024 * 1024; // 128MB
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct AddrTuple {
local_addr: SocketAddrV4,
remote_addr: SocketAddrV4,
local_addr: SocketAddr,
remote_addr: SocketAddr,
}
impl AddrTuple {
fn new(local_addr: SocketAddrV4, remote_addr: SocketAddrV4) -> AddrTuple {
fn new(local_addr: SocketAddr, remote_addr: SocketAddr) -> AddrTuple {
AddrTuple {
local_addr,
remote_addr,
@ -78,17 +81,18 @@ impl AddrTuple {
}
struct Shared {
tuples: RwLock<HashMap<AddrTuple, Sender<Bytes>>>,
tuples: RwLock<HashMap<AddrTuple, flume::Sender<Bytes>>>,
listening: RwLock<HashSet<u16>>,
tun: Vec<Arc<Tun>>,
ready: Sender<Socket>,
ready: mpsc::Sender<Socket>,
tuples_purge: broadcast::Sender<AddrTuple>,
}
pub struct Stack {
shared: Arc<Shared>,
local_ip: Ipv4Addr,
ready: Receiver<Socket>,
local_ip6: Option<Ipv6Addr>,
ready: mpsc::Receiver<Socket>,
}
pub enum State {
@ -101,11 +105,12 @@ pub enum State {
pub struct Socket {
shared: Arc<Shared>,
tun: Arc<Tun>,
incoming: AsyncMutex<Receiver<Bytes>>,
local_addr: SocketAddrV4,
remote_addr: SocketAddrV4,
incoming: flume::Receiver<Bytes>,
local_addr: SocketAddr,
remote_addr: SocketAddr,
seq: AtomicU32,
ack: AtomicU32,
last_ack: AtomicU32,
state: State,
}
@ -120,34 +125,38 @@ impl Socket {
fn new(
shared: Arc<Shared>,
tun: Arc<Tun>,
local_addr: SocketAddrV4,
remote_addr: SocketAddrV4,
local_addr: SocketAddr,
remote_addr: SocketAddr,
ack: Option<u32>,
state: State,
) -> (Socket, Sender<Bytes>) {
let (incoming_tx, incoming_rx) = mpsc::channel(MPSC_BUFFER_LEN);
) -> (Socket, flume::Sender<Bytes>) {
let (incoming_tx, incoming_rx) = flume::bounded(MPMC_BUFFER_LEN);
(
Socket {
shared,
tun,
incoming: AsyncMutex::new(incoming_rx),
incoming: incoming_rx,
local_addr,
remote_addr,
seq: AtomicU32::new(0),
ack: AtomicU32::new(ack.unwrap_or(0)),
last_ack: AtomicU32::new(ack.unwrap_or(0)),
state,
},
incoming_tx,
)
}
fn build_tcp_packet(&self, flags: u16, payload: Option<&[u8]>) -> Bytes {
fn build_tcp_packet(&self, flags: u8, payload: Option<&[u8]>) -> Bytes {
let ack = self.ack.load(Ordering::Relaxed);
self.last_ack.store(ack, Ordering::Relaxed);
build_tcp_packet(
self.local_addr,
self.remote_addr,
self.seq.load(Ordering::Relaxed),
self.ack.load(Ordering::Relaxed),
ack,
flags,
payload,
)
@ -165,12 +174,7 @@ impl Socket {
State::Established => {
let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, Some(payload));
self.seq.fetch_add(payload.len() as u32, Ordering::Relaxed);
tokio::select! {
res = self.tun.send(&buf) => {
res.ok().and(Some(()))
},
}
self.tun.send(&buf).await.ok().and(Some(()))
}
_ => unreachable!(),
}
@ -186,9 +190,8 @@ impl Socket {
pub async fn recv(&self, buf: &mut [u8]) -> Option<usize> {
match self.state {
State::Established => {
let mut incoming = self.incoming.lock().await;
incoming.recv().await.and_then(|raw_buf| {
let (_v4_packet, tcp_packet) = parse_ipv4_packet(&raw_buf);
self.incoming.recv_async().await.ok().and_then(|raw_buf| {
let (_v4_packet, tcp_packet) = parse_ip_packet(&raw_buf).unwrap();
if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 {
info!("Connection {} reset by peer", self);
@ -197,8 +200,18 @@ impl Socket {
let payload = tcp_packet.payload();
self.ack
.store(tcp_packet.get_sequence().wrapping_add(1), Ordering::Relaxed);
let new_ack = tcp_packet.get_sequence().wrapping_add(payload.len() as u32);
let last_ask = self.last_ack.load(Ordering::Relaxed);
self.ack.store(new_ack, Ordering::Relaxed);
if new_ack.overflowing_sub(last_ask).0 > MAX_UNACKED_LEN {
let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, None);
if let Err(e) = self.tun.try_send(&buf) {
// This should not really happen as we have not sent anything for
// quite some time...
info!("Connection {} unable to send idling ACK back: {}", self, e)
}
}
buf[..payload.len()].copy_from_slice(payload);
@ -220,10 +233,10 @@ impl Socket {
info!("Sent SYN + ACK to client");
}
State::SynReceived => {
let res = time::timeout(TIMEOUT, self.incoming.lock().await.recv()).await;
let res = time::timeout(TIMEOUT, self.incoming.recv_async()).await;
if let Ok(buf) = res {
let buf = buf.unwrap();
let (_v4_packet, tcp_packet) = parse_ipv4_packet(&buf);
let (_v4_packet, tcp_packet) = parse_ip_packet(&buf).unwrap();
if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 {
return;
@ -264,10 +277,10 @@ impl Socket {
info!("Sent SYN to server");
}
State::SynSent => {
match time::timeout(TIMEOUT, self.incoming.lock().await.recv()).await {
match time::timeout(TIMEOUT, self.incoming.recv_async()).await {
Ok(buf) => {
let buf = buf.unwrap();
let (_v4_packet, tcp_packet) = parse_ipv4_packet(&buf);
let (_v4_packet, tcp_packet) = parse_ip_packet(&buf).unwrap();
if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 {
return None;
@ -315,7 +328,14 @@ impl Drop for Socket {
// purge cache
self.shared.tuples_purge.send(tuple).unwrap();
let buf = self.build_tcp_packet(tcp::TcpFlags::RST, None);
let buf = build_tcp_packet(
self.local_addr,
self.remote_addr,
self.seq.load(Ordering::Relaxed),
0,
tcp::TcpFlags::RST,
None,
);
if let Err(e) = self.tun.try_send(&buf) {
warn!("Unable to send RST to remote end: {}", e);
}
@ -341,7 +361,7 @@ impl Stack {
/// When more than one [`Tun`](tokio_tun::Tun) object is passed in, same amount
/// of reader will be spawned later. This allows user to utilize the performance
/// benefit of Multiqueue Tun support on machines with SMP.
pub fn new(tun: Vec<Tun>) -> Stack {
pub fn new(tun: Vec<Tun>, local_ip: Ipv4Addr, local_ip6: Option<Ipv6Addr>) -> Stack {
let tun: Vec<Arc<Tun>> = tun.into_iter().map(Arc::new).collect();
let (ready_tx, ready_rx) = mpsc::channel(MPSC_BUFFER_LEN);
let (tuples_purge_tx, _tuples_purge_rx) = broadcast::channel(16);
@ -352,7 +372,6 @@ impl Stack {
ready: ready_tx,
tuples_purge: tuples_purge_tx.clone(),
});
let local_ip = tun[0].destination().unwrap();
for t in tun {
tokio::spawn(Stack::reader_task(
@ -365,6 +384,7 @@ impl Stack {
Stack {
shared,
local_ip,
local_ip6,
ready: ready_rx,
}
}
@ -381,26 +401,51 @@ impl Stack {
/// Connects to the remote end. `None` returned means
/// the connection attempt failed.
pub async fn connect(&mut self, addr: SocketAddrV4) -> Option<Socket> {
let mut rng = SmallRng::from_entropy();
let local_port: u16 = rng.gen_range(1024..65535);
let local_addr = SocketAddrV4::new(self.local_ip, local_port);
let tuple = AddrTuple::new(local_addr, addr);
let (mut sock, incoming) = Socket::new(
self.shared.clone(),
self.shared.tun.choose(&mut rng).unwrap().clone(),
local_addr,
addr,
None,
State::Idle,
);
pub async fn connect(&mut self, addr: SocketAddr) -> Option<Socket> {
let mut rng = SmallRng::from_os_rng();
for local_port in rng.random_range(32768..=60999)..=60999 {
let local_addr = SocketAddr::new(
if addr.is_ipv4() {
IpAddr::V4(self.local_ip)
} else {
IpAddr::V6(self.local_ip6.expect("IPv6 local address undefined"))
},
local_port,
);
let tuple = AddrTuple::new(local_addr, addr);
let mut sock;
{
let mut tuples = self.shared.tuples.write().unwrap();
assert!(tuples.insert(tuple, incoming.clone()).is_none());
{
let mut tuples = self.shared.tuples.write().unwrap();
if tuples.contains_key(&tuple) {
trace!(
"Fake TCP connection to {}, local port number {} already in use, trying another one",
addr, local_port
);
continue;
}
let incoming;
(sock, incoming) = Socket::new(
self.shared.clone(),
self.shared.tun.choose(&mut rng).unwrap().clone(),
local_addr,
addr,
None,
State::Idle,
);
assert!(tuples.insert(tuple, incoming).is_none());
}
return sock.connect().await.map(|_| sock);
}
sock.connect().await.map(|_| sock)
error!(
"Fake TCP connection to {} failed, emphemeral port number exhausted",
addr
);
None
}
async fn reader_task(
@ -408,11 +453,10 @@ impl Stack {
shared: Arc<Shared>,
mut tuples_purge: broadcast::Receiver<AddrTuple>,
) {
let mut tuples: HashMap<AddrTuple, Sender<Bytes>> = HashMap::new();
let mut tuples: HashMap<AddrTuple, flume::Sender<Bytes>> = HashMap::new();
loop {
let mut buf = BytesMut::with_capacity(MAX_PACKET_LEN);
buf.resize(MAX_PACKET_LEN, 0);
let mut buf = BytesMut::zeroed(MAX_PACKET_LEN);
tokio::select! {
size = tun.recv(&mut buf) => {
@ -420,95 +464,95 @@ impl Stack {
buf.truncate(size);
let buf = buf.freeze();
if buf[0] >> 4 != 4 {
// not an IPv4 packet
continue;
}
match parse_ip_packet(&buf) {
Some((ip_packet, tcp_packet)) => {
let local_addr =
SocketAddr::new(ip_packet.get_destination(), tcp_packet.get_destination());
let remote_addr = SocketAddr::new(ip_packet.get_source(), tcp_packet.get_source());
let (ip_packet, tcp_packet) = parse_ipv4_packet(&buf);
let local_addr =
SocketAddrV4::new(ip_packet.get_destination(), tcp_packet.get_destination());
let remote_addr = SocketAddrV4::new(ip_packet.get_source(), tcp_packet.get_source());
let tuple = AddrTuple::new(local_addr, remote_addr);
if let Some(c) = tuples.get(&tuple) {
if c.send_async(buf).await.is_err() {
trace!("Cache hit, but receiver already closed, dropping packet");
}
let tuple = AddrTuple::new(local_addr, remote_addr);
if let Some(c) = tuples.get(&tuple) {
if c.send(buf).await.is_err() {
trace!("Cache hit, but receiver already closed, dropping packet");
continue;
// If not Ok, receiver has been closed and just fall through to the slow
// path below
} else {
trace!("Cache miss, checking the shared tuples table for connection");
let sender = {
let tuples = shared.tuples.read().unwrap();
tuples.get(&tuple).cloned()
};
if let Some(c) = sender {
trace!("Storing connection information into local tuples");
tuples.insert(tuple, c.clone());
c.send_async(buf).await.unwrap();
continue;
}
}
if tcp_packet.get_flags() == tcp::TcpFlags::SYN
&& shared
.listening
.read()
.unwrap()
.contains(&tcp_packet.get_destination())
{
// SYN seen on listening socket
if tcp_packet.get_sequence() == 0 {
let (sock, incoming) = Socket::new(
shared.clone(),
tun.clone(),
local_addr,
remote_addr,
Some(tcp_packet.get_sequence() + 1),
State::Idle,
);
assert!(shared
.tuples
.write()
.unwrap()
.insert(tuple, incoming)
.is_none());
tokio::spawn(sock.accept());
} else {
trace!("Bad TCP SYN packet from {}, sending RST", remote_addr);
let buf = build_tcp_packet(
local_addr,
remote_addr,
0,
tcp_packet.get_sequence() + tcp_packet.payload().len() as u32 + 1, // +1 because of SYN flag set
tcp::TcpFlags::RST | tcp::TcpFlags::ACK,
None,
);
shared.tun[0].try_send(&buf).unwrap();
}
} else if (tcp_packet.get_flags() & tcp::TcpFlags::RST) == 0 {
info!("Unknown TCP packet from {}, sending RST", remote_addr);
let buf = build_tcp_packet(
local_addr,
remote_addr,
tcp_packet.get_acknowledgement(),
tcp_packet.get_sequence() + tcp_packet.payload().len() as u32,
tcp::TcpFlags::RST | tcp::TcpFlags::ACK,
None,
);
shared.tun[0].try_send(&buf).unwrap();
}
}
continue;
// If not Ok, receiver has been closed and just fall through to the slow
// path below
} else {
trace!("Cache miss, checking the shared tuples table for connection");
let sender = {
let tuples = shared.tuples.read().unwrap();
tuples.get(&tuple).cloned()
};
if let Some(c) = sender {
trace!("Storing connection information into local tuples");
tuples.insert(tuple, c.clone());
c.send(buf).await.unwrap();
None => {
continue;
}
}
if tcp_packet.get_flags() == tcp::TcpFlags::SYN
&& shared
.listening
.read()
.unwrap()
.contains(&tcp_packet.get_destination())
{
// SYN seen on listening socket
if tcp_packet.get_sequence() == 0 {
let (sock, incoming) = Socket::new(
shared.clone(),
tun.clone(),
local_addr,
remote_addr,
Some(tcp_packet.get_sequence() + 1),
State::Idle,
);
assert!(shared
.tuples
.write()
.unwrap()
.insert(tuple, incoming)
.is_none());
tokio::spawn(sock.accept());
} else {
trace!("Bad TCP SYN packet from {}, sending RST", remote_addr);
let buf = build_tcp_packet(
local_addr,
remote_addr,
0,
tcp_packet.get_sequence() + 1,
tcp::TcpFlags::RST,
None,
);
shared.tun[0].try_send(&buf).unwrap();
}
} else if (tcp_packet.get_flags() & tcp::TcpFlags::RST) == 0 {
info!("Unknown TCP packet from {}, sending RST", remote_addr);
let buf = build_tcp_packet(
local_addr,
remote_addr,
tcp_packet.get_acknowledgement(),
0,
tcp::TcpFlags::RST,
None,
);
shared.tun[0].try_send(&buf).unwrap();
}
},
tuple = tuples_purge.recv() => {
let tuple = tuple.unwrap();
tuples.remove(&tuple);
trace!("Removed cached tuple");
trace!("Removed cached tuple: {:?}", tuple);
}
}
}

View File

@ -1,45 +1,84 @@
use bytes::{Bytes, BytesMut};
use internet_checksum::Checksum;
use pnet::packet::Packet;
use pnet::packet::{ip, ipv4, tcp};
use pnet::packet::{ip, ipv4, ipv6, tcp};
use std::convert::TryInto;
use std::net::SocketAddrV4;
use std::net::{IpAddr, SocketAddr};
const IPV4_HEADER_LEN: usize = 20;
const IPV6_HEADER_LEN: usize = 40;
const TCP_HEADER_LEN: usize = 20;
pub const MAX_PACKET_LEN: usize = 1500;
pub enum IPPacket<'p> {
V4(ipv4::Ipv4Packet<'p>),
V6(ipv6::Ipv6Packet<'p>),
}
impl IPPacket<'_> {
pub fn get_source(&self) -> IpAddr {
match self {
IPPacket::V4(p) => IpAddr::V4(p.get_source()),
IPPacket::V6(p) => IpAddr::V6(p.get_source()),
}
}
pub fn get_destination(&self) -> IpAddr {
match self {
IPPacket::V4(p) => IpAddr::V4(p.get_destination()),
IPPacket::V6(p) => IpAddr::V6(p.get_destination()),
}
}
}
pub fn build_tcp_packet(
local_addr: SocketAddrV4,
remote_addr: SocketAddrV4,
local_addr: SocketAddr,
remote_addr: SocketAddr,
seq: u32,
ack: u32,
flags: u16,
flags: u8,
payload: Option<&[u8]>,
) -> Bytes {
let ip_header_len = match local_addr {
SocketAddr::V4(_) => IPV4_HEADER_LEN,
SocketAddr::V6(_) => IPV6_HEADER_LEN,
};
let wscale = (flags & tcp::TcpFlags::SYN) != 0;
let tcp_header_len = TCP_HEADER_LEN + if wscale { 4 } else { 0 }; // nop + wscale
let tcp_total_len = tcp_header_len + payload.map_or(0, |payload| payload.len());
let total_len = IPV4_HEADER_LEN + tcp_total_len;
let mut buf = BytesMut::with_capacity(total_len);
buf.resize(total_len, 0);
let total_len = ip_header_len + tcp_total_len;
let mut buf = BytesMut::zeroed(total_len);
let mut v4_buf = buf.split_to(IPV4_HEADER_LEN);
let mut ip_buf = buf.split_to(ip_header_len);
let mut tcp_buf = buf.split_to(tcp_total_len);
assert_eq!(0, buf.len());
let mut v4 = ipv4::MutableIpv4Packet::new(&mut v4_buf).unwrap();
v4.set_version(4);
v4.set_header_length(IPV4_HEADER_LEN as u8 / 4);
v4.set_next_level_protocol(ip::IpNextHeaderProtocols::Tcp);
v4.set_ttl(64);
v4.set_source(*local_addr.ip());
v4.set_destination(*remote_addr.ip());
v4.set_total_length(total_len.try_into().unwrap());
v4.set_flags(ipv4::Ipv4Flags::DontFragment);
let mut cksm = Checksum::new();
cksm.add_bytes(v4.packet());
v4.set_checksum(u16::from_be_bytes(cksm.checksum()));
match (local_addr, remote_addr) {
(SocketAddr::V4(local), SocketAddr::V4(remote)) => {
let mut v4 = ipv4::MutableIpv4Packet::new(&mut ip_buf).unwrap();
v4.set_version(4);
v4.set_header_length(IPV4_HEADER_LEN as u8 / 4);
v4.set_next_level_protocol(ip::IpNextHeaderProtocols::Tcp);
v4.set_ttl(64);
v4.set_source(*local.ip());
v4.set_destination(*remote.ip());
v4.set_total_length(total_len.try_into().unwrap());
v4.set_flags(ipv4::Ipv4Flags::DontFragment);
let mut cksm = Checksum::new();
cksm.add_bytes(v4.packet());
v4.set_checksum(u16::from_be_bytes(cksm.checksum()));
}
(SocketAddr::V6(local), SocketAddr::V6(remote)) => {
let mut v6 = ipv6::MutableIpv6Packet::new(&mut ip_buf).unwrap();
v6.set_version(6);
v6.set_payload_length(tcp_total_len.try_into().unwrap());
v6.set_next_header(ip::IpNextHeaderProtocols::Tcp);
v6.set_hop_limit(64);
v6.set_source(*local.ip());
v6.set_destination(*remote.ip());
}
_ => unreachable!(),
};
let mut tcp = tcp::MutableTcpPacket::new(&mut tcp_buf).unwrap();
tcp.set_window(0xffff);
@ -59,24 +98,55 @@ pub fn build_tcp_packet(
}
let mut cksm = Checksum::new();
cksm.add_bytes(&local_addr.ip().octets());
cksm.add_bytes(&remote_addr.ip().octets());
let ip::IpNextHeaderProtocol(tcp_protocol) = ip::IpNextHeaderProtocols::Tcp;
let mut pseudo = [0u8, tcp_protocol, 0, 0];
pseudo[2..].copy_from_slice(&(tcp_total_len as u16).to_be_bytes());
cksm.add_bytes(&pseudo);
match (local_addr, remote_addr) {
(SocketAddr::V4(local), SocketAddr::V4(remote)) => {
cksm.add_bytes(&local.ip().octets());
cksm.add_bytes(&remote.ip().octets());
let mut pseudo = [0u8, tcp_protocol, 0, 0];
pseudo[2..].copy_from_slice(&(tcp_total_len as u16).to_be_bytes());
cksm.add_bytes(&pseudo);
}
(SocketAddr::V6(local), SocketAddr::V6(remote)) => {
cksm.add_bytes(&local.ip().octets());
cksm.add_bytes(&remote.ip().octets());
let mut pseudo = [0u8, 0, 0, 0, 0, 0, 0, tcp_protocol];
pseudo[0..4].copy_from_slice(&(tcp_total_len as u32).to_be_bytes());
cksm.add_bytes(&pseudo);
}
_ => unreachable!(),
};
cksm.add_bytes(tcp.packet());
tcp.set_checksum(u16::from_be_bytes(cksm.checksum()));
v4_buf.unsplit(tcp_buf);
v4_buf.freeze()
ip_buf.unsplit(tcp_buf);
ip_buf.freeze()
}
pub fn parse_ipv4_packet(buf: &Bytes) -> (ipv4::Ipv4Packet, tcp::TcpPacket) {
let v4 = ipv4::Ipv4Packet::new(buf).unwrap();
let tcp = tcp::TcpPacket::new(&buf[IPV4_HEADER_LEN..]).unwrap();
pub fn parse_ip_packet(buf: &Bytes) -> Option<(IPPacket, tcp::TcpPacket)> {
if buf[0] >> 4 == 4 {
let v4 = ipv4::Ipv4Packet::new(buf).unwrap();
if v4.get_next_level_protocol() != ip::IpNextHeaderProtocols::Tcp {
return None;
}
(v4, tcp)
let tcp = tcp::TcpPacket::new(&buf[IPV4_HEADER_LEN..]).unwrap();
Some((IPPacket::V4(v4), tcp))
} else if buf[0] >> 4 == 6 {
let v6 = ipv6::Ipv6Packet::new(buf).unwrap();
if v6.get_next_header() != ip::IpNextHeaderProtocols::Tcp {
return None;
}
let tcp = tcp::TcpPacket::new(&buf[IPV6_HEADER_LEN..]).unwrap();
Some((IPPacket::V6(v6), tcp))
} else {
None
}
}
#[cfg(all(test, feature = "benchmark"))]

BIN
images/packet-headers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,6 +1,6 @@
[package]
name = "phantun"
version = "0.3.0"
version = "0.7.0"
edition = "2021"
authors = ["Datong Sun <dndx@idndx.com>"]
license = "MIT OR Apache-2.0"
@ -11,12 +11,14 @@ Transforms UDP stream into (fake) TCP streams that can go through
Layer 3 & Layer 4 (NAPT) firewalls/NATs.
"""
[dependencies]
clap = { version = "3.0", features = ["cargo"] }
socket2 = { version = "0.4", features = ["all"] }
fake-tcp = { path = "../fake-tcp", version = "0.2" }
tokio = { version = "1.14", features = ["full"] }
tokio-util = "0.7"
log = "0.4"
pretty_env_logger = "0.4"
tokio-tun = "0.5"
num_cpus = "1.13"
clap = { version = "4", features = ["cargo"] }
socket2 = { version = "0", features = ["all"] }
fake-tcp = { path = "../fake-tcp", version = "0" }
tokio = { version = "1", features = ["full"] }
tokio-util = "0"
log = "0"
pretty_env_logger = "0"
tokio-tun = "0"
num_cpus = "1"
neli = "0"
nix = { version = "0", features = ["net"] }

View File

@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021-2022 Datong Sun (dndx@idndx.com)
Copyright 2021-2024 Datong Sun (dndx@idndx.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-2022 Datong Sun (dndx@idndx.com)
Copyright (c) 2021-2024 Datong Sun (dndx@idndx.com)
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated

View File

@ -1,9 +1,11 @@
use clap::{crate_version, Arg, Command};
use clap::{crate_version, Arg, ArgAction, Command};
use fake_tcp::packet::MAX_PACKET_LEN;
use fake_tcp::{Socket, Stack};
use log::{debug, error, info};
use phantun::utils::new_udp_reuseport;
use phantun::utils::{assign_ipv6_address, new_udp_reuseport};
use std::collections::HashMap;
use std::fs;
use std::io;
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc;
use tokio::sync::{Notify, RwLock};
@ -14,7 +16,7 @@ use tokio_util::sync::CancellationToken;
use phantun::UDP_TTL;
#[tokio::main]
async fn main() {
async fn main() -> io::Result<()> {
pretty_env_logger::init();
let matches = Command::new("Phantun Client")
@ -27,7 +29,6 @@ async fn main() {
.required(true)
.value_name("IP:PORT")
.help("Sets the IP and port where Phantun Client listens for incoming UDP datagrams, IPv6 address need to be specified as: \"[IPv6]:PORT\"")
.takes_value(true),
)
.arg(
Arg::new("remote")
@ -35,8 +36,7 @@ async fn main() {
.long("remote")
.required(true)
.value_name("IP or HOST NAME:PORT")
.help("Sets the address or host name and port where Phantun Client connects to Phantun Server")
.takes_value(true),
.help("Sets the address or host name and port where Phantun Client connects to Phantun Server, IPv6 address need to be specified as: \"[IPv6]:PORT\"")
)
.arg(
Arg::new("tun")
@ -45,195 +45,258 @@ async fn main() {
.value_name("tunX")
.help("Sets the Tun interface name, if absent, pick the next available name")
.default_value("")
.takes_value(true),
)
.arg(
Arg::new("tun_local")
.long("tun-local")
.required(false)
.value_name("IP")
.help("Sets the Tun interface local address (O/S's end)")
.help("Sets the Tun interface IPv4 local address (O/S's end)")
.default_value("192.168.200.1")
.takes_value(true),
)
.arg(
Arg::new("tun_peer")
.long("tun-peer")
.required(false)
.value_name("IP")
.help("Sets the Tun interface destination (peer) address (Phantun Client's end). \
.help("Sets the Tun interface IPv4 destination (peer) address (Phantun Client's end). \
You will need to setup SNAT/MASQUERADE rules on your Internet facing interface \
in order for Phantun Client to connect to Phantun Server")
.default_value("192.168.200.2")
.takes_value(true),
)
.arg(
Arg::new("ipv4_only")
.long("ipv4-only")
.short('4')
.required(false)
.help("Only use IPv4 address when connecting to remote")
.action(ArgAction::SetTrue)
.conflicts_with_all(["tun_local6", "tun_peer6"]),
)
.arg(
Arg::new("tun_local6")
.long("tun-local6")
.required(false)
.value_name("IP")
.help("Sets the Tun interface IPv6 local address (O/S's end)")
.default_value("fcc8::1")
)
.arg(
Arg::new("tun_peer6")
.long("tun-peer6")
.required(false)
.value_name("IP")
.help("Sets the Tun interface IPv6 destination (peer) address (Phantun Client's end). \
You will need to setup SNAT/MASQUERADE rules on your Internet facing interface \
in order for Phantun Client to connect to Phantun Server")
.default_value("fcc8::2")
)
.arg(
Arg::new("handshake_packet")
.long("handshake-packet")
.required(false)
.value_name("PATH")
.help("Specify a file, which, after TCP handshake, its content will be sent as the \
first data packet to the server.\n\
Note: ensure this file's size does not exceed the MTU of the outgoing interface. \
The content is always sent out in a single packet and will not be further segmented")
)
.get_matches();
let local_addr: SocketAddr = matches
.value_of("local")
.get_one::<String>("local")
.unwrap()
.parse()
.expect("bad local address");
let remote_addr = tokio::net::lookup_host(matches.value_of("remote").unwrap())
let ipv4_only = matches.get_flag("ipv4_only");
let remote_addr = tokio::net::lookup_host(matches.get_one::<String>("remote").unwrap())
.await
.expect("bad remote address or host")
.find(|addr| addr.is_ipv4())
.expect("unable to resolve remote host name or no valid A record was returned");
let remote_addr = if let SocketAddr::V4(addr) = remote_addr {
addr
} else {
unreachable!();
};
.find(|addr| !ipv4_only || addr.is_ipv4())
.expect("unable to resolve remote host name");
info!("Remote address is: {}", remote_addr);
let tun_local: Ipv4Addr = matches
.value_of("tun_local")
.get_one::<String>("tun_local")
.unwrap()
.parse()
.expect("bad local address for Tun interface");
let tun_peer: Ipv4Addr = matches
.value_of("tun_peer")
.get_one::<String>("tun_peer")
.unwrap()
.parse()
.expect("bad peer address for Tun interface");
let (tun_local6, tun_peer6) = if matches.get_flag("ipv4_only") {
(None, None)
} else {
(
matches
.get_one::<String>("tun_local6")
.map(|v| v.parse().expect("bad local address for Tun interface")),
matches
.get_one::<String>("tun_peer6")
.map(|v| v.parse().expect("bad peer address for Tun interface")),
)
};
let tun_name = matches.get_one::<String>("tun").unwrap();
let handshake_packet: Option<Vec<u8>> = matches
.get_one::<String>("handshake_packet")
.map(fs::read)
.transpose()?;
let num_cpus = num_cpus::get();
info!("{} cores available", num_cpus);
let tun = TunBuilder::new()
.name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel.
.tap(false) // false (default): TUN, true: TAP.
.packet_info(false) // false: IFF_NO_PI, default is true.
.name(tun_name) // if name is empty, then it is set by kernel.
.up() // or set it up manually using `sudo ip link set <tun-name> up`.
.address(tun_local)
.destination(tun_peer)
.try_build_mq(num_cpus)
.queues(num_cpus)
.build()
.unwrap();
if remote_addr.is_ipv6() {
assign_ipv6_address(tun[0].name(), tun_local6.unwrap(), tun_peer6.unwrap());
}
info!("Created TUN device {}", tun[0].name());
let udp_sock = Arc::new(new_udp_reuseport(local_addr));
let connections = Arc::new(RwLock::new(HashMap::<SocketAddr, Arc<Socket>>::new()));
let mut stack = Stack::new(tun);
let mut stack = Stack::new(tun, tun_peer, tun_peer6);
let main_loop = tokio::spawn(async move {
let mut buf_r = [0u8; MAX_PACKET_LEN];
loop {
tokio::select! {
Ok((size, addr)) = udp_sock.recv_from(&mut buf_r) => {
// seen UDP packet to listening socket, this means:
// 1. It is a new UDP connection, or
// 2. It is some extra packets not filtered by more specific
// connected UDP socket yet
if let Some(sock) = connections.read().await.get(&addr) {
sock.send(&buf_r[..size]).await;
continue;
}
let (size, addr) = udp_sock.recv_from(&mut buf_r).await?;
// seen UDP packet to listening socket, this means:
// 1. It is a new UDP connection, or
// 2. It is some extra packets not filtered by more specific
// connected UDP socket yet
if let Some(sock) = connections.read().await.get(&addr) {
sock.send(&buf_r[..size]).await;
continue;
}
info!("New UDP client from {}", addr);
let sock = stack.connect(remote_addr).await;
if sock.is_none() {
error!("Unable to connect to remote {}", remote_addr);
continue;
}
info!("New UDP client from {}", addr);
let sock = stack.connect(remote_addr).await;
if sock.is_none() {
error!("Unable to connect to remote {}", remote_addr);
continue;
}
let sock = Arc::new(sock.unwrap());
// send first packet
let res = sock.send(&buf_r[..size]).await;
if res.is_none() {
continue;
}
let sock = Arc::new(sock.unwrap());
if let Some(ref p) = handshake_packet {
if sock.send(p).await.is_none() {
error!("Failed to send handshake packet to remote, closing connection.");
continue;
}
assert!(connections.write().await.insert(addr, sock.clone()).is_none());
debug!("inserted fake TCP socket into connection table");
debug!("Sent handshake packet to: {}", sock);
}
// spawn "fastpath" UDP socket and task, this will offload main task
// from forwarding UDP packets
// send first packet
if sock.send(&buf_r[..size]).await.is_none() {
continue;
}
let packet_received = Arc::new(Notify::new());
let quit = CancellationToken::new();
assert!(connections
.write()
.await
.insert(addr, sock.clone())
.is_none());
debug!("inserted fake TCP socket into connection table");
for i in 0..num_cpus {
let sock = sock.clone();
let quit = quit.child_token();
let packet_received = packet_received.clone();
// spawn "fastpath" UDP socket and task, this will offload main task
// from forwarding UDP packets
tokio::spawn(async move {
let mut buf_udp = [0u8; MAX_PACKET_LEN];
let mut buf_tcp = [0u8; MAX_PACKET_LEN];
let udp_sock = new_udp_reuseport(local_addr);
udp_sock.connect(addr).await.unwrap();
let packet_received = Arc::new(Notify::new());
let quit = CancellationToken::new();
loop {
tokio::select! {
Ok(size) = udp_sock.recv(&mut buf_udp) => {
if sock.send(&buf_udp[..size]).await.is_none() {
debug!("removed fake TCP socket from connections table");
quit.cancel();
return;
}
for i in 0..num_cpus {
let sock = sock.clone();
let quit = quit.clone();
let packet_received = packet_received.clone();
packet_received.notify_one();
},
res = sock.recv(&mut buf_tcp) => {
match res {
Some(size) => {
if size > 0 {
if let Err(e) = udp_sock.send(&buf_tcp[..size]).await {
error!("Unable to send UDP packet to {}: {}, closing connection", e, addr);
quit.cancel();
return;
}
}
},
None => {
debug!("removed fake TCP socket from connections table");
quit.cancel();
return;
},
}
tokio::spawn(async move {
let mut buf_udp = [0u8; MAX_PACKET_LEN];
let mut buf_tcp = [0u8; MAX_PACKET_LEN];
let udp_sock = new_udp_reuseport(local_addr);
udp_sock.connect(addr).await.unwrap();
packet_received.notify_one();
},
_ = quit.cancelled() => {
debug!("worker {} terminated", i);
return;
},
};
}
});
}
let connections = connections.clone();
tokio::spawn(async move {
loop {
let read_timeout = time::sleep(UDP_TTL);
let packet_received_fut = packet_received.notified();
tokio::select! {
_ = read_timeout => {
info!("No traffic seen in the last {:?}, closing connection", UDP_TTL);
connections.write().await.remove(&addr);
loop {
tokio::select! {
Ok(size) = udp_sock.recv(&mut buf_udp) => {
if sock.send(&buf_udp[..size]).await.is_none() {
debug!("removed fake TCP socket from connections table");
quit.cancel();
return;
},
_ = quit.cancelled() => {
connections.write().await.remove(&addr);
debug!("removed fake TCP socket from connections table");
return;
},
_ = packet_received_fut => {},
}
}
});
},
}
packet_received.notify_one();
},
res = sock.recv(&mut buf_tcp) => {
match res {
Some(size) => {
if size > 0 {
if let Err(e) = udp_sock.send(&buf_tcp[..size]).await {
error!("Unable to send UDP packet to {}: {}, closing connection", e, addr);
quit.cancel();
return;
}
}
},
None => {
debug!("removed fake TCP socket from connections table");
quit.cancel();
return;
},
}
packet_received.notify_one();
},
_ = quit.cancelled() => {
debug!("worker {} terminated", i);
return;
},
};
}
});
}
let connections = connections.clone();
tokio::spawn(async move {
loop {
let read_timeout = time::sleep(UDP_TTL);
let packet_received_fut = packet_received.notified();
tokio::select! {
_ = read_timeout => {
info!("No traffic seen in the last {:?}, closing connection", UDP_TTL);
connections.write().await.remove(&addr);
debug!("removed fake TCP socket from connections table");
quit.cancel();
return;
},
_ = quit.cancelled() => {
connections.write().await.remove(&addr);
debug!("removed fake TCP socket from connections table");
return;
},
_ = packet_received_fut => {},
}
}
});
}
});
tokio::join!(main_loop).0.unwrap();
tokio::join!(main_loop).0.unwrap()
}

View File

@ -1,8 +1,10 @@
use clap::{crate_version, Arg, Command};
use clap::{crate_version, Arg, ArgAction, Command};
use fake_tcp::packet::MAX_PACKET_LEN;
use fake_tcp::Stack;
use log::{debug, error, info};
use phantun::utils::new_udp_reuseport;
use phantun::utils::{assign_ipv6_address, new_udp_reuseport};
use std::fs;
use std::io;
use std::net::Ipv4Addr;
use std::sync::Arc;
use tokio::net::UdpSocket;
@ -14,7 +16,7 @@ use tokio_util::sync::CancellationToken;
use phantun::UDP_TTL;
#[tokio::main]
async fn main() {
async fn main() -> io::Result<()> {
pretty_env_logger::init();
let matches = Command::new("Phantun Server")
@ -27,7 +29,6 @@ async fn main() {
.required(true)
.value_name("PORT")
.help("Sets the port where Phantun Server listens for incoming Phantun Client TCP connections")
.takes_value(true),
)
.arg(
Arg::new("remote")
@ -36,7 +37,6 @@ async fn main() {
.required(true)
.value_name("IP or HOST NAME:PORT")
.help("Sets the address or host name and port where Phantun Server forwards UDP packets to, IPv6 address need to be specified as: \"[IPv6]:PORT\"")
.takes_value(true),
)
.arg(
Arg::new("tun")
@ -45,7 +45,6 @@ async fn main() {
.value_name("tunX")
.help("Sets the Tun interface name, if absent, pick the next available name")
.default_value("")
.takes_value(true),
)
.arg(
Arg::new("tun_local")
@ -54,7 +53,6 @@ async fn main() {
.value_name("IP")
.help("Sets the Tun interface local address (O/S's end)")
.default_value("192.168.201.1")
.takes_value(true),
)
.arg(
Arg::new("tun_peer")
@ -65,50 +63,110 @@ async fn main() {
You will need to setup DNAT rules to this address in order for Phantun Server \
to accept TCP traffic from Phantun Client")
.default_value("192.168.201.2")
.takes_value(true),
)
.arg(
Arg::new("ipv4_only")
.long("ipv4-only")
.short('4')
.required(false)
.help("Do not assign IPv6 addresses to Tun interface")
.action(ArgAction::SetTrue)
.conflicts_with_all(["tun_local6", "tun_peer6"]),
)
.arg(
Arg::new("tun_local6")
.long("tun-local6")
.required(false)
.value_name("IP")
.help("Sets the Tun interface IPv6 local address (O/S's end)")
.default_value("fcc9::1")
)
.arg(
Arg::new("tun_peer6")
.long("tun-peer6")
.required(false)
.value_name("IP")
.help("Sets the Tun interface IPv6 destination (peer) address (Phantun Client's end). \
You will need to setup SNAT/MASQUERADE rules on your Internet facing interface \
in order for Phantun Client to connect to Phantun Server")
.default_value("fcc9::2")
)
.arg(
Arg::new("handshake_packet")
.long("handshake-packet")
.required(false)
.value_name("PATH")
.help("Specify a file, which, after TCP handshake, its content will be sent as the \
first data packet to the client.\n\
Note: ensure this file's size does not exceed the MTU of the outgoing interface. \
The content is always sent out in a single packet and will not be further segmented")
)
.get_matches();
let local_port: u16 = matches
.value_of("local")
.get_one::<String>("local")
.unwrap()
.parse()
.expect("bad local port");
let remote_addr = tokio::net::lookup_host(matches.value_of("remote").unwrap())
let remote_addr = tokio::net::lookup_host(matches.get_one::<String>("remote").unwrap())
.await
.expect("bad remote address or host")
.next()
.expect("unable to resolve remote host name");
info!("Remote address is: {}", remote_addr);
let tun_local: Ipv4Addr = matches
.value_of("tun_local")
.get_one::<String>("tun_local")
.unwrap()
.parse()
.expect("bad local address for Tun interface");
let tun_peer: Ipv4Addr = matches
.value_of("tun_peer")
.get_one::<String>("tun_peer")
.unwrap()
.parse()
.expect("bad peer address for Tun interface");
let (tun_local6, tun_peer6) = if matches.get_flag("ipv4_only") {
(None, None)
} else {
(
matches
.get_one::<String>("tun_local6")
.map(|v| v.parse().expect("bad local address for Tun interface")),
matches
.get_one::<String>("tun_peer6")
.map(|v| v.parse().expect("bad peer address for Tun interface")),
)
};
let tun_name = matches.get_one::<String>("tun").unwrap();
let handshake_packet: Option<Vec<u8>> = matches
.get_one::<String>("handshake_packet")
.map(fs::read)
.transpose()?;
let num_cpus = num_cpus::get();
info!("{} cores available", num_cpus);
let tun = TunBuilder::new()
.name(matches.value_of("tun").unwrap()) // if name is empty, then it is set by kernel.
.tap(false) // false (default): TUN, true: TAP.
.packet_info(false) // false: IFF_NO_PI, default is true.
.name(tun_name) // if name is empty, then it is set by kernel.
.up() // or set it up manually using `sudo ip link set <tun-name> up`.
.address(tun_local)
.destination(tun_peer)
.try_build_mq(num_cpus)
.queues(num_cpus)
.build()
.unwrap();
if let (Some(tun_local6), Some(tun_peer6)) = (tun_local6, tun_peer6) {
assign_ipv6_address(tun[0].name(), tun_local6, tun_peer6);
}
info!("Created TUN device {}", tun[0].name());
//thread::sleep(time::Duration::from_secs(5));
let mut stack = Stack::new(tun);
let mut stack = Stack::new(tun, tun_local, tun_local6);
stack.listen(local_port);
info!("Listening on {}", local_port);
@ -119,6 +177,14 @@ async fn main() {
loop {
let sock = Arc::new(stack.accept().await);
info!("New connection: {}", sock);
if let Some(ref p) = handshake_packet {
if sock.send(p).await.is_none() {
error!("Failed to send handshake packet to remote, closing connection.");
continue;
}
debug!("Sent handshake packet to: {}", sock);
}
let packet_received = Arc::new(Notify::new());
let quit = CancellationToken::new();
@ -127,14 +193,13 @@ async fn main() {
} else {
"[::]:0"
})
.await
.unwrap();
let local_addr = udp_sock.local_addr().unwrap();
.await?;
let local_addr = udp_sock.local_addr()?;
drop(udp_sock);
for i in 0..num_cpus {
let sock = sock.clone();
let quit = quit.child_token();
let quit = quit.clone();
let packet_received = packet_received.clone();
let udp_sock = new_udp_reuseport(local_addr);
@ -198,5 +263,5 @@ async fn main() {
}
});
tokio::join!(main_loop).0.unwrap();
tokio::join!(main_loop).0.unwrap()
}

View File

@ -1,4 +1,15 @@
use std::net::SocketAddr;
use neli::{
consts::{
nl::{NlmF, NlmFFlags},
rtnl::{Ifa, IfaFFlags, RtAddrFamily, Rtm},
socket::NlFamily,
},
nl::{NlPayload, Nlmsghdr},
rtnl::{Ifaddrmsg, Rtattr},
socket::NlSocketHandle,
types::RtBuffer,
};
use std::net::{Ipv6Addr, SocketAddr};
use tokio::net::UdpSocket;
pub fn new_udp_reuseport(local_addr: SocketAddr) -> UdpSocket {
@ -20,3 +31,30 @@ pub fn new_udp_reuseport(local_addr: SocketAddr) -> UdpSocket {
let udp_sock: std::net::UdpSocket = udp_sock.into();
udp_sock.try_into().unwrap()
}
pub fn assign_ipv6_address(device_name: &str, local: Ipv6Addr, peer: Ipv6Addr) {
let index = nix::net::if_::if_nametoindex(device_name).unwrap();
let mut rtnl = NlSocketHandle::connect(NlFamily::Route, None, &[]).unwrap();
let mut rtattrs = RtBuffer::new();
rtattrs.push(Rtattr::new(None, Ifa::Local, &local.octets()[..]).unwrap());
rtattrs.push(Rtattr::new(None, Ifa::Address, &peer.octets()[..]).unwrap());
let ifaddrmsg = Ifaddrmsg {
ifa_family: RtAddrFamily::Inet6,
ifa_prefixlen: 128,
ifa_flags: IfaFFlags::empty(),
ifa_scope: 0,
ifa_index: index as i32,
rtattrs,
};
let nl_header = Nlmsghdr::new(
None,
Rtm::Newaddr,
NlmFFlags::new(&[NlmF::Request]),
None,
None,
NlPayload::Payload(ifaddrmsg),
);
rtnl.send(nl_header).unwrap();
}