Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Hans tunnel – copy to .env and fill values (or use: python scripts/setup_hans.py)
# Do not commit .env if it contains real secrets.
#
# One .env file configures BOTH server and client:
# - hans-server uses HANS_NETWORK, HANS_PASSPHRASE
# - hans-client uses HANS_SERVER, HANS_PASSPHRASE
# For local testing (both on same machine): set HANS_SERVER=127.0.0.1

# Shared by server and client
HANS_PASSPHRASE=your_passphrase_here
HANS_NETWORK=10.0.0.0

# Client only: server address (IP or hostname). Use 127.0.0.1 for local testing.
HANS_SERVER=127.0.0.1

# Role (server|client) – used by setup script only; docker-compose uses service names
HANS_ROLE=client
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
push:
branches: [main, develop, master]
pull_request:
branches: [main, develop, master]

jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build
run: |
make clean || true
make

- name: Check binary
run: |
test -f hans && echo "hans binary built" || (echo "hans not found"; exit 1)

build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- name: Install OpenSSL
run: brew install openssl

- name: Build
run: |
export CPPFLAGS="-I$(brew --prefix openssl)/include"
export LDFLAGS="-L$(brew --prefix openssl)/lib"
make clean || true
make

- name: Check binary
run: |
test -f hans && echo "hans binary built" || (echo "hans not found"; exit 1)
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Build
build/
hans
*.o

# Python / setup
.env
*.pyc
__pycache__/

# IDE / OS
.idea/
.vscode/
*.swp
.DS_Store
17 changes: 17 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
Fork: Modernization and features (2025)
---------------------------------------
* Metrics: app-level counters (packets_sent/received, dropped_send_fail, dropped_queue_full). Dump on SIGUSR1. Configure: see stats in syslog after kill -USR1 <pid>. Validate: run iperf3 over tunnel, send SIGUSR1, check syslog.
* Socket buffers: SO_RCVBUF/SO_SNDBUF on raw ICMP socket (default 256 KiB). Configure: -B recv,snd (e.g. -B 524288,524288). Validate: higher throughput under load.
* Batching: batch recvfrom (up to 32) when ICMP readable; non-blocking ICMP socket on Linux. Reduces syscall rate.
* Pacing: optional token bucket (-R rate_kbps). Configure: -R 80000 for 80 Mbps cap. Validate: smoother throughput, fewer drops under burst.
* Config knobs: -W max_buffered_packets (server), -B, -R. MAX_BUFFERED_PACKETS overridden by -W.
* CI: GitHub Actions build on Ubuntu and macOS.
* README: "Performance tuning on Ubuntu", "Troubleshooting packet loss", benchmark reference (docs/benchmark.md).
* IPv6: client -6 connects via AAAA; server dual-stack (IPv4 + IPv6). Tunnel payload remains IPv4. Configure: hans -c server -6. Validate: connect over IPv6, ping over tunnel.
* Docker: Dockerfile and docker-compose for server/client. Configure: HANS_SERVER, HANS_PASSPHRASE. See docs/docker.md.
* HMAC auth: handshake HMAC-SHA256 (version 2). New client sends 6-byte connection request with version=2; 32-byte HMAC response. Legacy (5-byte request, 20-byte SHA1) still supported. Configure: default on for new client. Validate: connect with new client to new server.
* MTU: -m mtu (unchanged). Docs: docs/mtu.md (typical values, path MTU discovery).
* Sequence/retransmit: TYPE_DATA_SEQ, TYPE_NACK; SEQUENCE_ENABLED in config.h. Docs: docs/sequence.md.
* Multiplexing: NUM_CHANNELS in config.h; docs/multiplexing.md.
* Congestion control: optional stub (congestion.cpp/h); off by default. Docs: README.

Release 1.1 (November 2022)
---------------------------
* Switch to utun devices on macOS (thanks to unkernet)
Expand Down
29 changes: 29 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Build stage
FROM debian:bookworm-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
make \
libssl-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src
COPY . .

RUN make clean 2>/dev/null || true
RUN make

# Runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
iproute2 \
net-tools \
iputils-ping \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=build /src/hans /usr/local/bin/hans

# Need NET_RAW for raw ICMP; NET_ADMIN for TUN
# Run as root for TUN device setup (or use --cap-add=NET_ADMIN,NET_RAW)
ENTRYPOINT ["/usr/local/bin/hans"]
28 changes: 23 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
LDFLAGS = `sh osflags ld $(MODE)`
# Capture env at parse time so we can append without recursion
ENV_CPPFLAGS := $(CPPFLAGS)
ENV_LDFLAGS := $(LDFLAGS)
LDFLAGS = `sh osflags ld $(MODE)` -lssl -lcrypto $(ENV_LDFLAGS)
CFLAGS = -c -g `sh osflags c $(MODE)`
CPPFLAGS = -c -g -std=c++98 -pedantic -Wall -Wextra -Wno-sign-compare -Wno-missing-field-initializers `sh osflags c $(MODE)`
CPPFLAGS = -c -g -std=c++98 -pedantic -Wall -Wextra -Wno-sign-compare -Wno-missing-field-initializers `sh osflags c $(MODE)` $(ENV_CPPFLAGS)
TUN_DEV_FILE = `sh osflags dev $(MODE)`
GCC = gcc
GPP = g++
Expand All @@ -16,8 +19,8 @@ build_dir:

tunemu.o: directories build/tunemu.o

hans: build/tun.o build/sha1.o build/main.o build/client.o build/server.o build/auth.o build/worker.o build/time.o build/tun_dev.o build/echo.o build/exception.o build/utility.o
$(GPP) -o hans build/tun.o build/sha1.o build/main.o build/client.o build/server.o build/auth.o build/worker.o build/time.o build/tun_dev.o build/echo.o build/exception.o build/utility.o $(LDFLAGS)
hans: build/tun.o build/sha1.o build/main.o build/client.o build/server.o build/auth.o build/worker.o build/time.o build/stats.o build/pacer.o build/tun_dev.o build/echo.o build/echo6.o build/hmac.o build/congestion.o build/exception.o build/utility.o
$(GPP) -o hans build/tun.o build/sha1.o build/main.o build/client.o build/server.o build/auth.o build/worker.o build/time.o build/stats.o build/pacer.o build/tun_dev.o build/echo.o build/echo6.o build/hmac.o build/congestion.o build/exception.o build/utility.o $(LDFLAGS)

build/utility.o: src/utility.cpp src/utility.h
$(GPP) -c src/utility.cpp -o $@ -o $@ $(CPPFLAGS)
Expand All @@ -28,6 +31,15 @@ build/exception.o: src/exception.cpp src/exception.h
build/echo.o: src/echo.cpp src/echo.h src/exception.h
$(GPP) -c src/echo.cpp -o $@ $(CPPFLAGS)

build/echo6.o: src/echo6.cpp src/echo6.h src/exception.h
$(GPP) -c src/echo6.cpp -o $@ $(CPPFLAGS)

build/hmac.o: src/hmac.cpp src/hmac.h
$(GPP) -c src/hmac.cpp -o $@ $(CPPFLAGS)

build/congestion.o: src/congestion.cpp src/congestion.h src/time.h
$(GPP) -c src/congestion.cpp -o $@ $(CPPFLAGS)

build/tun.o: src/tun.cpp src/tun.h src/exception.h src/utility.h src/tun_dev.h
$(GPP) -c src/tun.cpp -o $@ $(CPPFLAGS)

Expand All @@ -49,12 +61,18 @@ build/server.o: src/server.cpp src/server.h src/client.h src/utility.h src/confi
build/auth.o: src/auth.cpp src/auth.h src/sha1.h src/utility.h
$(GPP) -c src/auth.cpp -o $@ $(CPPFLAGS)

build/worker.o: src/worker.cpp src/worker.h src/tun.h src/exception.h src/time.h src/echo.h src/tun_dev.h src/config.h
build/worker.o: src/worker.cpp src/worker.h src/tun.h src/exception.h src/time.h src/echo.h src/stats.h src/pacer.h src/tun_dev.h src/config.h
$(GPP) -c src/worker.cpp -o $@ $(CPPFLAGS)

build/time.o: src/time.cpp src/time.h
$(GPP) -c src/time.cpp -o $@ $(CPPFLAGS)

build/stats.o: src/stats.cpp src/stats.h
$(GPP) -c src/stats.cpp -o $@ $(CPPFLAGS)

build/pacer.o: src/pacer.cpp src/pacer.h src/time.h
$(GPP) -c src/pacer.cpp -o $@ $(CPPFLAGS)

clean:
rm -rf build hans

Expand Down
184 changes: 183 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,188 @@
Hans - IP over ICMP
===================

Hans makes it possible to tunnel IPv4 through ICMP echo packets, so you could call it a ping tunnel. This can be useful when you find yourself in the situation that your Internet access is firewalled, but pings are allowed.
Hans tunnels IPv4 through ICMP echo packets (a “ping tunnel”). Useful when Internet access is firewalled but pings are allowed.

http://code.gerade.org/hans/

## Quick start

**Native (Linux):**

```bash
make
# Server (one host)
sudo ./hans -s 10.0.0.0 -p PASSPHRASE -f -d tun0
# Client (another host, or same for local test)
sudo ./hans -c SERVER_IP -p PASSPHRASE -f -d tun1
```

**Docker:** See [docs/docker.md](docs/docker.md). Build once, then run server and/or client separately:

```bash
docker compose build
docker compose up hans-server -d # server only
docker compose up hans-client -d # client only (set HANS_SERVER in .env)
```

## Command-line options

| Option | Description |
|--------|-------------|
| **Mode** | |
| `-c server` | Run as **client**. Connect to given server (IP or hostname). |
| `-s network` | Run as **server**. Use given network on tunnel (e.g. `10.0.0.0` → 10.0.0.0/24). |
| **Auth & identity** | |
| `-p passphrase` | Passphrase (required). |
| `-u username` | Drop privileges to this user after setup. |
| `-a ip` | (Client) Request this tunnel IP from the server. |
| **Tunnel** | |
| `-d device` | TUN device name (e.g. `tun0`, `tun1`). |
| `-m mtu` | MTU / max echo size (default 1500). Same on client and server. See [docs/mtu.md](docs/mtu.md). |
| **Server only** | |
| `-r` | Respond to ordinary pings in server mode. |
| **Client only** | |
| `-w polls` | Number of echo requests sent in advance (default 10). 0 disables polling. |
| `-i` | Change echo ID on every request (may help buggy routers). |
| `-q` | Change echo sequence on every request (may help buggy routers). |
| **Performance** | |
| `-B recv,snd` | Socket buffer sizes in bytes (e.g. `262144,262144`). Default 256 KiB each. |
| `-R rate` | Pacing: max send rate in Kbps (0 = disabled). |
| `-W packets` | (Server) Max buffered packets per client (default 20). |
| **IPv6** | |
| `-6` | (Client) Use IPv6 to reach server (AAAA / ICMPv6). |
| **Other** | |
| `-f` | Foreground (do not daemonize). |
| `-v` | Verbose / debug. |
| **Signals** | |
| `SIGUSR1` | Dump packet stats to syslog. |

**Examples:**

```bash
# Server
hans -s 10.0.0.0 -p mypass -f -d tun0
hans -s 10.0.0.0 -p mypass -f -d tun0 -W 64 -B 524288,524288

# Client (IPv4)
hans -c 192.168.1.100 -p mypass -f -d tun1
hans -c 192.168.1.100 -p mypass -f -d tun1 -w 20 -R 80000

# Client (IPv6)
hans -c server.example.com -6 -p mypass -f -d tun1
```

## Running server and client separately

- **Native:** Run the server on one host and the client on another (or same host for local test). No shared config; pass the same passphrase and ensure MTU matches.
- **Docker Compose:** Run only the service you need:
- `docker compose up hans-server -d` — server only
- `docker compose up hans-client -d` — client only (set `HANS_SERVER` in `.env` to the server’s IP)
- **Docker (no Compose):** Use `docker run` with `--cap-add=NET_RAW --cap-add=NET_ADMIN --device=/dev/net/tun --network=host`. Full examples: [docs/docker.md](docs/docker.md#running-server-and-client-separately).

## IPv4 and IPv6

- **IPv4 (default):** Client uses `-c SERVER_IP` (no `-6`). Server listens on IPv4; tunnel works over ICMP (IPv4).
- **IPv6:** Client uses `-6` and the server’s IPv6 address or hostname. The **control channel** (POLLs, DATA) then uses ICMPv6; the **tunnel payload** is still IPv4 (10.0.0.0/24). Server is dual-stack and accepts both IPv4 and IPv6 clients.

**How to test IPv6**

1. **Prerequisites:** Server host has a routable IPv6 address; ICMPv6 is allowed between client and server (firewall / security group).
2. **Native:** Start server as usual. Start client with `-6` and server IPv6 or hostname (with AAAA):
```bash
hans -c 2001:db8::1 -6 -p mypass -f -d tun1
# or: hans -c server.example.com -6 -p mypass -f -d tun1
```
3. **Docker:** Compose does not pass `-6` by default. Run the client with plain `docker run` and `-6`:
```bash
docker run -d --name hans-client \
--cap-add=NET_RAW --cap-add=NET_ADMIN \
--device=/dev/net/tun --network=host \
hans:latest -c SERVER_IPV6 -6 -p YOUR_PASSPHRASE -f -d tun1
```
4. **Verify:** Same as IPv4 — ping and iperf3 over the tunnel (e.g. `ping 10.0.0.100`, `iperf3 -c 10.0.0.100`). Tunnel addresses stay IPv4.

More: [docs/docker.md](docs/docker.md#testing-ipv4-vs-ipv6).

For **VPN / many users**: fairness and bandwidth are both important. See [docs/fairness-and-bandwidth.md](docs/fairness-and-bandwidth.md) for why throughput is limited (~137 Mbits/sec vs 1.6 Gbit/s), per-flow fairness (round-robin), tuning (`-w`/`-W`), and multiplexing/QUIC/KCP.

## Performance tuning on Ubuntu

- **Socket buffers:** Use `-B recv,snd` (bytes). Default is 256 KiB each. For higher throughput (e.g. 80+ Mbps), try `-B 524288,524288` (512 KiB). If you use larger values, raise system limits first:
```bash
sudo sysctl -w net.core.rmem_max=1048576
sudo sysctl -w net.core.wmem_max=1048576
```
- **Pacing:** Use `-R rate_kbps` to cap send rate and smooth bursts (e.g. `-R 80000` for 80 Mbps). Helps avoid kernel or middlebox drops under burst.
- **Server queue:** Use `-W packets` (server only) to allow more buffered packets per client. Default 20; increase (e.g. `-W 64`) if you see `dropped_queue_full` in stats.
- **Stats:** Send `SIGUSR1` to the hans process to dump packet counters to syslog: `kill -USR1 <pid>`.
- **ulimit:** If you run many FDs later (e.g. multiplexing), ensure `ulimit -n` is sufficient.
- **NIC offloads:** Leave on unless you are debugging; disabling can increase CPU use.

## Optional congestion control

A congestion module (see [src/congestion.h](src/congestion.h)) is provided as a stub: it can report sent bytes, loss, and RTT. When fully wired, it would drive pacing or rate (e.g. AIMD or token bucket with feedback). Off by default; enable via config or future `-C` option.

## Docker

Build once, then run **server and/or client separately** as needed. Full instructions: [docs/docker.md](docs/docker.md).

```bash
docker compose build
# Server only
docker compose up hans-server -d
# Client only (set HANS_SERVER in .env to server’s IP)
docker compose up hans-client -d
# Or both (e.g. local test)
docker compose up -d
```

Containers need `NET_RAW`, `NET_ADMIN`, `--device=/dev/net/tun`, and `--network=host`. See [docs/docker.md](docs/docker.md) for plain `docker run` examples and WSL notes.

## Authentication

- **Legacy (SHA1):** Old clients send a 5-byte connection request; server expects 20-byte SHA1 challenge response. Still supported.
- **HMAC-SHA256:** New clients send a 6-byte connection request with version 2; server expects 32-byte HMAC-SHA256(challenge) response. Enabled by default for new builds. Backward compatible with legacy servers (server accepts both 5- and 6-byte requests).

## IPv6 support

- **Client:** Use `-6` to connect to the server via IPv6 (ICMPv6). Server can be specified by IPv6 address or hostname (AAAA). Without `-6`, the client uses IPv4 (A record).
- **Server:** Listens on both IPv4 and IPv6 by default; accepts clients from either. Tunnel payload is still IPv4 (TUN device carries IPv4). On environments where the kernel does not support `IPV6_CHECKSUM` (e.g. some WSL/Docker setups), hans uses a userspace ICMPv6 checksum.

## Troubleshooting packet loss

1. **Check app-level counters**
Send `SIGUSR1` to the hans process and check syslog for `stats: ... dropped_send_fail=... dropped_queue_full=...`.
- High `dropped_send_fail`: kernel send buffer or pacing; increase `-B` or reduce rate.
- High `dropped_queue_full`: server has no poll ids (client not sending POLLs fast enough); increase `-W` or client `-w` (polls in advance).

2. **Kernel socket buffers**
Default raw ICMP buffers may be small. Use `-B recv,snd` and raise `net.core.rmem_max` / `net.core.wmem_max` if needed.

3. **Queue full (server→client heavy)**
Server can only send when the client has sent a POLL. If traffic is mostly server→client, increase client `-w` (e.g. 20) and server `-W` (e.g. 64).

4. **Pacing**
Enable `-R rate_kbps` to smooth bursts and avoid middlebox/kernel drops.

5. **MTU**
Use `-m mtu` to match path MTU (default 1500). If path MTU is smaller, reduce `-m` to avoid fragmentation. See [docs/mtu.md](docs/mtu.md) for typical values and path MTU discovery.

6. **Reproduce with netem**
Use `tc qdisc add dev eth0 root netem delay 20ms loss 1%` to simulate loss; run iperf3 over the tunnel and compare stats before/after. See [docs/benchmark.md](docs/benchmark.md).

## Changes and features (this fork)

Summary of additions and edits; see [CHANGES](CHANGES) for details.

- **Metrics:** Packet/byte counters and drop reasons; dump on `SIGUSR1`.
- **Socket buffers:** Configurable `-B recv,snd`; default 256 KiB.
- **Batching:** Batch receive on ICMP socket to reduce syscalls.
- **Pacing:** Optional `-R rate_kbps` token bucket.
- **Server queue:** `-W packets` (server); default 20.
- **IPv6:** Client `-6`; server dual-stack (IPv4 + IPv6). Userspace ICMPv6 checksum fallback when `IPV6_CHECKSUM` is unsupported (e.g. WSL/Docker).
- **Docker:** Dockerfile and docker-compose; run server and client separately; see [docs/docker.md](docs/docker.md).
- **Auth:** HMAC-SHA256 (version 2) with legacy SHA1 support.
- **MTU:** `-m mtu`; [docs/mtu.md](docs/mtu.md).
- **Multiplexing:** NUM_CHANNELS (default 4) with per-channel POLL queues; client sends maxPolls×num_channels POLLs for higher in-flight capacity and throughput. See [docs/multiplexing.md](docs/multiplexing.md).
- **Stubs/docs:** Sequence/retransmit ([docs/sequence.md](docs/sequence.md)), congestion ([src/congestion.h](src/congestion.h)).
Loading