Compare commits

...

93 commits

Author SHA1 Message Date
3ca5ae2181
Merge pull request #85 from aramperes/dependabot/cargo/async-trait-0.1.87
build(deps): bump async-trait from 0.1.83 to 0.1.87
2025-03-10 23:32:19 -04:00
ac83ddbd4d
Merge pull request #86 from aramperes/dependabot/cargo/anyhow-1.0.97
build(deps): bump anyhow from 1.0.94 to 1.0.97
2025-03-10 23:32:12 -04:00
17f424140d
Merge pull request #87 from aramperes/dependabot/cargo/tokio-1.44.0
build(deps): bump tokio from 1.42.0 to 1.44.0
2025-03-10 23:31:55 -04:00
dependabot[bot]
8030ca1a2d
build(deps): bump tokio from 1.42.0 to 1.44.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.42.0 to 1.44.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.42.0...tokio-1.44.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 16:58:22 +00:00
dependabot[bot]
7eddf3f17f
build(deps): bump anyhow from 1.0.94 to 1.0.97
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.94 to 1.0.97.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.94...1.0.97)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 16:47:17 +00:00
dependabot[bot]
bcfa43702a
build(deps): bump async-trait from 0.1.83 to 0.1.87
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.83 to 0.1.87.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.83...0.1.87)

---
updated-dependencies:
- dependency-name: async-trait
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 16:47:00 +00:00
d0fcab38c3 docs: update README and LICENSE 2025-01-25 21:45:38 -05:00
c83c9ec500
Merge pull request #67 from aramperes/dependabot/cargo/priority-queue-2.1.1 2024-12-11 19:56:22 -05:00
caadd415cd
Merge pull request #68 from aramperes/dependabot/cargo/pretty_env_logger-0.5.0 2024-12-11 19:55:18 -05:00
3a89f2877d
Merge pull request #69 from aramperes/dependabot/cargo/anyhow-1.0.94 2024-12-11 19:54:56 -05:00
341849762c
Merge pull request #70 from aramperes/dependabot/cargo/tokio-1.42.0 2024-12-11 19:54:32 -05:00
dependabot[bot]
57e6ddc74c
Bump tokio from 1.41.1 to 1.42.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.41.1 to 1.42.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.41.1...tokio-1.42.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 17:28:44 +00:00
dependabot[bot]
08d99b9d22
Bump anyhow from 1.0.93 to 1.0.94
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.93 to 1.0.94.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.93...1.0.94)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 17:28:37 +00:00
dependabot[bot]
2661a2d29f
Bump pretty_env_logger from 0.4.0 to 0.5.0
Bumps [pretty_env_logger](https://github.com/seanmonstar/pretty-env-logger) from 0.4.0 to 0.5.0.
- [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
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:38:35 +00:00
dependabot[bot]
6722237902
Bump priority-queue from 1.4.0 to 2.1.1
Bumps [priority-queue](https://github.com/garro95/priority-queue) from 1.4.0 to 2.1.1.
- [Release notes](https://github.com/garro95/priority-queue/releases)
- [Commits](https://github.com/garro95/priority-queue/commits/2.1.1)

---
updated-dependencies:
- dependency-name: priority-queue
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:38:32 +00:00
83ef02c695
Create dependabot.yml 2024-12-01 22:37:33 -05:00
89c3b59610 fix: typo 2024-12-01 16:03:32 -05:00
c6544cfe05 fix: assume path is target 2024-12-01 16:01:27 -05:00
f75909fd8f fix: expected output location in release 2024-12-01 15:56:37 -05:00
52d1d589ac use cross to cross-build 2024-12-01 15:45:25 -05:00
eb9c0be437 force cargo build with target 2024-12-01 15:39:16 -05:00
d307a11819 release: v0.3.10 2024-12-01 15:33:13 -05:00
c4c52babae
Merge pull request #65 from aramperes/smoltcp-0.12 2024-12-01 15:31:03 -05:00
6b2f6148c6 chore: add linux-aarch64 build 2024-12-01 15:29:59 -05:00
991eef0311 chore: update MSRV to 1.80.0 2024-12-01 15:27:12 -05:00
0e93a6435a chore: udpate to smoltcp 0.12 2024-12-01 15:22:37 -05:00
ca3590a4c0 chore: bump minor dependencies 2024-12-01 15:13:46 -05:00
784ab97c8b release: v0.3.9; add macos-aarch64 build 2024-12-01 12:41:23 -05:00
f3661c0a2c fix docker build 2024-12-01 12:33:56 -05:00
4fa8304799 bump MSRV to 1.78.0 2024-12-01 12:30:13 -05:00
1f3d9f035f release: v0.3.8 2024-12-01 12:28:13 -05:00
06049161ab bump MSRV to 1.74.0 2024-12-01 12:27:41 -05:00
e26cca089f
Merge pull request #64 from aramperes/fix/63 2024-12-01 12:08:24 -05:00
88ce124544 formatting 2024-12-01 12:03:51 -05:00
9ccd2e19f6 increase default smoltcp interface limit and add to README 2024-12-01 12:03:41 -05:00
c86784ed70 log a better error regarding smoltcp max interface limit 2024-12-01 11:33:53 -05:00
e25c88410e
Merge pull request #61 from PeterDaveHelloKitchen/OptimizeDockerfile 2024-04-07 20:38:08 -04:00
Peter Dave Hello
2b6d21572e
Optimize apt-get commands to reduce image size in Dockerfile
This commit improves the Dockerfile by consolidating apt-get update and apt-get install commands into a single RUN statement and adding cleanup steps for the apt cache.
2024-04-07 01:32:37 +08:00
56c950d159 Use bail when possible 2023-12-24 15:23:12 -05:00
ce40f85efa Cleanup usage of anyhow with_context 2023-12-24 15:06:22 -05:00
3ccd000ea8 Minor dependency updates 2023-12-24 14:58:51 -05:00
5fd28164b5
Merge pull request #60 from aramperes/patch/boringtun-0.6 2023-12-24 14:45:45 -05:00
1d703facc0 Implement locking of Tunn in WireGuardTunnel 2023-12-24 14:42:34 -05:00
e23cfc3e7e Update to new x25519 primitives 2023-12-24 11:52:07 -05:00
0931ed496a update boringtun to 0.6.0 2023-12-24 11:51:28 -05:00
91e6c79832
Merge pull request #59 from aramperes/patch/smoltcp-0.11 2023-12-24 11:31:00 -05:00
72ab679142 update to smoltcp 0.11 2023-12-24 11:28:15 -05:00
10b88ccc60 cleanup: SockSet can be owned by static
ref: https://github.com/smoltcp-rs/smoltcp/pull/813
2023-12-24 11:23:58 -05:00
83beb48b07 release: v0.3.7 2023-12-23 21:59:55 -05:00
21fe78f540 Add docs/example for SOCKS proxy 2023-12-23 21:44:25 -05:00
c3b752678e
Merge pull request #58 from aramperes/patch/smoltcp-0.10 2023-12-23 21:23:41 -05:00
32f189e53a Revert virtual port for server socket 2023-12-23 21:01:35 -05:00
488a0e0807 remove AnyIP; fix IPv6 virtual addresses 2023-12-23 21:01:00 -05:00
ssrlive
38fc217a29 smoltcp version 0.10 applied 2023-12-23 20:22:01 -05:00
ae15b4203c release: v0.3.6 2023-12-21 15:34:08 -05:00
992e55bf91
Merge pull request #55 from lu-zero/update-clap 2023-12-21 15:31:27 -05:00
a100f90a92 chore: update MSRV to 1.70.0 2023-12-21 15:29:43 -05:00
Luca Barbato
1613d2bb5c Bump clap version 2023-12-21 15:24:44 -05:00
Luca Barbato
29fb98f02b Update deps 2023-12-21 15:24:44 -05:00
767b83d241
Merge pull request #57 from aramperes/chore/update-docker-rust-version 2023-12-21 15:21:58 -05:00
731218d943 Fix new clippy warnings 2023-12-21 15:16:57 -05:00
7200cc07e7 chore: update MSRV to 1.65.0 2023-12-20 17:31:39 -05:00
Marco Nalon
1997ae7ea8 chore: update Dockerfile rust version 1.63.0 -> 1.65.0 2023-12-20 16:59:27 +01:00
9f53198f17 Remove $ from README examples 2023-10-02 19:55:24 -04:00
998d1cfc8d Add maintenance disclaimer 2023-10-02 19:51:18 -04:00
0a06df59f9 Update copyright year 2023-10-02 18:17:52 -04:00
61da97f4aa Update release action to latest Ubuntu 2023-10-02 18:05:30 -04:00
c5e803192f Disable macos package install 2023-10-02 17:57:15 -04:00
2896a4fcdb Update dependencies and bump MSRV to 1.63 2023-10-02 17:55:05 -04:00
07e895c770 release: v0.3.5 2023-10-02 17:37:18 -04:00
c0d0a5cb02
Merge pull request #51 from george-hopkins/psk 2023-10-02 17:29:15 -04:00
1333ea8a7c Rename option to --preshared-key and add to README 2023-10-02 17:21:32 -04:00
6f143280d1 Pin older version of base64 for now 2023-10-02 17:07:37 -04:00
George Hopkins
653c314409 Support pre-shared key 2023-10-02 16:24:37 +02:00
43a20ef6b3 Update dependencies 2023-01-12 02:53:56 -05:00
4f935c5a2d reorder dep 2023-01-12 02:38:36 -05:00
5dc04d9614
Merge pull request #49 from aramperes/bytes 2023-01-12 01:46:41 -05:00
fa634a08dc Fix a clippy warning 2023-01-12 01:43:32 -05:00
76b6a6e346 Use bytes 2023-01-12 01:40:04 -05:00
e62b7d30fe release: v0.3.4 2022-09-25 17:29:04 -04:00
0553fce5c6 chore: bump msrv to 1.57 2022-09-25 17:24:03 -04:00
6c64531940 chore: update dependencies 2022-09-25 17:19:16 -04:00
77981371fc
Merge pull request #45 from TitanNano/issues/44 2022-09-25 16:55:24 -04:00
Jovan Gerodetti
fbc76e3fb0 Handle WireGuardError::ConnectionExpired #44 2022-09-25 22:34:03 +02:00
85195d8aba
Merge pull request #41 from samhug/stackoverflow
Fix stack overflow on windows
2022-08-20 09:17:14 -04:00
eb9b8ff15d
Merge pull request #42 from kianmeng/fix-typos-and-markdowns 2022-08-12 09:53:09 -04:00
Kian-Meng Ang
074e1b430c Fix typos and markdowns
Found via these commands:

    codespell -L crate
    markdownlint -f README.md --disable MD013 MD033 MD041
2022-08-12 18:45:14 +08:00
Sam Hug
cea343c2c9 heap alloc WireGuardTunnel::consume_task() future 2022-08-11 15:32:24 -07:00
aef90a5c0c Add --endpoint-bind-addr to README 2022-07-18 20:41:48 -04:00
b78cab58ee release: v0.3.3 2022-06-25 15:05:10 -04:00
8cee210ccb Expose boringtun x25519 primitives 2022-06-25 14:38:08 -04:00
96be421495 Increase MSRV to 1.56.1 2022-06-25 14:03:18 -04:00
c09a541788 Update dependencies 2022-06-25 13:55:26 -04:00
22 changed files with 1358 additions and 648 deletions

4
.cargo/config.toml Normal file
View file

@ -0,0 +1,4 @@
[env]
# Each interface needs 1 IP allocated to the WireGuard peer IP.
# "8" = 7 tunnels per protocol.
SMOLTCP_IFACE_MAX_ADDR_COUNT = "8"

View file

@ -1,6 +1,6 @@
#!/bin/sh
brew install asciidoctor
# brew install asciidoctor
brew install openssl@1.1
cp /usr/local/opt/openssl@1.1/lib/pkgconfig/*.pc /usr/local/lib/pkgconfig/
# brew install openssl@1.1
# cp /usr/local/opt/openssl@1.1/lib/pkgconfig/*.pc /usr/local/lib/pkgconfig/

10
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,10 @@
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
rebase-strategy: "disabled"

View file

@ -10,7 +10,7 @@ jobs:
matrix:
rust:
- stable
- 1.56.0
- 1.80.0
steps:
- name: Checkout sources
uses: actions/checkout@v2
@ -39,7 +39,7 @@ jobs:
matrix:
rust:
- stable
- 1.56.0
- 1.80.0
steps:
- name: Checkout sources
uses: actions/checkout@v2

View file

@ -61,7 +61,7 @@ jobs:
run: echo "${{ env.VERSION }}" > artifacts/release-version
- name: Upload artifacts
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: artifacts
path: artifacts
@ -75,20 +75,28 @@ jobs:
RUST_BACKTRACE: 1
strategy:
matrix:
build: [ linux-amd64, macos-intel, windows ]
build: [ linux-amd64, linux-aarch64, macos-aarch64, windows ]
include:
- build: linux-amd64
os: ubuntu-18.04
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-musl
- build: macos-intel
cross: true
- build: linux-aarch64
os: ubuntu-latest
rust: stable
target: aarch64-unknown-linux-musl
cross: true
- build: macos-aarch64
os: macos-latest
rust: stable
target: x86_64-apple-darwin
target: aarch64-apple-darwin
cross: false
- build: windows
os: windows-2019
rust: stable
target: x86_64-pc-windows-msvc
cross: false
steps:
- name: Checkout repository
@ -97,7 +105,7 @@ jobs:
fetch-depth: 1
- name: Install packages (Ubuntu)
if: matrix.os == 'ubuntu-18.04'
if: matrix.os == 'ubuntu-latest'
run: |
.github/ci/ubuntu-install-packages
- name: Install packages (macOS)
@ -113,7 +121,7 @@ jobs:
target: ${{ matrix.target }}
- name: Get release download URL
uses: actions/download-artifact@v1
uses: actions/download-artifact@v4
with:
name: artifacts
path: artifacts
@ -126,17 +134,24 @@ jobs:
echo "release upload url: $release_upload_url"
- name: Build onetun binary
run: cargo build --release
shell: bash
run: |
if [ "${{ matrix.cross }}" = "true" ]; then
cargo install cross
cross build --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
- name: Prepare onetun binary
shell: bash
run: |
mkdir -p ci/assets
if [ "${{ matrix.build }}" = "windows" ]; then
cp "target/release/onetun.exe" "ci/assets/onetun.exe"
cp "target/${{ matrix.target }}/release/onetun.exe" "ci/assets/onetun.exe"
echo "ASSET=onetun.exe" >> $GITHUB_ENV
else
cp "target/release/onetun" "ci/assets/onetun-${{ matrix.build }}"
cp "target/${{ matrix.target }}/release/onetun" "ci/assets/onetun-${{ matrix.build }}"
echo "ASSET=onetun-${{ matrix.build }}" >> $GITHUB_ENV
fi

1
.gitignore vendored
View file

@ -3,3 +3,4 @@
.envrc
*.log
*.pcap
.DS_Store

1069
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "onetun"
version = "0.3.2"
version = "0.3.10"
edition = "2021"
license = "MIT"
description = "A cross-platform, user-space WireGuard port-forwarder that requires no system network configurations."
@ -11,20 +11,34 @@ repository = "https://github.com/aramperes/onetun"
[dependencies]
# Required dependencies (bin and lib)
boringtun = { version = "0.4.0", default-features = false }
boringtun = { version = "0.6.0", default-features = false }
log = "0.4"
anyhow = "1"
tokio = { version = "1", features = [ "rt", "sync", "io-util", "net", "time", "fs", "macros" ] }
futures = "0.3.17"
rand = "0.8.4"
futures = "0.3"
rand = "0.8"
nom = "7"
async-trait = "0.1.51"
priority-queue = "1.2.0"
smoltcp = { version = "0.8.0", default-features = false, features = ["std", "log", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-udp", "socket-tcp"] }
async-trait = "0.1"
priority-queue = "2.1"
smoltcp = { version = "0.12", default-features = false, features = [
"std",
"log",
"medium-ip",
"proto-ipv4",
"proto-ipv6",
"socket-udp",
"socket-tcp",
] }
bytes = "1"
base64 = "0.13"
# forward boringtuns tracing events to log
tracing = { version = "0.1", default-features = false, features = ["log"] }
# bin-only dependencies
clap = { version = "2.33", default-features = false, features = ["suggestions"], optional = true }
pretty_env_logger = { version = "0.4", optional = true }
clap = { version = "4.4.11", default-features = false, features = ["suggestions", "std", "env", "help", "wrap_help"], optional = true }
pretty_env_logger = { version = "0.5", optional = true }
async-recursion = "1.0"
[features]
pcap = []

View file

@ -1,4 +1,4 @@
FROM rust:1.56 as cargo-build
FROM rust:1.82.0 as cargo-build
WORKDIR /usr/src/onetun
COPY Cargo.toml Cargo.toml
@ -15,8 +15,9 @@ COPY . .
RUN cargo build --release
FROM debian:11-slim
RUN apt-get update
RUN apt-get install dumb-init -y
RUN apt-get update \
&& apt-get install dumb-init -y \
&& rm -rf /var/lib/apt/lists/*
COPY --from=cargo-build /usr/src/onetun/target/release/onetun /usr/local/bin/onetun

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-2022 Aram Peres
Copyright (c) 2025 Aram Peres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

127
README.md
View file

@ -21,28 +21,28 @@ For example,
## Download
onetun is available to install from [crates.io](https://crates.io/crates/onetun) with Rust ≥1.56:
onetun is available to install from [crates.io](https://crates.io/crates/onetun) with Rust ≥1.80.0:
```shell
$ cargo install onetun
cargo install onetun
```
You can also download the binary for Windows, macOS (Intel), and Linux (amd64) from
You can also download the binary for Windows, macOS (Apple Silicon), and Linux (amd64, arm64) from
the [Releases](https://github.com/aramperes/onetun/releases) page.
You can also run onetun using [Docker](https://hub.docker.com/r/aramperes/onetun):
```shell
$ docker run --rm --name onetun --user 1000 -p 8080:8080 aramperes/onetun \
docker run --rm --name onetun --user 1000 -p 8080:8080 aramperes/onetun \
0.0.0.0:8080:192.168.4.2:8080 [...options...]
```
You can also build onetun locally, using Rust ≥1.56:
You can also build onetun locally, using Rust ≥1.80.0:
```shell
$ git clone https://github.com/aramperes/onetun && cd onetun
$ cargo build --release
$ ./target/release/onetun
git clone https://github.com/aramperes/onetun && cd onetun
cargo build --release
./target/release/onetun
```
## Usage
@ -54,7 +54,7 @@ access, or install any WireGuard tool on your local system for it to work.
The only prerequisite is to register a peer IP and public key on the remote WireGuard endpoint; those are necessary for
the WireGuard endpoint to trust the onetun peer and for packets to be routed.
```
```shell
onetun [src_host:]<src_port>:<dst_host>:<dst_port>[:TCP,UDP,...] [...] \
--endpoint-addr <public WireGuard endpoint address> \
--endpoint-public-key <the public key of the peer on the endpoint> \
@ -70,7 +70,7 @@ onetun [src_host:]<src_port>:<dst_host>:<dst_port>[:TCP,UDP,...] [...] \
Suppose your WireGuard endpoint has the following configuration, and is accessible from `140.30.3.182:51820`:
```
```shell
# /etc/wireguard/wg0.conf
[Interface]
@ -103,14 +103,14 @@ onetun 127.0.0.1:8080:192.168.4.2:8080 \
You'll then see this log:
```
```shell
INFO onetun > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
```
Which means you can now access the port locally!
```
$ curl 127.0.0.1:8080
```shell
curl 127.0.0.1:8080
Hello world!
```
@ -118,24 +118,32 @@ Hello world!
**onetun** supports running multiple tunnels in parallel. For example:
```
$ onetun 127.0.0.1:8080:192.168.4.2:8080 127.0.0.1:8081:192.168.4.4:8081
```shell
onetun 127.0.0.1:8080:192.168.4.2:8080 127.0.0.1:8081:192.168.4.4:8081
INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8081]->[192.168.4.4:8081] (via [140.30.3.182:51820] as peer 192.168.4.3)
```
... would open TCP ports 8080 and 8081 locally, which forward to their respective ports on the different peers.
#### Maximum number of tunnels
`smoltcp` imposes a compile-time limit on the number of IP addresses assigned to an interface. **onetun** increases
the default value to support most use-cases. In effect, the default limit on the number of **onetun** peers
is **7 per protocol** (TCP and UDP).
Should you need more unique IP addresses to forward ports to, you can increase the limit in `.cargo/config.toml` and recompile **onetun**.
### UDP Support
**onetun** supports UDP forwarding. You can add `:UDP` at the end of the port-forward configuration, or `UDP,TCP` to support
both protocols on the same port (note that this opens 2 separate tunnels, just on the same port)
```
$ onetun 127.0.0.1:8080:192.168.4.2:8080:UDP
```shell
onetun 127.0.0.1:8080:192.168.4.2:8080:UDP
INFO onetun::tunnel > Tunneling UDP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
$ onetun 127.0.0.1:8080:192.168.4.2:8080:UDP,TCP
onetun 127.0.0.1:8080:192.168.4.2:8080:UDP,TCP
INFO onetun::tunnel > Tunneling UDP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
```
@ -147,16 +155,16 @@ it in any production capacity.
**onetun** supports both IPv4 and IPv6. In fact, you can use onetun to forward some IP version to another, e.g. 6-to-4:
```
$ onetun [::1]:8080:192.168.4.2:8080
```shell
onetun [::1]:8080:192.168.4.2:8080
INFO onetun::tunnel > Tunneling TCP [[::1]:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
```
Note that each tunnel can only support one "source" IP version and one "destination" IP version. If you want to support
both IPv4 and IPv6 on the same port, you should create a second port-forward:
```
$ onetun [::1]:8080:192.168.4.2:8080 127.0.0.1:8080:192.168.4.2:8080
```shell
onetun [::1]:8080:192.168.4.2:8080 127.0.0.1:8080:192.168.4.2:8080
INFO onetun::tunnel > Tunneling TCP [[::1]:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
```
@ -166,16 +174,33 @@ INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [
For debugging purposes, you can enable the capture of IP packets sent between onetun and the WireGuard peer.
The output is a libpcap capture file that can be viewed with Wireshark.
```
$ onetun --pcap wg.pcap 127.0.0.1:8080:192.168.4.2:8080
```shell
onetun --pcap wg.pcap 127.0.0.1:8080:192.168.4.2:8080
INFO onetun::pcap > Capturing WireGuard IP packets to wg.pcap
INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3)
```
To capture packets sent to and from the onetun local port, you must use an external tool like `tcpdump` with root access:
```shell
sudo tcpdump -i lo -w local.pcap 'dst 127.0.0.1 && port 8080'
```
$ sudo tcpdump -i lo -w local.pcap 'dst 127.0.0.1 && port 8080'
### WireGuard Options
By default, onetun will create the UDP socket to communicate with the WireGuard endpoint on all interfaces and on a dynamic port,
i.e. `0.0.0.0:0` for IPv4 endpoints, or `[::]:0` for IPv6.
You can bind to a static address instead using `--endpoint-bind-addr`:
```shell
onetun --endpoint-bind-addr 0.0.0.0:51820 --endpoint-addr 140.30.3.182:51820 [...]
```
The security of the WireGuard connection can be further enhanced with a **pre-shared key** (PSK). You can generate such a key with the `wg genpsk` command, and provide it using `--preshared-key`.
The peer must also have this key configured using the `PresharedKey` option.
```shell
onetun --preshared-key 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' [...]
```
## Architecture
@ -242,6 +267,56 @@ if the least recently used port hasn't been used for a certain amount of time. I
All in all, I would not recommend using UDP forwarding for public services, since it's most likely prone to simple DoS or DDoS.
## HTTP/SOCKS Proxy
**onetun** is a Transport-layer proxy (also known as port forwarding); it is not in scope to provide
a HTTP/SOCKS proxy server. However, you can easily chain **onetun** with a proxy server on a remote
that is locked down to your WireGuard network.
For example, you could run [dante-server](https://www.inet.no/dante/) on a peer (ex. `192.168.4.2`) with the following configuration:
```
# /etc/danted.conf
logoutput: syslog
user.privileged: root
user.unprivileged: nobody
internal: 192.168.4.2 port=1080
external: eth0
socksmethod: none
clientmethod: none
# Locks down proxy use to WireGuard peers (192.168.4.x)
client pass {
from: 192.168.4.0/24 to: 0.0.0.0/0
}
socks pass {
from: 192.168.4.0/24 to: 0.0.0.0/0
}
```
Then use **onetun** to expose the SOCKS5 proxy locally:
```shell
onetun 127.0.0.1:1080:192.168.4.2:1080
INFO onetun::tunnel > Tunneling TCP [127.0.0.1:1080]->[192.168.4.2:1080] (via [140.30.3.182:51820] as peer 192.168.4.3)
```
Test with `curl` (or configure your browser):
```shell
curl -x socks5://127.0.0.1:1080 https://ifconfig.me
```
## Contributing and Maintenance
I will gladly accept contributions to onetun, and set aside time to review all pull-requests.
Please consider opening a GitHub issue if you are unsure if your contribution is within the scope of the project.
**Disclaimer**: I do not have enough personal time to actively maintain onetun besides open-source contributions.
## License
MIT License. See `LICENSE` for details. Copyright &copy; 2021-2022 Aram Peres.
MIT License. See `LICENSE` for details. Copyright &copy; 2025 Aram Peres.

View file

@ -5,18 +5,19 @@ use std::fs::read_to_string;
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::sync::Arc;
use anyhow::Context;
use boringtun::crypto::{X25519PublicKey, X25519SecretKey};
use anyhow::{bail, Context};
pub use boringtun::x25519::{PublicKey, StaticSecret};
const DEFAULT_PORT_FORWARD_SOURCE: &str = "127.0.0.1";
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct Config {
pub port_forwards: Vec<PortForwardConfig>,
#[allow(dead_code)]
pub remote_port_forwards: Vec<PortForwardConfig>,
pub private_key: Arc<X25519SecretKey>,
pub endpoint_public_key: Arc<X25519PublicKey>,
pub private_key: Arc<StaticSecret>,
pub endpoint_public_key: Arc<PublicKey>,
pub preshared_key: Option<[u8; 32]>,
pub endpoint_addr: SocketAddr,
pub endpoint_bind_addr: SocketAddr,
pub source_peer_ip: IpAddr,
@ -30,18 +31,17 @@ pub struct Config {
impl Config {
#[cfg(feature = "bin")]
pub fn from_args() -> anyhow::Result<Self> {
use clap::{App, Arg};
use clap::{Arg, Command};
let mut warnings = vec![];
let matches = App::new("onetun")
let matches = Command::new("onetun")
.author("Aram Peres <aram.peres@gmail.com>")
.version(env!("CARGO_PKG_VERSION"))
.args(&[
Arg::with_name("PORT_FORWARD")
Arg::new("PORT_FORWARD")
.required(false)
.multiple(true)
.takes_value(true)
.num_args(1..)
.help("Port forward configurations. The format of each argument is [src_host:]<src_port>:<dst_host>:<dst_port>[:TCP,UDP,...], \
where [src_host] is the local IP to listen on, <src_port> is the local port to listen on, <dst_host> is the remote peer IP to forward to, and <dst_port> is the remote port to forward to. \
Environment variables of the form 'ONETUN_PORT_FORWARD_[#]' are also accepted, where [#] starts at 1.\n\
@ -55,74 +55,79 @@ impl Config {
\tlocalhost:8080:192.168.4.1:8081:TCP\n\
\tlocalhost:8080:peer.intranet:8081:TCP\
"),
Arg::with_name("private-key")
.required_unless("private-key-file")
.takes_value(true)
Arg::new("private-key")
.conflicts_with("private-key-file")
.num_args(1)
.long("private-key")
.env("ONETUN_PRIVATE_KEY")
.help("The private key of this peer. The corresponding public key should be registered in the WireGuard endpoint. \
You can also use '--private-key-file' to specify a file containing the key instead."),
Arg::with_name("private-key-file")
.takes_value(true)
Arg::new("private-key-file")
.num_args(1)
.long("private-key-file")
.env("ONETUN_PRIVATE_KEY_FILE")
.help("The path to a file containing the private key of this peer. The corresponding public key should be registered in the WireGuard endpoint."),
Arg::with_name("endpoint-public-key")
Arg::new("endpoint-public-key")
.required(true)
.takes_value(true)
.num_args(1)
.long("endpoint-public-key")
.env("ONETUN_ENDPOINT_PUBLIC_KEY")
.help("The public key of the WireGuard endpoint (remote)."),
Arg::with_name("endpoint-addr")
Arg::new("preshared-key")
.required(false)
.num_args(1)
.long("preshared-key")
.env("ONETUN_PRESHARED_KEY")
.help("The pre-shared key (PSK) as configured with the peer."),
Arg::new("endpoint-addr")
.required(true)
.takes_value(true)
.num_args(1)
.long("endpoint-addr")
.env("ONETUN_ENDPOINT_ADDR")
.help("The address (IP + port) of the WireGuard endpoint (remote). Example: 1.2.3.4:51820"),
Arg::with_name("endpoint-bind-addr")
Arg::new("endpoint-bind-addr")
.required(false)
.takes_value(true)
.num_args(1)
.long("endpoint-bind-addr")
.env("ONETUN_ENDPOINT_BIND_ADDR")
.help("The address (IP + port) used to bind the local UDP socket for the WireGuard tunnel. Example: 1.2.3.4:30000. Defaults to 0.0.0.0:0 for IPv4 endpoints, or [::]:0 for IPv6 endpoints."),
Arg::with_name("source-peer-ip")
Arg::new("source-peer-ip")
.required(true)
.takes_value(true)
.num_args(1)
.long("source-peer-ip")
.env("ONETUN_SOURCE_PEER_IP")
.help("The source IP to identify this peer as (local). Example: 192.168.4.3"),
Arg::with_name("keep-alive")
Arg::new("keep-alive")
.required(false)
.takes_value(true)
.num_args(1)
.long("keep-alive")
.env("ONETUN_KEEP_ALIVE")
.help("Configures a persistent keep-alive for the WireGuard tunnel, in seconds."),
Arg::with_name("max-transmission-unit")
Arg::new("max-transmission-unit")
.required(false)
.takes_value(true)
.num_args(1)
.long("max-transmission-unit")
.env("ONETUN_MTU")
.default_value("1420")
.help("Configures the max-transmission-unit (MTU) of the WireGuard tunnel."),
Arg::with_name("log")
Arg::new("log")
.required(false)
.takes_value(true)
.num_args(1)
.long("log")
.env("ONETUN_LOG")
.default_value("info")
.help("Configures the log level and format."),
Arg::with_name("pcap")
Arg::new("pcap")
.required(false)
.takes_value(true)
.num_args(1)
.long("pcap")
.env("ONETUN_PCAP")
.help("Decrypts and captures IP packets on the WireGuard tunnel to a given output file."),
Arg::with_name("remote")
Arg::new("remote")
.required(false)
.takes_value(true)
.multiple(true)
.num_args(1..)
.long("remote")
.short("r")
.short('r')
.help("Remote port forward configurations. The format of each argument is <src_port>:<dst_host>:<dst_port>[:TCP,UDP,...], \
where <src_port> is the port the other peers will reach the server with, <dst_host> is the IP to forward to, and <dst_port> is the port to forward to. \
The <src_port> will be bound on onetun's peer IP, as specified by --source-peer-ip. If you pass a different value for <src_host> here, it will be rejected.\n\
@ -137,7 +142,7 @@ impl Config {
// Combine `PORT_FORWARD` arg and `ONETUN_PORT_FORWARD_#` envs
let mut port_forward_strings = HashSet::new();
if let Some(values) = matches.values_of("PORT_FORWARD") {
if let Some(values) = matches.get_many::<String>("PORT_FORWARD") {
for value in values {
port_forward_strings.insert(value.to_owned());
}
@ -156,18 +161,18 @@ impl Config {
.map(|s| PortForwardConfig::from_notation(&s, DEFAULT_PORT_FORWARD_SOURCE))
.collect();
let port_forwards: Vec<PortForwardConfig> = port_forwards
.with_context(|| "Failed to parse port forward config")?
.context("Failed to parse port forward config")?
.into_iter()
.flatten()
.collect();
// Read source-peer-ip
let source_peer_ip = parse_ip(matches.value_of("source-peer-ip"))
.with_context(|| "Invalid source peer IP")?;
let source_peer_ip = parse_ip(matches.get_one::<String>("source-peer-ip"))
.context("Invalid source peer IP")?;
// Combined `remote` arg and `ONETUN_REMOTE_PORT_FORWARD_#` envs
let mut port_forward_strings = HashSet::new();
if let Some(values) = matches.values_of("remote") {
if let Some(values) = matches.get_many::<String>("remote") {
for value in values {
port_forward_strings.insert(value.to_owned());
}
@ -186,30 +191,30 @@ impl Config {
.map(|s| {
PortForwardConfig::from_notation(
&s,
matches.value_of("source-peer-ip").unwrap(),
matches.get_one::<String>("source-peer-ip").unwrap(),
)
})
.collect();
let mut remote_port_forwards: Vec<PortForwardConfig> = remote_port_forwards
.with_context(|| "Failed to parse remote port forward config")?
.context("Failed to parse remote port forward config")?
.into_iter()
.flatten()
.collect();
for port_forward in remote_port_forwards.iter_mut() {
if port_forward.source.ip() != source_peer_ip {
return Err(anyhow::anyhow!("Remote port forward config <src_host> must match --source-peer-ip ({}), or be omitted.", source_peer_ip));
bail!("Remote port forward config <src_host> must match --source-peer-ip ({}), or be omitted.", source_peer_ip);
}
port_forward.source = SocketAddr::from((source_peer_ip, port_forward.source.port()));
port_forward.remote = true;
}
if port_forwards.is_empty() && remote_port_forwards.is_empty() {
return Err(anyhow::anyhow!("No port forward configurations given."));
bail!("No port forward configurations given.");
}
// Read private key from file or CLI argument
let (group_readable, world_readable) = matches
.value_of("private-key-file")
.get_one::<String>("private-key-file")
.and_then(is_file_insecurely_readable)
.unwrap_or_default();
if group_readable {
@ -219,31 +224,32 @@ impl Config {
warnings.push("Private key file is world-readable. This is insecure.".into());
}
let private_key = if let Some(private_key_file) = matches.value_of("private-key-file") {
let private_key = if let Some(private_key_file) =
matches.get_one::<String>("private-key-file")
{
read_to_string(private_key_file)
.map(|s| s.trim().to_string())
.with_context(|| "Failed to read private key file")
.context("Failed to read private key file")
} else {
if std::env::var("ONETUN_PRIVATE_KEY").is_err() {
warnings.push("Private key was passed using CLI. This is insecure. \
Use \"--private-key-file <file containing private key>\", or the \"ONETUN_PRIVATE_KEY\" env variable instead.".into());
}
matches
.value_of("private-key")
.map(String::from)
.with_context(|| "Missing private key")
.get_one::<String>("private-key")
.cloned()
.context("Missing private key")
}?;
let endpoint_addr = parse_addr(matches.value_of("endpoint-addr"))
.with_context(|| "Invalid endpoint address")?;
let endpoint_addr = parse_addr(matches.get_one::<String>("endpoint-addr"))
.context("Invalid endpoint address")?;
let endpoint_bind_addr = if let Some(addr) = matches.value_of("endpoint-bind-addr") {
let addr = parse_addr(Some(addr)).with_context(|| "Invalid bind address")?;
let endpoint_bind_addr = if let Some(addr) = matches.get_one::<String>("endpoint-bind-addr")
{
let addr = parse_addr(Some(addr)).context("Invalid bind address")?;
// Make sure the bind address and endpoint address are the same IP version
if addr.ip().is_ipv4() != endpoint_addr.ip().is_ipv4() {
return Err(anyhow::anyhow!(
"Endpoint and bind addresses must be the same IP version"
));
bail!("Endpoint and bind addresses must be the same IP version");
}
addr
} else {
@ -257,54 +263,77 @@ impl Config {
Ok(Self {
port_forwards,
remote_port_forwards,
private_key: Arc::new(
parse_private_key(&private_key).with_context(|| "Invalid private key")?,
),
private_key: Arc::new(parse_private_key(&private_key).context("Invalid private key")?),
endpoint_public_key: Arc::new(
parse_public_key(matches.value_of("endpoint-public-key"))
.with_context(|| "Invalid endpoint public key")?,
parse_public_key(matches.get_one::<String>("endpoint-public-key"))
.context("Invalid endpoint public key")?,
),
preshared_key: parse_preshared_key(matches.get_one::<String>("preshared-key"))?,
endpoint_addr,
endpoint_bind_addr,
source_peer_ip,
keepalive_seconds: parse_keep_alive(matches.value_of("keep-alive"))
.with_context(|| "Invalid keep-alive value")?,
max_transmission_unit: parse_mtu(matches.value_of("max-transmission-unit"))
.with_context(|| "Invalid max-transmission-unit value")?,
log: matches.value_of("log").unwrap_or_default().into(),
pcap_file: matches.value_of("pcap").map(String::from),
keepalive_seconds: parse_keep_alive(matches.get_one::<String>("keep-alive"))
.context("Invalid keep-alive value")?,
max_transmission_unit: parse_mtu(matches.get_one::<String>("max-transmission-unit"))
.context("Invalid max-transmission-unit value")?,
log: matches
.get_one::<String>("log")
.cloned()
.unwrap_or_default(),
pcap_file: matches.get_one::<String>("pcap").cloned(),
warnings,
})
}
}
fn parse_addr(s: Option<&str>) -> anyhow::Result<SocketAddr> {
s.with_context(|| "Missing address")?
fn parse_addr<T: AsRef<str>>(s: Option<T>) -> anyhow::Result<SocketAddr> {
s.context("Missing address")?
.as_ref()
.to_socket_addrs()
.with_context(|| "Invalid address")?
.context("Invalid address")?
.next()
.with_context(|| "Could not lookup address")
.context("Could not lookup address")
}
fn parse_ip(s: Option<&str>) -> anyhow::Result<IpAddr> {
s.with_context(|| "Missing IP")?
fn parse_ip(s: Option<&String>) -> anyhow::Result<IpAddr> {
s.context("Missing IP address")?
.parse::<IpAddr>()
.with_context(|| "Invalid IP address")
.context("Invalid IP address")
}
fn parse_private_key(s: &str) -> anyhow::Result<X25519SecretKey> {
s.parse::<X25519SecretKey>()
.map_err(|e| anyhow::anyhow!("{}", e))
fn parse_private_key(s: &str) -> anyhow::Result<StaticSecret> {
let decoded = base64::decode(s).context("Failed to decode private key")?;
if let Ok::<[u8; 32], _>(bytes) = decoded.try_into() {
Ok(StaticSecret::from(bytes))
} else {
bail!("Invalid private key")
}
}
fn parse_public_key(s: Option<&str>) -> anyhow::Result<X25519PublicKey> {
s.with_context(|| "Missing public key")?
.parse::<X25519PublicKey>()
.map_err(|e| anyhow::anyhow!("{}", e))
.with_context(|| "Invalid public key")
fn parse_public_key(s: Option<&String>) -> anyhow::Result<PublicKey> {
let encoded = s.context("Missing public key")?;
let decoded = base64::decode(encoded).context("Failed to decode public key")?;
if let Ok::<[u8; 32], _>(bytes) = decoded.try_into() {
Ok(PublicKey::from(bytes))
} else {
bail!("Invalid public key")
}
}
fn parse_keep_alive(s: Option<&str>) -> anyhow::Result<Option<u16>> {
fn parse_preshared_key(s: Option<&String>) -> anyhow::Result<Option<[u8; 32]>> {
if let Some(s) = s {
let decoded = base64::decode(s).context("Failed to decode preshared key")?;
if let Ok::<[u8; 32], _>(bytes) = decoded.try_into() {
Ok(Some(bytes))
} else {
bail!("Invalid preshared key")
}
} else {
Ok(None)
}
}
fn parse_keep_alive(s: Option<&String>) -> anyhow::Result<Option<u16>> {
if let Some(s) = s {
let parsed: u16 = s.parse().with_context(|| {
format!(
@ -318,23 +347,21 @@ fn parse_keep_alive(s: Option<&str>) -> anyhow::Result<Option<u16>> {
}
}
fn parse_mtu(s: Option<&str>) -> anyhow::Result<usize> {
s.with_context(|| "Missing MTU")?
.parse()
.with_context(|| "Invalid MTU")
fn parse_mtu(s: Option<&String>) -> anyhow::Result<usize> {
s.context("Missing MTU")?.parse().context("Invalid MTU")
}
#[cfg(unix)]
fn is_file_insecurely_readable(path: &str) -> Option<(bool, bool)> {
fn is_file_insecurely_readable(path: &String) -> Option<(bool, bool)> {
use std::fs::File;
use std::os::unix::fs::MetadataExt;
let mode = File::open(&path).ok()?.metadata().ok()?.mode();
let mode = File::open(path).ok()?.metadata().ok()?.mode();
Some((mode & 0o40 > 0, mode & 0o4 > 0))
}
#[cfg(not(unix))]
fn is_file_insecurely_readable(_path: &str) -> Option<(bool, bool)> {
fn is_file_insecurely_readable(_path: &String) -> Option<(bool, bool)> {
// No good way to determine permissions on non-Unix target
None
}
@ -450,27 +477,21 @@ impl PortForwardConfig {
let source = (
src_addr.0.unwrap_or(default_source),
src_addr
.1
.parse::<u16>()
.with_context(|| "Invalid source port")?,
src_addr.1.parse::<u16>().context("Invalid source port")?,
)
.to_socket_addrs()
.with_context(|| "Invalid source address")?
.context("Invalid source address")?
.next()
.with_context(|| "Could not resolve source address")?;
.context("Could not resolve source address")?;
let destination = (
dst_addr.0,
dst_addr
.1
.parse::<u16>()
.with_context(|| "Invalid source port")?,
dst_addr.1.parse::<u16>().context("Invalid source port")?,
)
.to_socket_addrs() // TODO: Pass this as given and use DNS config instead (issue #15)
.with_context(|| "Invalid destination address")?
.context("Invalid destination address")?
.next()
.with_context(|| "Could not resolve destination address")?;
.context("Could not resolve destination address")?;
// Parse protocols
let protocols = if let Some(protocols) = protocols {
@ -480,7 +501,7 @@ impl PortForwardConfig {
} else {
Ok(vec![PortProtocol::Tcp])
}
.with_context(|| "Failed to parse protocols")?;
.context("Failed to parse protocols")?;
// Returns an config for each protocol
Ok(protocols

View file

@ -1,3 +1,4 @@
use bytes::Bytes;
use std::fmt::{Display, Formatter};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
@ -16,13 +17,13 @@ pub enum Event {
/// A connection was dropped from the pool and should be closed in all interfaces.
ClientConnectionDropped(VirtualPort),
/// Data received by the local server that should be sent to the virtual server.
LocalData(PortForwardConfig, VirtualPort, Vec<u8>),
LocalData(PortForwardConfig, VirtualPort, Bytes),
/// Data received by the remote server that should be sent to the local client.
RemoteData(VirtualPort, Vec<u8>),
RemoteData(VirtualPort, Bytes),
/// IP packet received from the WireGuard tunnel that should be passed through the corresponding virtual device.
InboundInternetPacket(PortProtocol, Vec<u8>),
InboundInternetPacket(PortProtocol, Bytes),
/// IP packet to be sent through the WireGuard tunnel as crafted by the virtual device.
OutboundInternetPacket(Vec<u8>),
OutboundInternetPacket(Bytes),
/// Notifies that a virtual device read an IP packet.
VirtualDeviceFed(PortProtocol),
}

View file

@ -41,7 +41,7 @@ pub async fn start_tunnels(config: Config, bus: Bus) -> anyhow::Result<()> {
let wg = WireGuardTunnel::new(&config, bus.clone())
.await
.with_context(|| "Failed to initialize WireGuard tunnel")?;
.context("Failed to initialize WireGuard tunnel")?;
let wg = Arc::new(wg);
{
@ -53,7 +53,7 @@ pub async fn start_tunnels(config: Config, bus: Bus) -> anyhow::Result<()> {
{
// Start consumption task for WireGuard
let wg = wg.clone();
tokio::spawn(async move { wg.consume_task().await });
tokio::spawn(Box::pin(async move { wg.consume_task().await }));
}
{

View file

@ -8,7 +8,7 @@ async fn main() -> anyhow::Result<()> {
use anyhow::Context;
use onetun::{config::Config, events::Bus};
let config = Config::from_args().with_context(|| "Failed to read config")?;
let config = Config::from_args().context("Configuration has errors")?;
init_logger(&config)?;
for warning in &config.warnings {
@ -32,7 +32,5 @@ fn init_logger(config: &onetun::config::Config) -> anyhow::Result<()> {
let mut builder = pretty_env_logger::formatted_timed_builder();
builder.parse_filters(&config.log);
builder
.try_init()
.with_context(|| "Failed to initialize logger")
builder.try_init().context("Failed to initialize logger")
}

View file

@ -16,7 +16,7 @@ impl Pcap {
self.writer
.flush()
.await
.with_context(|| "Failed to flush pcap writer")
.context("Failed to flush pcap writer")
}
async fn write(&mut self, data: &[u8]) -> anyhow::Result<usize> {
@ -30,14 +30,14 @@ impl Pcap {
self.writer
.write_u16(value)
.await
.with_context(|| "Failed to write u16 to pcap writer")
.context("Failed to write u16 to pcap writer")
}
async fn write_u32(&mut self, value: u32) -> anyhow::Result<()> {
self.writer
.write_u32(value)
.await
.with_context(|| "Failed to write u32 to pcap writer")
.context("Failed to write u32 to pcap writer")
}
async fn global_header(&mut self) -> anyhow::Result<()> {
@ -64,14 +64,14 @@ impl Pcap {
async fn packet(&mut self, timestamp: Instant, packet: &[u8]) -> anyhow::Result<()> {
self.packet_header(timestamp, packet.len())
.await
.with_context(|| "Failed to write packet header to pcap writer")?;
.context("Failed to write packet header to pcap writer")?;
self.write(packet)
.await
.with_context(|| "Failed to write packet to pcap writer")?;
.context("Failed to write packet to pcap writer")?;
self.writer
.flush()
.await
.with_context(|| "Failed to flush pcap writer")?;
.context("Failed to flush pcap writer")?;
self.flush().await
}
}
@ -81,14 +81,14 @@ pub async fn capture(pcap_file: String, bus: Bus) -> anyhow::Result<()> {
let mut endpoint = bus.new_endpoint();
let file = File::create(&pcap_file)
.await
.with_context(|| "Failed to create pcap file")?;
.context("Failed to create pcap file")?;
let writer = BufWriter::new(file);
let mut writer = Pcap { writer };
writer
.global_header()
.await
.with_context(|| "Failed to write global header to pcap writer")?;
.context("Failed to write global header to pcap writer")?;
info!("Capturing WireGuard IP packets to {}", &pcap_file);
loop {
@ -98,14 +98,14 @@ pub async fn capture(pcap_file: String, bus: Bus) -> anyhow::Result<()> {
writer
.packet(instant, &ip)
.await
.with_context(|| "Failed to write inbound IP packet to pcap writer")?;
.context("Failed to write inbound IP packet to pcap writer")?;
}
Event::OutboundInternetPacket(ip) => {
let instant = Instant::now();
writer
.packet(instant, &ip)
.await
.with_context(|| "Failed to write output IP packet to pcap writer")?;
.context("Failed to write output IP packet to pcap writer")?;
}
_ => {}
}

View file

@ -1,17 +1,18 @@
use crate::config::{PortForwardConfig, PortProtocol};
use crate::virtual_iface::VirtualPort;
use anyhow::Context;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::net::{TcpListener, TcpStream};
use std::ops::Range;
use std::sync::Arc;
use std::time::Duration;
use crate::events::{Bus, Event};
use anyhow::Context;
use bytes::BytesMut;
use rand::seq::SliceRandom;
use rand::thread_rng;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use crate::config::{PortForwardConfig, PortProtocol};
use crate::events::{Bus, Event};
use crate::virtual_iface::VirtualPort;
const MAX_PACKET: usize = 65536;
const MIN_PORT: u16 = 1000;
@ -26,14 +27,14 @@ pub async fn tcp_proxy_server(
) -> anyhow::Result<()> {
let listener = TcpListener::bind(port_forward.source)
.await
.with_context(|| "Failed to listen on TCP proxy server")?;
.context("Failed to listen on TCP proxy server")?;
loop {
let port_pool = port_pool.clone();
let (socket, peer_addr) = listener
.accept()
.await
.with_context(|| "Failed to accept connection on TCP proxy server")?;
.context("Failed to accept connection on TCP proxy server")?;
// Assign a 'virtual port': this is a unique port number used to route IP packets
// received from the WireGuard tunnel. It is the port number that the virtual client will
@ -81,7 +82,7 @@ async fn handle_tcp_proxy_connection(
let mut endpoint = bus.new_endpoint();
endpoint.send(Event::ClientConnectionInitiated(port_forward, virtual_port));
let mut buffer = Vec::with_capacity(MAX_PACKET);
let mut buffer = BytesMut::with_capacity(MAX_PACKET);
loop {
tokio::select! {
readable_result = socket.readable() => {
@ -90,7 +91,7 @@ async fn handle_tcp_proxy_connection(
match socket.try_read_buf(&mut buffer) {
Ok(size) if size > 0 => {
let data = Vec::from(&buffer[..size]);
endpoint.send(Event::LocalData(port_forward, virtual_port, data));
endpoint.send(Event::LocalData(port_forward, virtual_port, data.into()));
// Reset buffer
buffer.clear();
}
@ -185,13 +186,13 @@ impl TcpPortPool {
}
}
/// Requests a free port from the pool. An error is returned if none is available (exhaused max capacity).
/// Requests a free port from the pool. An error is returned if none is available (exhausted max capacity).
pub async fn next(&self) -> anyhow::Result<VirtualPort> {
let mut inner = self.inner.write().await;
let port = inner
.queue
.pop_front()
.with_context(|| "TCP virtual port pool is exhausted")?;
.context("TCP virtual port pool is exhausted")?;
Ok(VirtualPort::new(port, PortProtocol::Tcp))
}

View file

@ -4,14 +4,15 @@ use std::ops::Range;
use std::sync::Arc;
use std::time::Instant;
use crate::events::{Bus, Event};
use anyhow::Context;
use bytes::Bytes;
use priority_queue::double_priority_queue::DoublePriorityQueue;
use rand::seq::SliceRandom;
use rand::thread_rng;
use tokio::net::UdpSocket;
use crate::config::{PortForwardConfig, PortProtocol};
use crate::events::{Bus, Event};
use crate::virtual_iface::VirtualPort;
const MAX_PACKET: usize = 65536;
@ -36,7 +37,7 @@ pub async fn udp_proxy_server(
let mut endpoint = bus.new_endpoint();
let socket = UdpSocket::bind(port_forward.source)
.await
.with_context(|| "Failed to bind on UDP proxy address")?;
.context("Failed to bind on UDP proxy address")?;
let mut buffer = [0u8; MAX_PACKET];
loop {
@ -98,11 +99,11 @@ async fn next_udp_datagram(
socket: &UdpSocket,
buffer: &mut [u8],
port_pool: UdpPortPool,
) -> anyhow::Result<Option<(VirtualPort, Vec<u8>)>> {
) -> anyhow::Result<Option<(VirtualPort, Bytes)>> {
let (size, peer_addr) = socket
.recv_from(buffer)
.await
.with_context(|| "Failed to accept incoming UDP datagram")?;
.context("Failed to accept incoming UDP datagram")?;
// Assign a 'virtual port': this is a unique port number used to route IP packets
// received from the WireGuard tunnel. It is the port number that the virtual client will
@ -126,7 +127,7 @@ async fn next_udp_datagram(
port_pool.update_last_transmit(port).await;
let data = buffer[..size].to_vec();
Ok(Some((port, data)))
Ok(Some((port, data.into())))
}
/// A pool of virtual ports available for TCP connections.
@ -211,7 +212,7 @@ impl UdpPortPool {
None
}
})
.with_context(|| "virtual port pool is exhausted")?;
.context("Virtual port pool is exhausted")?;
inner.port_by_peer_addr.insert(peer_addr, port);
inner.peer_addr_by_port.insert(port, peer_addr);

View file

@ -1,10 +1,15 @@
use crate::config::PortProtocol;
use crate::events::{BusSender, Event};
use crate::Bus;
use smoltcp::phy::{Device, DeviceCapabilities, Medium};
use smoltcp::time::Instant;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use bytes::{BufMut, Bytes, BytesMut};
use smoltcp::{
phy::{DeviceCapabilities, Medium},
time::Instant,
};
use std::{
collections::VecDeque,
sync::{Arc, Mutex},
};
/// A virtual device that processes IP packets through smoltcp and WireGuard.
pub struct VirtualIpDevice {
@ -13,7 +18,7 @@ pub struct VirtualIpDevice {
/// Channel receiver for received IP packets.
bus_sender: BusSender,
/// Local queue for packets received from the bus that need to go through the smoltcp interface.
process_queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
process_queue: Arc<Mutex<VecDeque<Bytes>>>,
}
impl VirtualIpDevice {
@ -49,11 +54,17 @@ impl VirtualIpDevice {
}
}
impl<'a> Device<'a> for VirtualIpDevice {
type RxToken = RxToken;
type TxToken = TxToken;
impl smoltcp::phy::Device for VirtualIpDevice {
type RxToken<'a>
= RxToken
where
Self: 'a;
type TxToken<'a>
= TxToken
where
Self: 'a;
fn receive(&'a mut self) -> Option<(Self::RxToken, Self::TxToken)> {
fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
let next = {
let mut queue = self
.process_queue
@ -63,7 +74,13 @@ impl<'a> Device<'a> for VirtualIpDevice {
};
match next {
Some(buffer) => Some((
Self::RxToken { buffer },
Self::RxToken {
buffer: {
let mut buf = BytesMut::new();
buf.put(buffer);
buf
},
},
Self::TxToken {
sender: self.bus_sender.clone(),
},
@ -72,7 +89,7 @@ impl<'a> Device<'a> for VirtualIpDevice {
}
}
fn transmit(&'a mut self) -> Option<Self::TxToken> {
fn transmit(&mut self, _timestamp: Instant) -> Option<Self::TxToken<'_>> {
Some(TxToken {
sender: self.bus_sender.clone(),
})
@ -88,15 +105,15 @@ impl<'a> Device<'a> for VirtualIpDevice {
#[doc(hidden)]
pub struct RxToken {
buffer: Vec<u8>,
buffer: BytesMut,
}
impl smoltcp::phy::RxToken for RxToken {
fn consume<R, F>(mut self, _timestamp: Instant, f: F) -> smoltcp::Result<R>
fn consume<R, F>(self, f: F) -> R
where
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
F: FnOnce(&[u8]) -> R,
{
f(&mut self.buffer)
f(&self.buffer)
}
}
@ -106,14 +123,14 @@ pub struct TxToken {
}
impl smoltcp::phy::TxToken for TxToken {
fn consume<R, F>(self, _timestamp: Instant, len: usize, f: F) -> smoltcp::Result<R>
fn consume<R, F>(self, len: usize, f: F) -> R
where
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
F: FnOnce(&mut [u8]) -> R,
{
let mut buffer = Vec::new();
buffer.resize(len, 0);
let mut buffer = vec![0; len];
let result = f(&mut buffer);
self.sender.send(Event::OutboundInternetPacket(buffer));
self.sender
.send(Event::OutboundInternetPacket(buffer.into()));
result
}
}

View file

@ -5,12 +5,19 @@ use crate::virtual_iface::{VirtualInterfacePoll, VirtualPort};
use crate::Bus;
use anyhow::Context;
use async_trait::async_trait;
use smoltcp::iface::{InterfaceBuilder, SocketHandle};
use smoltcp::socket::{TcpSocket, TcpSocketBuffer, TcpState};
use smoltcp::wire::{IpAddress, IpCidr};
use std::collections::{HashMap, HashSet, VecDeque};
use std::net::IpAddr;
use std::time::Duration;
use bytes::Bytes;
use smoltcp::iface::PollResult;
use smoltcp::{
iface::{Config, Interface, SocketHandle, SocketSet},
socket::tcp,
time::Instant,
wire::{HardwareAddress, IpAddress, IpCidr, IpVersion},
};
use std::{
collections::{HashMap, HashSet, VecDeque},
net::IpAddr,
time::Duration,
};
const MAX_PACKET: usize = 65536;
@ -19,6 +26,7 @@ pub struct TcpVirtualInterface {
source_peer_ip: IpAddr,
port_forwards: Vec<PortForwardConfig>,
bus: Bus,
sockets: SocketSet<'static>,
}
impl TcpVirtualInterface {
@ -32,33 +40,34 @@ impl TcpVirtualInterface {
.collect(),
source_peer_ip,
bus,
sockets: SocketSet::new([]),
}
}
fn new_server_socket(port_forward: PortForwardConfig) -> anyhow::Result<TcpSocket<'static>> {
fn new_server_socket(port_forward: PortForwardConfig) -> anyhow::Result<tcp::Socket<'static>> {
static mut TCP_SERVER_RX_DATA: [u8; 0] = [];
static mut TCP_SERVER_TX_DATA: [u8; 0] = [];
let tcp_rx_buffer = TcpSocketBuffer::new(unsafe { &mut TCP_SERVER_RX_DATA[..] });
let tcp_tx_buffer = TcpSocketBuffer::new(unsafe { &mut TCP_SERVER_TX_DATA[..] });
let mut socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
let tcp_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut TCP_SERVER_RX_DATA[..] });
let tcp_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut TCP_SERVER_TX_DATA[..] });
let mut socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer);
socket
.listen((
IpAddress::from(port_forward.destination.ip()),
port_forward.destination.port(),
))
.with_context(|| "Virtual server socket failed to listen")?;
.context("Virtual server socket failed to listen")?;
Ok(socket)
}
fn new_client_socket() -> anyhow::Result<TcpSocket<'static>> {
fn new_client_socket() -> anyhow::Result<tcp::Socket<'static>> {
let rx_data = vec![0u8; MAX_PACKET];
let tx_data = vec![0u8; MAX_PACKET];
let tcp_rx_buffer = TcpSocketBuffer::new(rx_data);
let tcp_tx_buffer = TcpSocketBuffer::new(tx_data);
let socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
let tcp_rx_buffer = tcp::SocketBuffer::new(rx_data);
let tcp_tx_buffer = tcp::SocketBuffer::new(tx_data);
let socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer);
Ok(socket)
}
@ -70,26 +79,32 @@ impl TcpVirtualInterface {
}
addresses
.into_iter()
.map(|addr| IpCidr::new(addr, 32))
.map(|addr| IpCidr::new(addr, addr_length(&addr)))
.collect()
}
}
#[async_trait]
impl VirtualInterfacePoll for TcpVirtualInterface {
async fn poll_loop(self, device: VirtualIpDevice) -> anyhow::Result<()> {
async fn poll_loop(mut self, mut device: VirtualIpDevice) -> anyhow::Result<()> {
// Create CIDR block for source peer IP + each port forward IP
let addresses = self.addresses();
let config = Config::new(HardwareAddress::Ip);
// Create virtual interface (contains smoltcp state machine)
let mut iface = InterfaceBuilder::new(device, vec![])
.ip_addrs(addresses)
.finalize();
let mut iface = Interface::new(config, &mut device, Instant::now());
iface.update_ip_addrs(|ip_addrs| {
addresses.into_iter().for_each(|addr| {
ip_addrs
.push(addr)
.expect("maximum number of IPs in TCP interface reached");
});
});
// Create virtual server for each port forward
for port_forward in self.port_forwards.iter() {
let server_socket = TcpVirtualInterface::new_server_socket(*port_forward)?;
iface.add_socket(server_socket);
self.sockets.add(server_socket);
}
// The next time to poll the interface. Can be None for instant poll.
@ -102,7 +117,7 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
let mut port_client_handle_map: HashMap<VirtualPort, SocketHandle> = HashMap::new();
// Data packets to send from a virtual client
let mut send_queue: HashMap<VirtualPort, VecDeque<Vec<u8>>> = HashMap::new();
let mut send_queue: HashMap<VirtualPort, VecDeque<Bytes>> = HashMap::new();
loop {
tokio::select! {
@ -115,11 +130,11 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
// Find closed sockets
port_client_handle_map.retain(|virtual_port, client_handle| {
let client_socket = iface.get_socket::<TcpSocket>(*client_handle);
if client_socket.state() == TcpState::Closed {
let client_socket = self.sockets.get_mut::<tcp::Socket>(*client_handle);
if client_socket.state() == tcp::State::Closed {
endpoint.send(Event::ClientConnectionDropped(*virtual_port));
send_queue.remove(virtual_port);
iface.remove_socket(*client_handle);
self.sockets.remove(*client_handle);
false
} else {
// Not closed, retain
@ -127,16 +142,12 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
}
});
match iface.poll(loop_start) {
Ok(processed) if processed => {
trace!("TCP virtual interface polled some packets to be processed");
}
Err(e) => error!("TCP virtual interface poll error: {:?}", e),
_ => {}
if iface.poll(loop_start, &mut device, &mut self.sockets) == PollResult::SocketStateChanged {
log::trace!("TCP virtual interface polled some packets to be processed");
}
for (virtual_port, client_handle) in port_client_handle_map.iter() {
let client_socket = iface.get_socket::<TcpSocket>(*client_handle);
let client_socket = self.sockets.get_mut::<tcp::Socket>(*client_handle);
if client_socket.can_send() {
if let Some(send_queue) = send_queue.get_mut(virtual_port) {
let to_transfer = send_queue.pop_front();
@ -147,7 +158,7 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
if sent < total {
// Sometimes only a subset is sent, so the rest needs to be sent on the next poll
let tx_extra = Vec::from(&to_transfer_slice[sent..total]);
send_queue.push_front(tx_extra);
send_queue.push_front(tx_extra.into());
}
}
Err(e) => {
@ -156,13 +167,13 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
);
}
}
} else if client_socket.state() == TcpState::CloseWait {
} else if client_socket.state() == tcp::State::CloseWait {
client_socket.close();
}
}
}
if client_socket.can_recv() {
match client_socket.recv(|buffer| (buffer.len(), buffer.to_vec())) {
match client_socket.recv(|buffer| (buffer.len(), Bytes::from(buffer.to_vec()))) {
Ok(data) => {
debug!("[{}] Received {} bytes from virtual server", virtual_port, data.len());
if !data.is_empty() {
@ -179,7 +190,7 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
}
// The virtual interface determines the next time to poll (this is to reduce unnecessary polls)
next_poll = match iface.poll_delay(loop_start) {
next_poll = match iface.poll_delay(loop_start, &self.sockets) {
Some(smoltcp::time::Duration::ZERO) => None,
Some(delay) => {
trace!("TCP Virtual interface delayed next poll by {}", delay);
@ -192,13 +203,14 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
match event {
Event::ClientConnectionInitiated(port_forward, virtual_port) => {
let client_socket = TcpVirtualInterface::new_client_socket()?;
let client_handle = iface.add_socket(client_socket);
let client_handle = self.sockets.add(client_socket);
// Add handle to map
port_client_handle_map.insert(virtual_port, client_handle);
send_queue.insert(virtual_port, VecDeque::new());
let (client_socket, context) = iface.get_socket_and_context::<TcpSocket>(client_handle);
let client_socket = self.sockets.get_mut::<tcp::Socket>(client_handle);
let context = iface.context();
client_socket
.connect(
@ -209,13 +221,13 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
),
(IpAddress::from(self.source_peer_ip), virtual_port.num()),
)
.with_context(|| "Virtual server socket failed to listen")?;
.context("Virtual server socket failed to listen")?;
next_poll = None;
}
Event::ClientConnectionDropped(virtual_port) => {
if let Some(client_handle) = port_client_handle_map.get(&virtual_port) {
let client_socket = iface.get_socket::<TcpSocket>(*client_handle);
let client_socket = self.sockets.get_mut::<tcp::Socket>(*client_handle);
client_socket.close();
next_poll = None;
}
@ -226,7 +238,7 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
next_poll = None;
}
}
Event::VirtualDeviceFed(protocol) if protocol == PortProtocol::Tcp => {
Event::VirtualDeviceFed(PortProtocol::Tcp) => {
next_poll = None;
}
_ => {}
@ -236,3 +248,10 @@ impl VirtualInterfacePoll for TcpVirtualInterface {
}
}
}
const fn addr_length(addr: &IpAddress) -> u8 {
match addr.version() {
IpVersion::Ipv4 => 32,
IpVersion::Ipv6 => 128,
}
}

View file

@ -1,18 +1,23 @@
use anyhow::Context;
use std::collections::{HashMap, HashSet, VecDeque};
use std::net::IpAddr;
use crate::events::Event;
use crate::{Bus, PortProtocol};
use async_trait::async_trait;
use smoltcp::iface::{InterfaceBuilder, SocketHandle};
use smoltcp::socket::{UdpPacketMetadata, UdpSocket, UdpSocketBuffer};
use smoltcp::wire::{IpAddress, IpCidr};
use std::time::Duration;
use crate::config::PortForwardConfig;
use crate::events::Event;
use crate::virtual_device::VirtualIpDevice;
use crate::virtual_iface::{VirtualInterfacePoll, VirtualPort};
use crate::{Bus, PortProtocol};
use anyhow::Context;
use async_trait::async_trait;
use bytes::Bytes;
use smoltcp::iface::PollResult;
use smoltcp::{
iface::{Config, Interface, SocketHandle, SocketSet},
socket::udp::{self, UdpMetadata},
time::Instant,
wire::{HardwareAddress, IpAddress, IpCidr, IpVersion},
};
use std::{
collections::{HashMap, HashSet, VecDeque},
net::IpAddr,
time::Duration,
};
const MAX_PACKET: usize = 65536;
@ -20,6 +25,7 @@ pub struct UdpVirtualInterface {
source_peer_ip: IpAddr,
port_forwards: Vec<PortForwardConfig>,
bus: Bus,
sockets: SocketSet<'static>,
}
impl UdpVirtualInterface {
@ -33,44 +39,47 @@ impl UdpVirtualInterface {
.collect(),
source_peer_ip,
bus,
sockets: SocketSet::new([]),
}
}
fn new_server_socket(port_forward: PortForwardConfig) -> anyhow::Result<UdpSocket<'static>> {
static mut UDP_SERVER_RX_META: [UdpPacketMetadata; 0] = [];
fn new_server_socket(port_forward: PortForwardConfig) -> anyhow::Result<udp::Socket<'static>> {
static mut UDP_SERVER_RX_META: [udp::PacketMetadata; 0] = [];
static mut UDP_SERVER_RX_DATA: [u8; 0] = [];
static mut UDP_SERVER_TX_META: [UdpPacketMetadata; 0] = [];
static mut UDP_SERVER_TX_META: [udp::PacketMetadata; 0] = [];
static mut UDP_SERVER_TX_DATA: [u8; 0] = [];
let udp_rx_buffer = UdpSocketBuffer::new(unsafe { &mut UDP_SERVER_RX_META[..] }, unsafe {
let udp_rx_buffer =
udp::PacketBuffer::new(unsafe { &mut UDP_SERVER_RX_META[..] }, unsafe {
&mut UDP_SERVER_RX_DATA[..]
});
let udp_tx_buffer = UdpSocketBuffer::new(unsafe { &mut UDP_SERVER_TX_META[..] }, unsafe {
let udp_tx_buffer =
udp::PacketBuffer::new(unsafe { &mut UDP_SERVER_TX_META[..] }, unsafe {
&mut UDP_SERVER_TX_DATA[..]
});
let mut socket = UdpSocket::new(udp_rx_buffer, udp_tx_buffer);
let mut socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
socket
.bind((
IpAddress::from(port_forward.destination.ip()),
port_forward.destination.port(),
))
.with_context(|| "UDP virtual server socket failed to bind")?;
.context("UDP virtual server socket failed to bind")?;
Ok(socket)
}
fn new_client_socket(
source_peer_ip: IpAddr,
client_port: VirtualPort,
) -> anyhow::Result<UdpSocket<'static>> {
let rx_meta = vec![UdpPacketMetadata::EMPTY; 10];
let tx_meta = vec![UdpPacketMetadata::EMPTY; 10];
) -> anyhow::Result<udp::Socket<'static>> {
let rx_meta = vec![udp::PacketMetadata::EMPTY; 10];
let tx_meta = vec![udp::PacketMetadata::EMPTY; 10];
let rx_data = vec![0u8; MAX_PACKET];
let tx_data = vec![0u8; MAX_PACKET];
let udp_rx_buffer = UdpSocketBuffer::new(rx_meta, rx_data);
let udp_tx_buffer = UdpSocketBuffer::new(tx_meta, tx_data);
let mut socket = UdpSocket::new(udp_rx_buffer, udp_tx_buffer);
let udp_rx_buffer = udp::PacketBuffer::new(rx_meta, rx_data);
let udp_tx_buffer = udp::PacketBuffer::new(tx_meta, tx_data);
let mut socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
socket
.bind((IpAddress::from(source_peer_ip), client_port.num()))
.with_context(|| "UDP virtual client failed to bind")?;
.context("UDP virtual client failed to bind")?;
Ok(socket)
}
@ -82,26 +91,32 @@ impl UdpVirtualInterface {
}
addresses
.into_iter()
.map(|addr| IpCidr::new(addr, 32))
.map(|addr| IpCidr::new(addr, addr_length(&addr)))
.collect()
}
}
#[async_trait]
impl VirtualInterfacePoll for UdpVirtualInterface {
async fn poll_loop(self, device: VirtualIpDevice) -> anyhow::Result<()> {
async fn poll_loop(mut self, mut device: VirtualIpDevice) -> anyhow::Result<()> {
// Create CIDR block for source peer IP + each port forward IP
let addresses = self.addresses();
let config = Config::new(HardwareAddress::Ip);
// Create virtual interface (contains smoltcp state machine)
let mut iface = InterfaceBuilder::new(device, vec![])
.ip_addrs(addresses)
.finalize();
let mut iface = Interface::new(config, &mut device, Instant::now());
iface.update_ip_addrs(|ip_addrs| {
addresses.into_iter().for_each(|addr| {
ip_addrs
.push(addr)
.expect("maximum number of IPs in UDP interface reached");
});
});
// Create virtual server for each port forward
for port_forward in self.port_forwards.iter() {
let server_socket = UdpVirtualInterface::new_server_socket(*port_forward)?;
iface.add_socket(server_socket);
self.sockets.add(server_socket);
}
// The next time to poll the interface. Can be None for instant poll.
@ -114,7 +129,7 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
let mut port_client_handle_map: HashMap<VirtualPort, SocketHandle> = HashMap::new();
// Data packets to send from a virtual client
let mut send_queue: HashMap<VirtualPort, VecDeque<(PortForwardConfig, Vec<u8>)>> =
let mut send_queue: HashMap<VirtualPort, VecDeque<(PortForwardConfig, Bytes)>> =
HashMap::new();
loop {
@ -126,16 +141,12 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
} => {
let loop_start = smoltcp::time::Instant::now();
match iface.poll(loop_start) {
Ok(processed) if processed => {
trace!("UDP virtual interface polled some packets to be processed");
}
Err(e) => error!("UDP virtual interface poll error: {:?}", e),
_ => {}
if iface.poll(loop_start, &mut device, &mut self.sockets) == PollResult::SocketStateChanged {
log::trace!("UDP virtual interface polled some packets to be processed");
}
for (virtual_port, client_handle) in port_client_handle_map.iter() {
let client_socket = iface.get_socket::<UdpSocket>(*client_handle);
let client_socket = self.sockets.get_mut::<udp::Socket>(*client_handle);
if client_socket.can_send() {
if let Some(send_queue) = send_queue.get_mut(virtual_port) {
let to_transfer = send_queue.pop_front();
@ -143,7 +154,7 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
client_socket
.send_slice(
&data,
(IpAddress::from(port_forward.destination.ip()), port_forward.destination.port()).into(),
UdpMetadata::from(port_forward.destination),
)
.unwrap_or_else(|e| {
error!(
@ -158,7 +169,7 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
match client_socket.recv() {
Ok((data, _peer)) => {
if !data.is_empty() {
endpoint.send(Event::RemoteData(*virtual_port, data.to_vec()));
endpoint.send(Event::RemoteData(*virtual_port, data.to_vec().into()));
}
}
Err(e) => {
@ -171,7 +182,7 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
}
// The virtual interface determines the next time to poll (this is to reduce unnecessary polls)
next_poll = match iface.poll_delay(loop_start) {
next_poll = match iface.poll_delay(loop_start, &self.sockets) {
Some(smoltcp::time::Duration::ZERO) => None,
Some(delay) => {
trace!("UDP Virtual interface delayed next poll by {}", delay);
@ -189,7 +200,7 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
} else {
// Client socket does not exist
let client_socket = UdpVirtualInterface::new_client_socket(self.source_peer_ip, virtual_port)?;
let client_handle = iface.add_socket(client_socket);
let client_handle = self.sockets.add(client_socket);
// Add handle to map
port_client_handle_map.insert(virtual_port, client_handle);
@ -197,7 +208,7 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
}
next_poll = None;
}
Event::VirtualDeviceFed(protocol) if protocol == PortProtocol::Udp => {
Event::VirtualDeviceFed(PortProtocol::Udp) => {
next_poll = None;
}
_ => {}
@ -207,3 +218,10 @@ impl VirtualInterfacePoll for UdpVirtualInterface {
}
}
}
const fn addr_length(addr: &IpAddress) -> u8 {
match addr.version() {
IpVersion::Ipv4 => 32,
IpVersion::Ipv6 => 128,
}
}

View file

@ -1,12 +1,15 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use crate::Bus;
use anyhow::Context;
use async_recursion::async_recursion;
use boringtun::noise::errors::WireGuardError;
use boringtun::noise::{Tunn, TunnResult};
use log::Level;
use smoltcp::wire::{IpProtocol, IpVersion, Ipv4Packet, Ipv6Packet};
use tokio::net::UdpSocket;
use tokio::sync::Mutex;
use crate::config::{Config, PortProtocol};
use crate::events::Event;
@ -21,7 +24,7 @@ const MAX_PACKET: usize = 65536;
pub struct WireGuardTunnel {
pub(crate) source_peer_ip: IpAddr,
/// `boringtun` peer/tunnel implementation, used for crypto & WG protocol.
peer: Box<Tunn>,
peer: Mutex<Box<Tunn>>,
/// The UDP socket for the public WireGuard endpoint to connect to.
udp: UdpSocket,
/// The address of the public WireGuard endpoint (UDP).
@ -34,11 +37,11 @@ impl WireGuardTunnel {
/// Initialize a new WireGuard tunnel.
pub async fn new(config: &Config, bus: Bus) -> anyhow::Result<Self> {
let source_peer_ip = config.source_peer_ip;
let peer = Self::create_tunnel(config)?;
let peer = Mutex::new(Box::new(Self::create_tunnel(config)?));
let endpoint = config.endpoint_addr;
let udp = UdpSocket::bind(config.endpoint_bind_addr)
.await
.with_context(|| "Failed to create UDP socket for WireGuard connection")?;
.context("Failed to create UDP socket for WireGuard connection")?;
Ok(Self {
source_peer_ip,
@ -53,12 +56,16 @@ impl WireGuardTunnel {
pub async fn send_ip_packet(&self, packet: &[u8]) -> anyhow::Result<()> {
trace_ip_packet("Sending IP packet", packet);
let mut send_buf = [0u8; MAX_PACKET];
match self.peer.encapsulate(packet, &mut send_buf) {
let encapsulate_result = {
let mut peer = self.peer.lock().await;
peer.encapsulate(packet, &mut send_buf)
};
match encapsulate_result {
TunnResult::WriteToNetwork(packet) => {
self.udp
.send_to(packet, self.endpoint)
.await
.with_context(|| "Failed to send encrypted IP packet to WireGuard endpoint.")?;
.context("Failed to send encrypted IP packet to WireGuard endpoint.")?;
debug!(
"Sent {} bytes to WireGuard endpoint (encrypted IP packet)",
packet.len()
@ -102,7 +109,14 @@ impl WireGuardTunnel {
loop {
let mut send_buf = [0u8; MAX_PACKET];
match self.peer.update_timers(&mut send_buf) {
let tun_result = { self.peer.lock().await.update_timers(&mut send_buf) };
self.handle_routine_tun_result(tun_result).await;
}
}
#[async_recursion]
async fn handle_routine_tun_result<'a: 'async_recursion>(&self, result: TunnResult<'a>) -> () {
match result {
TunnResult::WriteToNetwork(packet) => {
debug!(
"Sending routine packet of {} bytes to WireGuard endpoint",
@ -118,6 +132,18 @@ impl WireGuardTunnel {
}
};
}
TunnResult::Err(WireGuardError::ConnectionExpired) => {
warn!("Wireguard handshake has expired!");
let mut buf = vec![0u8; MAX_PACKET];
let result = self
.peer
.lock()
.await
.format_handshake_initiation(&mut buf[..], false);
self.handle_routine_tun_result(result).await
}
TunnResult::Err(e) => {
error!(
"Failed to prepare routine packet for WireGuard endpoint: {:?}",
@ -131,8 +157,7 @@ impl WireGuardTunnel {
other => {
warn!("Unexpected WireGuard routine task state: {:?}", other);
}
}
}
};
}
/// WireGuard consumption task. Receives encrypted packets from the WireGuard endpoint,
@ -156,7 +181,11 @@ impl WireGuardTunnel {
};
let data = &recv_buf[..size];
match self.peer.decapsulate(None, data, &mut send_buf) {
let decapsulate_result = {
let mut peer = self.peer.lock().await;
peer.decapsulate(None, data, &mut send_buf)
};
match decapsulate_result {
TunnResult::WriteToNetwork(packet) => {
match self.udp.send_to(packet, self.endpoint).await {
Ok(_) => {}
@ -165,9 +194,10 @@ impl WireGuardTunnel {
continue;
}
};
let mut peer = self.peer.lock().await;
loop {
let mut send_buf = [0u8; MAX_PACKET];
match self.peer.decapsulate(None, &[], &mut send_buf) {
match peer.decapsulate(None, &[], &mut send_buf) {
TunnResult::WriteToNetwork(packet) => {
match self.udp.send_to(packet, self.endpoint).await {
Ok(_) => {}
@ -193,7 +223,7 @@ impl WireGuardTunnel {
trace_ip_packet("Received IP packet", packet);
if let Some(proto) = self.route_protocol(packet) {
endpoint.send(Event::InboundInternetPacket(proto, packet.into()));
endpoint.send(Event::InboundInternetPacket(proto, packet.to_vec().into()));
}
}
_ => {}
@ -201,17 +231,20 @@ impl WireGuardTunnel {
}
}
fn create_tunnel(config: &Config) -> anyhow::Result<Box<Tunn>> {
fn create_tunnel(config: &Config) -> anyhow::Result<Tunn> {
let private = config.private_key.as_ref().clone();
let public = *config.endpoint_public_key.as_ref();
Tunn::new(
config.private_key.clone(),
config.endpoint_public_key.clone(),
None,
private,
public,
config.preshared_key,
config.keepalive_seconds,
0,
None,
)
.map_err(|s| anyhow::anyhow!("{}", s))
.with_context(|| "Failed to initialize boringtun Tunn")
.context("Failed to initialize boringtun Tunn")
}
/// Determine the inner protocol of the incoming IP packet (TCP/UDP).
@ -220,8 +253,8 @@ impl WireGuardTunnel {
Ok(IpVersion::Ipv4) => Ipv4Packet::new_checked(&packet)
.ok()
// Only care if the packet is destined for this tunnel
.filter(|packet| Ipv4Addr::from(packet.dst_addr()) == self.source_peer_ip)
.and_then(|packet| match packet.protocol() {
.filter(|packet| packet.dst_addr() == self.source_peer_ip)
.and_then(|packet| match packet.next_header() {
IpProtocol::Tcp => Some(PortProtocol::Tcp),
IpProtocol::Udp => Some(PortProtocol::Udp),
// Unrecognized protocol, so we cannot determine where to route
@ -230,7 +263,7 @@ impl WireGuardTunnel {
Ok(IpVersion::Ipv6) => Ipv6Packet::new_checked(&packet)
.ok()
// Only care if the packet is destined for this tunnel
.filter(|packet| Ipv6Addr::from(packet.dst_addr()) == self.source_peer_ip)
.filter(|packet| packet.dst_addr() == self.source_peer_ip)
.and_then(|packet| match packet.next_header() {
IpProtocol::Tcp => Some(PortProtocol::Tcp),
IpProtocol::Udp => Some(PortProtocol::Udp),