diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5a6baa6 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7bd8e85 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b934b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Build +build/ +hans +*.o + +# Python / setup +.env +*.pyc +__pycache__/ + +# IDE / OS +.idea/ +.vscode/ +*.swp +.DS_Store diff --git a/CHANGES b/CHANGES index 5ffd93d..027fbe8 100644 --- a/CHANGES +++ b/CHANGES @@ -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 . 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) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2db849a --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile index 332f3d4..ed1b3b0 100644 --- a/Makefile +++ b/Makefile @@ -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++ @@ -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) @@ -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) @@ -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 diff --git a/README.md b/README.md index 9ab9e2e..f203f6e 100644 --- a/README.md +++ b/README.md @@ -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 `. +- **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)). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d9c13cb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +version: "3.8" + +services: + hans-server: + build: . + image: hans:latest + container_name: hans-server + cap_add: + - NET_RAW + - NET_ADMIN + devices: + - /dev/net/tun + network_mode: host + # Server: -s network -p passphrase -f; -B/-W for throughput (see docs/benchmark.md) + command: ["-s", "${HANS_NETWORK:-10.0.0.0}", "-p", "${HANS_PASSPHRASE:-passphrase}", "-f", "-d", "tun0", "-B", "524288,524288", "-W", "128"] + environment: + - HANS_PASSPHRASE=${HANS_PASSPHRASE:-passphrase} + - HANS_NETWORK=${HANS_NETWORK:-10.0.0.0} + # Optional: mount config or use env + restart: "no" + # Healthcheck: ping over tunnel (client IP 10.0.0.100) if client is up + # healthcheck: + # test: ["CMD", "ping", "-c", "1", "10.0.0.100"] + # interval: 30s + # timeout: 5s + # retries: 2 + # start_period: 10s + + hans-client: + build: . + image: hans:latest + container_name: hans-client + cap_add: + - NET_RAW + - NET_ADMIN + devices: + - /dev/net/tun + network_mode: host + # Client: -c server -p passphrase -f; -B/-w for throughput (see docs/benchmark.md) + command: ["-c", "${HANS_SERVER:-127.0.0.1}", "-p", "${HANS_PASSPHRASE:-passphrase}", "-f", "-d", "tun1", "-B", "524288,524288", "-w", "64"] + environment: + - HANS_SERVER=${HANS_SERVER:-127.0.0.1} + - HANS_PASSPHRASE=${HANS_PASSPHRASE:-passphrase} + - HANS_NETWORK=${HANS_NETWORK:-10.0.0.0} + # No depends_on: server and client run on different hosts (different VPS) + restart: "no" diff --git a/docs/benchmark.md b/docs/benchmark.md new file mode 100644 index 0000000..0da1b9a --- /dev/null +++ b/docs/benchmark.md @@ -0,0 +1,84 @@ +# Benchmark and packet loss reproduction + +This document describes how to reproduce and measure tunnel throughput and packet loss, and how to capture app-level and OS-level counters. + +## Prerequisites + +- Two Ubuntu machines or containers (server and client) +- `hans` built and installed (or run from build directory) +- `iperf3` installed on both ends +- Optional: `tc` (netem) for adding latency/loss; `netstat` or `ss` for OS stats + +## Basic throughput test + +1. **Start server** (as root or with CAP_NET_RAW): + ```bash + ./hans -s 10.0.0.0 -p passphrase -f -B 524288,524288 + ``` + Tunnel network will be 10.0.0.0/24; server tunnel IP 10.0.0.1, client will get e.g. 10.0.0.100. + +2. **Start client** (pointing at server’s real IP): + ```bash + ./hans -c -p passphrase -f -B 524288,524288 + ``` + +3. **Run iperf3** over the tunnel (use the client’s *tunnel* IP, e.g. 10.0.0.100): + - On server: `iperf3 -s` + - On client (or another host that can reach the tunnel): `iperf3 -c 10.0.0.100 -t 120 -P 4` + - For **more even throughput** and fewer 0 KB/s streams, use **-P 2** or **-P 4** (see “Why 0 KB/s?” below). Single flow: `iperf3 -c 10.0.0.100 -t 120` (no -P). + - For UDP at ~80 Mbps: `iperf3 -u -c 10.0.0.100 -b 80M -t 120` + +4. **Dump app stats** (on server or client process): + ```bash + kill -USR1 + ``` + Check syslog for lines like: + `stats: packets_sent=... packets_received=... bytes_sent=... bytes_received=... dropped_send_fail=... dropped_queue_full=...` + +## With artificial loss (netem) + +On the **server** or **client** host (on the interface used for ICMP): + +```bash +# Add 20 ms delay and 1% loss +sudo tc qdisc add dev eth0 root netem delay 20ms loss 1% + +# Run iperf3 as above, then inspect stats (SIGUSR1) and iperf3 loss report + +# Remove netem +sudo tc qdisc del dev eth0 root +``` + +## OS-level counters + +- **ICMP stats:** `netstat -s` (ICMP section) or `cat /proc/net/snmp` (Ip: InReceives, etc.). +- **Socket buffers:** After increasing with `-B`, you may need to raise system limits: + ```bash + # Optional, if -B values larger than default max + sudo sysctl -w net.core.rmem_max=1048576 + sudo sysctl -w net.core.wmem_max=1048576 + ``` + +## Performance tuning options + +- **Socket buffers:** `-B recv,snd` (e.g. `-B 524288,524288` for 512 KB). +- **Pacing:** `-R rate_kbps` to cap send rate (e.g. `-R 80000` for 80 Mbps). +- **Server queue:** `-W packets` (e.g. `-W 64`) to allow more buffered packets per client. + +## Why do I see 0 KB/s on some iperf3 streams? + +The tunnel is **one logical pipe** per client: the server has a single FIFO queue of packets to that client and sends **one packet per POLL reply**. With **8 parallel streams** (`-P 8`), all streams share that one queue. The kernel delivers segments from all TCP connections into the TUN; one connection often gets many segments in a row. So one stream’s packets can sit at the front of the queue and get most of the send slots, while others get almost none → you see one stream at ~40–50 Mbits/sec and several at 0 KB/s. + +**What we do:** The default **recv batch size is 1** (one ICMP packet per `select()`), matching original hans / petrich/hans. That keeps processing interleaved across flows and avoids extreme 0 KB/s and “control socket has closed unexpectedly”. If you build with `HANS_RECV_BATCH_MAX=32` in `config.h` (or `-DHANS_RECV_BATCH_MAX=32`), throughput can increase but fairness drops and you may see 0 KB/s and iperf3 control-socket timeouts again. + +**What you can do:** + +- Use **fewer parallel streams**: `iperf3 -c 10.0.0.100 -t 30 -P 2` or `-P 4` for more even distribution. +- Or a **single flow**: `iperf3 -c 10.0.0.100 -t 30` (no `-P`) for one stream and predictable throughput. + +Total bandwidth (SUM) is similar; with -P 2 or -P 4 the per-stream rates are less extreme. + +## Interpreting stats + +- **dropped_send_fail:** `sendto()` failed or pacing denied send; increase socket buffers or reduce rate. +- **dropped_queue_full:** Server had no poll id and pending queue was full; increase `-W` or ensure client sends POLLs (e.g. use `-w 10` or higher). diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..8f49f27 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,177 @@ +# Running Hans in Docker + +## Env and Python setup + +**Option A – copy example and edit:** Copy [.env.example](../.env.example) to `.env` and set `HANS_PASSPHRASE`, `HANS_SERVER` (client), `HANS_NETWORK` (server). + +**Option B – interactive script:** From the repo root, run the setup script to fill passphrase, server address, and tunnel network; it writes `.env` for docker-compose: + +```bash +python scripts/setup_hans.py +``` + +Use `1`/`s` for server, `2`/`c` for client; you can choose to generate a random passphrase. Then build and run as below. See [scripts/README.md](scripts/README.md) for all options. + +### Local testing (server and client on the same machine) + +You use **one** `.env` file for both services. Docker Compose loads `.env` once; the server uses `HANS_NETWORK` and `HANS_PASSPHRASE`, the client uses `HANS_SERVER` and `HANS_PASSPHRASE`. + +1. Copy `.env.example` to `.env` (or run `python scripts/setup_hans.py` and pick server then client if you want both in one go). +2. Set `HANS_SERVER=127.0.0.1` so the client talks to the server on localhost. +3. Start both: + + ```bash + docker compose up -d + ``` + + That starts `hans-server` and `hans-client`; no need for two `.env` files or separate configs. + +## Build + +```bash +docker compose build +# or +docker build -t hans:latest . +``` + +## Throughput (iperf3 / bandwidth) + +Compose is configured for better throughput: **server** uses `-B 524288,524288` (512 KiB socket buffers) and `-W 64` (server queue); **client** uses `-B 524288,524288` and `-w 20` (polls in advance). That should reduce retransmissions and improve bitrate compared to defaults. + +On each **VPS host** (before or after starting containers), raise kernel socket limits so the larger buffers take effect: + +```bash +sudo sysctl -w net.core.rmem_max=1048576 +sudo sysctl -w net.core.wmem_max=1048576 +``` + +To make these persistent: add the same lines to `/etc/sysctl.conf` or a file under `/etc/sysctl.d/`. See [docs/benchmark.md](benchmark.md) for iperf3 test steps and tuning. + +## Running server and client separately + +You can run **only the server**, **only the client**, or **both** on different hosts. + +### With Docker Compose + +- **Server only** (from repo root; uses `.env` for `HANS_NETWORK`, `HANS_PASSPHRASE`): + ```bash + docker compose up hans-server -d + ``` + +- **Client only** (ensure `.env` has `HANS_SERVER` set to the server’s real IP or hostname): + ```bash + docker compose up hans-client -d + ``` + +- **Both** (e.g. local test): + ```bash + docker compose up -d + ``` + +To stop only one service: + +```bash +docker compose stop hans-server +# or +docker compose stop hans-client +``` + +### With plain Docker (no Compose) + +Use these when you are not using Compose or when running server and client on different machines. Replace `hans:latest` if you use another image name. + +**Server only (IPv4):** + +```bash +docker run -d --name hans-server \ + --cap-add=NET_RAW --cap-add=NET_ADMIN \ + --device=/dev/net/tun --network=host \ + hans:latest -s 10.0.0.0 -p YOUR_PASSPHRASE -f -d tun0 +``` + +**Client only (IPv4):** + +```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_IP -p YOUR_PASSPHRASE -f -d tun1 +``` + +**Client only (IPv6):** use `-6` and the server’s IPv6 address or hostname: + +```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_OR_HOSTNAME -6 -p YOUR_PASSPHRASE -f -d tun1 +``` + +Add options as needed (e.g. `-B 524288,524288`, `-R 80000`, `-w 20`, `-m 1500`). See [README](../README.md) for all options. + +## Server (reference) + +Run the server (listens on host network for ICMP): + +```bash +docker compose up hans-server -d +# Or with custom passphrase and device (plain docker): +docker run --rm -d --name hans-server \ + --cap-add=NET_RAW --cap-add=NET_ADMIN --device=/dev/net/tun --network=host \ + hans:latest -s 10.0.0.0 -p mypass -f -d tun0 +``` + +The server uses network `10.0.0.0/24` by default; tunnel device is `tun0` (or set via `-d`). Server listens on both IPv4 and IPv6 by default. + +## Client (reference) + +Run the client (connects to server’s real IP): + +```bash +# With Compose (set HANS_SERVER in .env first) +docker compose up hans-client -d + +# Plain docker – IPv4 +docker run --rm -d --name hans-client \ + --cap-add=NET_RAW --cap-add=NET_ADMIN --device=/dev/net/tun --network=host \ + hans:latest -c 192.168.1.100 -p mypass -f -d tun1 + +# Plain docker – IPv6 +docker run --rm -d --name hans-client \ + --cap-add=NET_RAW --cap-add=NET_ADMIN --device=/dev/net/tun --network=host \ + hans:latest -c 2001:db8::1 -6 -p mypass -f -d tun1 +``` + +## Requirements + +- **Capabilities:** `NET_RAW` (raw ICMP) and `NET_ADMIN` (TUN device). +- **TUN device:** The container must have access to `/dev/net/tun`. The compose file passes it from the host via `devices: - /dev/net/tun`. If you see *"could not create tunnel device: No such file or directory"*, the host has no TUN device (see below). +- **Network:** `network_mode: host` is used so the container shares the host network and can send/receive ICMP and create TUN devices. For non-host networking you would need to expose ICMP (not typical) and handle TUN differently. + +### WSL (Windows Subsystem for Linux) + +When running Docker from WSL2: + +1. **Docker Desktop (WSL2 backend):** Containers run in Docker’s Linux VM, not inside WSL. That VM must have `/dev/net/tun`. If the error persists, try running hans **directly in WSL** (no Docker): install build deps and run `./hans` so it uses WSL’s kernel, which usually has TUN. +2. **Docker Engine inside WSL2:** The “host” is WSL2. Check that TUN exists: + ```bash + ls -l /dev/net/tun + ``` + If it’s missing, load the module (if built as module): `sudo modprobe tun`. On some WSL2 kernels TUN is built-in and the node should exist; if not, you may need a WSL2 kernel with `CONFIG_TUN=m` or `=y`. +3. **Run without Docker:** From the repo in WSL, build with `make` and run e.g. `sudo ./hans -c -p -f -d tun1`. That uses WSL’s `/dev/net/tun` and often works when Docker doesn’t. + +## Testing IPv4 vs IPv6 + +- **IPv4 (default):** Compose client uses `HANS_SERVER` (e.g. `192.168.1.100` or `127.0.0.1`). No `-6` flag. Server and client communicate over IPv4. +- **IPv6:** Run the client with `-6` and the server’s IPv6 address. Compose does not pass `-6` by default; use plain `docker run` for the client with `-6` and `-c ` (see “Client only (IPv6)” above). Ensure the server host has an IPv6 address and that ICMPv6 is allowed. + +To test IPv4 only: start server and client without `-6`; ping over the tunnel (e.g. client gets `10.0.0.100`). +To test IPv6 only: start server, then start client with `-6 -c `; ping over the tunnel the same way (tunnel payload is still IPv4). + +## Healthcheck + +To enable a simple healthcheck (ping over tunnel), uncomment the `healthcheck` block in `docker-compose.yml` for `hans-server` and ensure the client is up with tunnel IP `10.0.0.100` (or adjust the ping target). + +## Plugging tunnel into host or other containers + +With `network_mode: host`, the TUN device is created on the host. Routes and firewall rules on the host apply. To give another container access you would typically run without host network and use a different strategy (e.g. proxy or shared network namespace). diff --git a/docs/fairness-and-bandwidth.md b/docs/fairness-and-bandwidth.md new file mode 100644 index 0000000..2ebeca6 --- /dev/null +++ b/docs/fairness-and-bandwidth.md @@ -0,0 +1,68 @@ +# Fairness and bandwidth (VPN / many users) + +This doc explains why throughput and fairness look the way they do, and what helps to get closer to your server’s capacity (e.g. 1.6 Gbit/s) and fair shares across users/streams. + +## Why throughput is limited (~137 Mbits/sec vs 1.6 Gbit/s) + +The hans tunnel is **POLL-based**: the client sends ICMP echo requests (POLLs), and the server can send **one data packet per POLL reply**. So: + +- **In-flight packets** = number of POLLs the client has sent that the server hasn’t yet “used” for a reply. +- **Throughput** ≈ (in-flight packets) × (packet size) / RTT. + +With `-w 20` (20 polls in advance) and RTT ~20 ms and 1500-byte packets: +20 × 1500 × 8 / 0.02 ≈ **12 Mbits/sec** per “channel”. To reach 1.6 Gbit/s you’d need on the order of **100+ in-flight packets** or **multiple parallel channels** (multiplexing). + +**What we already do:** Compose uses `-w 20` and `-W 64`. Increasing `-w` (e.g. 64 or 100) and `-W` (e.g. 128 or 200) gives more in-flight packets and can push throughput higher (and reduce retransmissions). Try: + +- Client: `-w 64` or `-w 100` +- Server: `-W 128` or `-W 200` + +Raise kernel socket limits (`net.core.rmem_max`, `net.core.wmem_max`) if you use larger `-B`. + +## Why one stream gets 51 MB and another 7 MB (fairness) + +There is **one FIFO queue per client**. All flows (e.g. 8 iperf3 streams) share that queue. Whichever flow’s packets sit at the front more often gets most of the send slots → uneven rates (51 MB vs 7 MB). + +**What we do:** The server can use **per-flow queues** and **round-robin** when sending: parse the inner IP packet (5-tuple: src/dst IP, protocol, src/dst port), hash to a flow id, maintain one queue per flow (e.g. 16), and when a POLL arrives send from the next non-empty queue in round-robin order. That spreads send slots across flows and improves fairness (see [Per-flow fairness](#per-flow-fairness) below). + +## Do QUIC, KCP, multiplexing help? + +- **QUIC / KCP** – These are **transport** protocols. They run **over** the tunnel (your users’ traffic). Hans tunnels IP; it doesn’t replace TCP with QUIC inside the tunnel. So QUIC/KCP over the VPN can help with loss recovery and multi-stream behavior for that traffic; they don’t increase the tunnel’s raw capacity. See [Borrowing from QUIC/KCP](#borrowing-from-quickcp) below for using their *ideas* inside the tunnel. + +- **Multiplexing** – **Yes, implemented.** Multiple logical “channels” (POLL/reply streams) multiply in-flight packets and throughput. Default **NUM_CHANNELS=4**; the server assigns POLLs to channels by `id % NUM_CHANNELS` and sends round-robin. The client sends **maxPolls × num_channels** POLLs (num_channels in CONNECTION_ACCEPT). See [docs/multiplexing.md](multiplexing.md). + +- **Per-flow fairness** – Implemented: round-robin across flow queues so multiple streams/users get more equal shares (see below). + +## Borrowing from QUIC/KCP + +We can adopt the **infrastructure and logic** of QUIC/KCP in our ICMP tunnel without running QUIC/KCP as the transport: + +- **Multiplexing** – Multiple logical streams/channels over one “connection” (like QUIC streams). **Done:** NUM_CHANNELS and per-channel POLL queues; client sends more POLLs when server advertises more channels. +- **Fast retransmit / NACK** – Detect loss and retransmit without waiting for TCP RTO. **Stub:** TYPE_DATA_SEQ and TYPE_NACK in the protocol; see [docs/sequence.md](sequence.md). Full implementation would track per-packet sequence and NACK from the receiver. +- **Congestion control** – Adjust send rate from loss/RTT (e.g. AIMD or BBR-like). **Stub:** [src/congestion.h](src/congestion.h); not yet wired into the send path. Would drive pacing (-R) or per-channel rate. +- **Connection ID** – QUIC’s connection ID for migration; less relevant for a fixed client↔server tunnel. + +So: we don’t run QUIC/KCP inside hans, but we can (and do) use **multiplexing**; **sequence/NACK** and **congestion control** are the next logical steps if you want QUIC/KCP-style behavior on top of ICMP. + +## Per-flow fairness + +When **per-flow fairness** is enabled (default in this fork), the server: + +1. Parses the inner IP packet (IPv4 header; for TCP/UDP uses 5-tuple, else 3-tuple). +2. Hashes to a flow index (0 … N−1, N = 16 by default). +3. Keeps **N queues per client** (one per flow). +4. When a POLL arrives, sends from the **next non-empty queue in round-robin order** instead of always from the front of a single queue. + +So multiple TCP streams (or multiple users’ traffic) share the tunnel more fairly. You should see less disparity (e.g. no 51 MB vs 7 MB) and more even per-stream rates. + +Config: `HANS_NUM_FLOW_QUEUES` in [src/config.h](src/config.h) (default 16). Set to **1** to disable (single FIFO, original behavior). Rebuild after changing. + +## Roadmap (short → long term) + +1. **Tuning (now)** – Increase `-w` and `-W` in Compose or CLI for more in-flight packets and higher throughput. See [docs/docker.md](docker.md) and [docs/benchmark.md](benchmark.md). +2. **Per-flow fairness (done)** – Round-robin across flow queues for fairer multi-stream / multi-user behavior. +3. **Multiplexing (done)** – NUM_CHANNELS (default 4), per-channel POLL queues, client sends maxPolls×num_channels POLLs. Scale toward 1.6 Gbit/s with higher `-w` and NUM_CHANNELS=4 or 8. See [docs/multiplexing.md](multiplexing.md). +4. **Sequence / NACK (stub)** – Per-packet sequence and NACK-based retransmit; see [docs/sequence.md](sequence.md). +5. **Congestion control (stub)** – Wire [src/congestion.h](src/congestion.h) into send path for QUIC/KCP-style rate adaptation. + +For a **VPN with many users**, use per-flow fairness, multiplexing (NUM_CHANNELS=4), and higher `-w`/`-W` for both fairness and bandwidth. diff --git a/docs/mtu.md b/docs/mtu.md new file mode 100644 index 0000000..62431c6 --- /dev/null +++ b/docs/mtu.md @@ -0,0 +1,38 @@ +# MTU handling + +## Setting MTU + +Use `-m mtu` to set the maximum echo packet size (the MTU of the path between client and server). This must match on both ends. Default is 1500 (Ethernet). + +Example: +```bash +hans -c server -p passphrase -m 1500 +hans -s 10.0.0.0 -p passphrase -m 1500 +``` + +The program subtracts the ICMP + tunnel header overhead from `-m` to get the tunnel payload size. So with `-m 1500`, the tunnel payload is about 1500 - 28 (IP+ICMP) - 5 (TunnelHeader) = 1467 bytes. + +## Typical values + +- **1500** – Ethernet, most networks. +- **1492** – PPPoE. +- **9000** – Jumbo frames (if path supports it). + +## Path MTU discovery + +ICMP echo does not fragment; if the payload is larger than the path MTU, the packet may be dropped or you may get ICMP "fragmentation needed". To discover path MTU: + +1. Use `ping -M do -s ` (Linux) to find the largest size that works without fragmentation. +2. Set `-m` to that size (or slightly smaller to be safe). + +Example: +```bash +ping -M do -s 1472 192.168.1.1 # 1472 + 28 (IP+ICMP) = 1500 +``` + +If you see "Frag needed" or loss, reduce the size. + +## Troubleshooting + +- **Symptoms of MTU too large:** Packet loss, especially for larger transfers; connections that stall. +- **Fix:** Reduce `-m` (e.g. try 1400 or 1280) so the full packet fits the path MTU. diff --git a/docs/multiplexing.md b/docs/multiplexing.md new file mode 100644 index 0000000..e962561 --- /dev/null +++ b/docs/multiplexing.md @@ -0,0 +1,28 @@ +# Multiplexing for higher bandwidth + +Multiple logical **channels** (POLL/reply streams) increase in-flight capacity and throughput. Each channel has its own POLL queue on the server; the server sends data round-robin across channels. + +## How it works + +- **Server:** Assigns each incoming POLL to a channel by `channel = echoId % NUM_CHANNELS`. Keeps one POLL queue per channel per client. When sending data, takes the next POLL from channels in round-robin order (`getNextPollFromChannels`). +- **Client:** Receives `num_channels` (1–255) in CONNECTION_ACCEPT (5-byte payload: 4 bytes tunnel IP + 1 byte num_channels). Sends **maxPolls × num_channels** POLLs initially (and one POLL per DATA received / per timeout), so all channels get POLLs. +- **Effect:** With NUM_CHANNELS=4 and client -w 20, the client sends 80 POLLs (20×4), so the server can have up to 80 in-flight packets (4× before). Throughput scales with in-flight packets, so multiplexing plus higher -w gets you closer to 1.6 Gbit/s. + +## Configuration + +- **NUM_CHANNELS** in [src/config.h](src/config.h): number of channels (default **4**). Set to **1** for original single-channel behavior. Rebuild after changing. +- **Protocol:** CONNECTION_ACCEPT may be 4 bytes (IP only, backward compatible) or 5 bytes (IP + num_channels). Old clients accept 4 bytes only; new clients accept 4 or 5 and use num_channels to send more POLLs. + +## Compatibility + +- **Server NUM_CHANNELS=1:** Sends 4-byte CONNECTION_ACCEPT; any client works. +- **Server NUM_CHANNELS>1:** Sends 5-byte CONNECTION_ACCEPT; new clients send maxPolls×numChannels POLLs; old clients (expect 4 bytes) may fail on CONNECTION_ACCEPT. Use NUM_CHANNELS=1 when talking to old clients. +- **Client:** Accepts 4 or 5 bytes; if 5, uses num_channels for initial POLL count. + +## Ordering + +Packets on the same channel are sent in order. Ordering across channels is not guaranteed (round-robin). For TCP over the tunnel this is fine; reordering is handled by TCP. + +## Metrics + +Per-channel stats (e.g. in SIGUSR1 dump) can be added later. For now, aggregate stats apply to all channels. diff --git a/docs/sequence.md b/docs/sequence.md new file mode 100644 index 0000000..3a64037 --- /dev/null +++ b/docs/sequence.md @@ -0,0 +1,21 @@ +# Sequence and retransmission + +## Overview + +Optional sequence numbers and NACK-based retransmission can reduce the impact of packet loss. When enabled (see below), DATA packets carry a 4-byte sequence number; the receiver tracks the last seen sequence and sends TYPE_NACK when it detects a gap. The sender keeps a small send buffer and resends on NACK. + +## Enabling + +Set `SEQUENCE_ENABLED` to 1 in [src/config.h](src/config.h) and rebuild. When enabled: + +- Data packets use TYPE_DATA_SEQ (10) with payload `[sequence uint32_t][data]`. +- Receiver sends TYPE_NACK (11) with a gap list when it sees a sequence gap. +- Sender keeps the last `SEND_BUF_SIZE` (default 64) packets and resends on NACK. + +## Backward compatibility + +Old peers that do not support TYPE_DATA_SEQ/TYPE_NACK ignore these packet types. New peers negotiate sequence support via handshake version (future) or by using TYPE_DATA_SEQ only when both ends are configured with SEQUENCE_ENABLED. + +## Pacing + +When using retransmission, enable pacing (`-R rate_kbps`) so resends do not burst and cause more loss. diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..8b9e05c --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,70 @@ +# Hans setup scripts + +## Env example + +Copy [../.env.example](../.env.example) to `.env` and fill values, or use the Python script to generate `.env`: + +```bash +cp .env.example .env +# Edit .env with your passphrase, server address, etc. +``` + +## Python setup (fill required values) + +Use the Python script to fill in the required options and generate a `.env` file for docker-compose. + +**Requirements:** Python 3.6+ (no extra packages). + +**Interactive:** + +```bash +# From repo root +python scripts/setup_hans.py +``` + +You will be prompted for: + +- **Role:** `1` or `s` = server, `2` or `c` = client (default: 2) +- **Passphrase:** type your own, or choose “Generate random passphrase? [y/N]” to get a random one (shown once; save it) +- **Server address:** (client only; IP or hostname) +- **Tunnel network:** (server only; e.g. `10.0.0.0`) + +The script writes `.env` in the current directory. Then run: + +```bash +docker compose build +docker compose up hans-server -d # or hans-client -d +``` + +**Role shortcuts:** `1` / `s` = server, `2` / `c` = client (short is easier than typing “server” or “client”). + +**Random passphrase:** + +```bash +# Generate random passphrase (shown once) +python scripts/setup_hans.py --random-pass +python scripts/setup_hans.py -r c -s 192.168.1.100 --random-pass + +# Custom length (default 24) +python scripts/setup_hans.py -r s --random-pass --pass-length 32 +``` + +**Non-interactive (e.g. CI):** + +```bash +python scripts/setup_hans.py -r c -s 192.168.1.100 -p mypass -o .env +python scripts/setup_hans.py -r s -n 10.0.0.0 -p mypass -o .env +``` + +**Options:** + +- `-r`, `--role` – `1`/`s`/server or `2`/`c`/client +- `-p`, `--passphrase` – passphrase (avoid in shell history) +- `--random-pass` – generate random passphrase (shown once) +- `--pass-length N` – length of random passphrase (default 24) +- `-s`, `--server` – server address (client only) +- `-n`, `--network` – tunnel network for server (default 10.0.0.0) +- `-o`, `--env` – output file (default .env) +- `--dry-run` – print variables only, do not write `.env` + +**Security:** Do not commit `.env` if it contains your real passphrase. `.env` is in `.gitignore`. diff --git a/scripts/setup_hans.py b/scripts/setup_hans.py new file mode 100644 index 0000000..00ef900 --- /dev/null +++ b/scripts/setup_hans.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Hans tunnel setup – fill required values and generate .env for docker-compose. + +Usage: + python scripts/setup_hans.py + python scripts/setup_hans.py -r c -s 192.168.1.100 --random-pass + python scripts/setup_hans.py --role client --server 192.168.1.100 + +Run from the repo root. Creates .env in the current directory. +""" + +from __future__ import print_function + +import argparse +import getpass +import os +import secrets +import string +import sys + + +# Role shortcuts: number or single char -> server|client +ROLE_CHOICES = { + "1": "server", "s": "server", "server": "server", + "2": "client", "c": "client", "client": "client", +} + + +def parse_role(s): + s = (s or "").strip().lower() + return ROLE_CHOICES.get(s, None) + + +def generate_passphrase(length=24): + """Generate a random passphrase (letters + digits + a few symbols).""" + alphabet = string.ascii_letters + string.digits + "!@#$%&*" + return "".join(secrets.choice(alphabet) for _ in range(length)) + + +def main(): + parser = argparse.ArgumentParser( + description="Fill required Hans tunnel options and write .env for docker-compose." + ) + parser.add_argument( + "-r", "--role", + metavar="1|2|s|c", + help="Role: 1 or s = server, 2 or c = client (default: prompt).", + ) + parser.add_argument( + "-p", "--passphrase", + metavar="PHRASE", + help="Passphrase (default: prompt; avoid passing on command line).", + ) + parser.add_argument( + "--random-pass", "--random-password", + action="store_true", + dest="random_pass", + help="Generate a random passphrase (shown once; save it).", + ) + parser.add_argument( + "--pass-length", + type=int, + default=24, + metavar="N", + help="Length of random passphrase (default: 24).", + ) + parser.add_argument( + "-s", "--server", + metavar="IP_OR_HOST", + help="Server address (client only).", + ) + parser.add_argument( + "-n", "--network", + metavar="NET", + default="10.0.0.0", + help="Tunnel network for server (default: 10.0.0.0).", + ) + parser.add_argument( + "-o", "--env", + metavar="FILE", + default=".env", + dest="env", + help="Output .env file path (default: .env).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print variables only, do not write .env.", + ) + args = parser.parse_args() + + # --- Role --- + role = None + if args.role is not None: + role = parse_role(args.role) + if role is None: + print("Invalid role. Use 1/s/server or 2/c/client.", file=sys.stderr) + sys.exit(1) + if not role: + print("Role: 1 (or s) = server 2 (or c) = client") + raw = input("Choice [2]: ").strip().lower() or "2" + role = parse_role(raw) + if role is None: + role = "client" + + # --- Passphrase --- + passphrase = args.passphrase + if passphrase is None and args.random_pass: + passphrase = generate_passphrase(max(12, min(64, args.pass_length))) + print("Generated passphrase (save it; it won't be shown again):") + print(" {}".format(passphrase)) + elif passphrase is None: + use_random = input("Generate random passphrase? [y/N]: ").strip().lower() + if use_random in ("y", "yes"): + passphrase = generate_passphrase(args.pass_length) + print("Generated passphrase (save it):") + print(" {}".format(passphrase)) + else: + passphrase = getpass.getpass("Passphrase: ") + if not passphrase: + print("Passphrase cannot be empty.", file=sys.stderr) + sys.exit(1) + passphrase_confirm = getpass.getpass("Passphrase (again): ") + if passphrase != passphrase_confirm: + print("Passphrases do not match.", file=sys.stderr) + sys.exit(1) + + # --- Server (client only) --- + server = args.server + if role == "client" and not server: + server = input("Server address (IP or hostname) [127.0.0.1]: ").strip() or "127.0.0.1" + + # --- Network (server) --- + network = args.network + if role == "server": + network_in = input("Tunnel network [{}]: ".format(network)).strip() + if network_in: + network = network_in + + # --- Build .env content --- + env_lines = [ + "# Generated by scripts/setup_hans.py – do not commit if it contains secrets", + "HANS_PASSPHRASE={}".format(passphrase), + "HANS_NETWORK={}".format(network), + "HANS_ROLE={}".format(role), + ] + if role == "client": + env_lines.insert(-1, "HANS_SERVER={}".format(server)) + + content = "\n".join(env_lines) + "\n" + + if args.dry_run: + print("Would write to {}:".format(args.env)) + print(content) + return 0 + + env_path = os.path.abspath(args.env) + with open(env_path, "w") as f: + f.write(content) + print("Wrote {}".format(env_path)) + + if role == "server": + print("\nNext steps:") + print(" docker compose build") + print(" docker compose up hans-server -d") + print(" (Server listens on tunnel network {}.)".format(network)) + else: + print("\nNext steps:") + print(" docker compose build") + print(" docker compose up hans-client -d") + print(" (Client connects to {}.)".format(server)) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/auth.cpp b/src/auth.cpp index 095a7fc..18a3197 100644 --- a/src/auth.cpp +++ b/src/auth.cpp @@ -19,6 +19,7 @@ #include "auth.h" #include "sha1.h" +#include "hmac.h" #include "utility.h" #include @@ -54,3 +55,13 @@ Auth::Challenge Auth::generateChallenge(int length) const return challenge; } + +Auth::Challenge Auth::getResponseHMAC(const Challenge &challenge) const +{ + return Hmac::sign(passphrase, &challenge[0], challenge.size()); +} + +bool Auth::verifyChallengeResponseHMAC(const Challenge &challenge, const char *response, size_t responseLen) const +{ + return Hmac::verify(passphrase, &challenge[0], challenge.size(), response, responseLen); +} diff --git a/src/auth.h b/src/auth.h index a2432fd..0b5f61a 100644 --- a/src/auth.h +++ b/src/auth.h @@ -40,6 +40,8 @@ class Auth Challenge generateChallenge(int length) const; Response getResponse(const Challenge &challenge) const; + Challenge getResponseHMAC(const Challenge &challenge) const; + bool verifyChallengeResponseHMAC(const Challenge &challenge, const char *response, size_t responseLen) const; protected: std::string passphrase; diff --git a/src/client.cpp b/src/client.cpp index b47e29a..f4798ef 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -22,6 +22,7 @@ #include "exception.h" #include "config.h" #include "utility.h" +#include "hmac.h" #include #include @@ -35,13 +36,22 @@ const Worker::TunnelHeader::Magic Client::magic("hanc"); Client::Client(int tunnelMtu, const string *deviceName, uint32_t serverIp, int maxPolls, const string &passphrase, uid_t uid, gid_t gid, - bool changeEchoId, bool changeEchoSeq, uint32_t desiredIp) - : Worker(tunnelMtu, deviceName, false, uid, gid), auth(passphrase) + bool changeEchoId, bool changeEchoSeq, uint32_t desiredIp, + int recvBufSize, int sndBufSize, int rateKbps, + bool useIPv6, const struct in6_addr *serverIp6) + : Worker(tunnelMtu, deviceName, false, uid, gid, recvBufSize, sndBufSize, rateKbps, !useIPv6, useIPv6), auth(passphrase) { this->serverIp = serverIp; + this->isIPv6 = useIPv6; + if (useIPv6 && serverIp6) + this->serverIp6 = *serverIp6; + else + memset(&this->serverIp6, 0, sizeof(this->serverIp6)); this->clientIp = INADDR_NONE; this->desiredIp = desiredIp; + this->useHmac = true; this->maxPolls = maxPolls; + this->numChannels = 1; this->nextEchoId = Utility::rand(); this->changeEchoId = changeEchoId; this->changeEchoSeq = changeEchoSeq; @@ -58,10 +68,11 @@ Client::~Client() void Client::sendConnectionRequest() { Server::ClientConnectData *connectData = (Server::ClientConnectData *)echoSendPayloadBuffer(); + connectData->version = 2; connectData->maxPolls = maxPolls; - connectData->desiredIp = desiredIp; + connectData->desiredIp = htonl(desiredIp); - syslog(LOG_DEBUG, "sending connection request"); + syslog(LOG_DEBUG, "sending connection request (HMAC)"); sendEchoToServer(TunnelHeader::TYPE_CONNECTION_REQUEST, sizeof(Server::ClientConnectData)); @@ -90,9 +101,18 @@ void Client::sendChallengeResponse(int dataLength) setTimeout(5000); } +bool Client::handleEchoData6(const Worker::TunnelHeader &header, int dataLength, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq) +{ + if (!isIPv6 || memcmp(&realIp, &serverIp6, sizeof(serverIp6)) != 0 || !reply) + return false; + return handleEchoData(header, dataLength, 0, reply, id, seq); +} + bool Client::handleEchoData(const TunnelHeader &header, int dataLength, uint32_t realIp, bool reply, uint16_t, uint16_t) { - if (realIp != serverIp || !reply) + if (!reply) + return false; + if (!isIPv6 && realIp != serverIp) return false; if (header.magic != Server::magic) @@ -122,7 +142,7 @@ bool Client::handleEchoData(const TunnelHeader &header, int dataLength, uint32_t case TunnelHeader::TYPE_CONNECTION_ACCEPT: if (state == STATE_CHALLENGE_RESPONSE_SENT) { - if (dataLength != sizeof(uint32_t)) + if (dataLength != sizeof(uint32_t) && dataLength != 5) { throw Exception("invalid ip received"); return true; @@ -130,7 +150,12 @@ bool Client::handleEchoData(const TunnelHeader &header, int dataLength, uint32_t syslog(LOG_INFO, "connection established"); - uint32_t ip = ntohl(*(uint32_t *)echoReceivePayloadBuffer()); + const char *buf = echoReceivePayloadBuffer(); + uint32_t ip = ntohl(*(const uint32_t *)buf); + if (dataLength >= 5) + numChannels = (unsigned char)buf[4]; + if (numChannels < 1) + numChannels = 1; if (ip != clientIp) { if (privilegesDropped) @@ -175,7 +200,10 @@ void Client::sendEchoToServer(Worker::TunnelHeader::Type type, int dataLength) if (maxPolls == 0 && state == STATE_ESTABLISHED) setTimeout(KEEP_ALIVE_INTERVAL); - sendEcho(magic, type, dataLength, serverIp, false, nextEchoId, nextEchoSequence); + if (isIPv6) + sendEcho6(magic, type, dataLength, serverIp6, false, nextEchoId, nextEchoSequence); + else + sendEcho(magic, type, dataLength, serverIp, false, nextEchoId, nextEchoSequence); if (changeEchoId) nextEchoId = nextEchoId + 38543; // some random prime @@ -191,7 +219,8 @@ void Client::startPolling() } else { - for (int i = 0; i < maxPolls; i++) + int n = maxPolls * (numChannels > 0 ? numChannels : 1); + for (int i = 0; i < n; i++) sendEchoToServer(TunnelHeader::TYPE_POLL, 0); setTimeout(POLL_INTERVAL); } diff --git a/src/client.h b/src/client.h index 11d7388..2040fcd 100644 --- a/src/client.h +++ b/src/client.h @@ -24,6 +24,7 @@ #include "auth.h" #include +#include class Client : public Worker { @@ -31,7 +32,9 @@ class Client : public Worker public: Client(int tunnelMtu, const std::string *deviceName, uint32_t serverIp, int maxPolls, const std::string &passphrase, uid_t uid, gid_t gid, - bool changeEchoId, bool changeEchoSeq, uint32_t desiredIp); + bool changeEchoId, bool changeEchoSeq, uint32_t desiredIp, + int recvBufSize = 256 * 1024, int sndBufSize = 256 * 1024, int rateKbps = 0, + bool useIPv6 = false, const struct in6_addr *serverIp6 = NULL); virtual ~Client(); virtual void run(); @@ -47,6 +50,7 @@ class Client : public Worker }; virtual bool handleEchoData(const TunnelHeader &header, int dataLength, uint32_t realIp, bool reply, uint16_t id, uint16_t seq); + virtual bool handleEchoData6(const Worker::TunnelHeader &header, int dataLength, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq); virtual void handleTunData(int dataLength, uint32_t sourceIp, uint32_t destIp); virtual void handleTimeout(); @@ -61,10 +65,14 @@ class Client : public Worker Auth auth; uint32_t serverIp; + bool isIPv6; + struct in6_addr serverIp6; + bool useHmac; uint32_t clientIp; uint32_t desiredIp; int maxPolls; + int numChannels; /* from CONNECTION_ACCEPT (multiplexing); 1 = single channel */ int pollTimeoutNr; bool changeEchoId, changeEchoSeq; diff --git a/src/config.h b/src/config.h index d44a991..205751b 100644 --- a/src/config.h +++ b/src/config.h @@ -24,5 +24,23 @@ #define CHALLENGE_SIZE 20 +#define SEQUENCE_ENABLED 0 +#define SEND_BUF_SIZE 64 + +/* Multiplexing: number of logical channels (POLL/reply streams). 1 = original; 4 or 8 = more in-flight, higher throughput. */ +#ifndef NUM_CHANNELS +#define NUM_CHANNELS 8 +#endif + +/* ICMP recv batch: 1 = original behavior, fair with multiple flows; larger = more throughput but unfair (0 KB/s on some iperf3 streams) */ +#ifndef HANS_RECV_BATCH_MAX +#define HANS_RECV_BATCH_MAX 1 +#endif + +/* Per-flow queues for fairness: number of flow queues per client (round-robin send). 1 = single FIFO (original). */ +#ifndef HANS_NUM_FLOW_QUEUES +#define HANS_NUM_FLOW_QUEUES 16 +#endif + // #define DEBUG_ONLY(a) a #define DEBUG_ONLY(a) diff --git a/src/congestion.cpp b/src/congestion.cpp new file mode 100644 index 0000000..412c764 --- /dev/null +++ b/src/congestion.cpp @@ -0,0 +1,41 @@ +/* + * Hans - IP over ICMP + * + * Optional congestion control. Stub: tracks sent/loss/RTT; rate is fixed when disabled. + */ + +#include "congestion.h" + +Congestion::Congestion() + : enabled(false) + , bytesSent(0) + , packetsLost(0) + , rttMs(0) + , currentRateKbps(0) +{ +} + +void Congestion::reportSent(int bytes) +{ + bytesSent += bytes; +} + +void Congestion::reportLoss() +{ + packetsLost++; +} + +void Congestion::reportRttMs(int ms) +{ + rttMs = ms; +} + +int Congestion::getCurrentRateKbps() const +{ + return currentRateKbps; +} + +void Congestion::refill(Time now) +{ + (void)now; +} diff --git a/src/congestion.h b/src/congestion.h new file mode 100644 index 0000000..ce55727 --- /dev/null +++ b/src/congestion.h @@ -0,0 +1,34 @@ +/* + * Hans - IP over ICMP + * + * Optional congestion control / rate control module. + * Off by default; when enabled, provides rate feedback based on loss and optional RTT. + */ + +#ifndef CONGESTION_H +#define CONGESTION_H + +#include "time.h" +#include + +class Congestion +{ +public: + Congestion(); + void setEnabled(bool on) { enabled = on; } + void reportSent(int bytes); + void reportLoss(); + void reportRttMs(int ms); + int getCurrentRateKbps() const; + void refill(Time now); + +private: + bool enabled; + uint64_t bytesSent; + uint64_t packetsLost; + int rttMs; + int currentRateKbps; + Time lastRefill; +}; + +#endif diff --git a/src/echo.cpp b/src/echo.cpp index c931d04..304d8fa 100644 --- a/src/echo.cpp +++ b/src/echo.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -35,12 +36,31 @@ typedef ip IpHeader; -Echo::Echo(int maxPayloadSize) +Echo::Echo(int maxPayloadSize, int recvBufSize, int sndBufSize) { fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (fd == -1) throw Exception("creating icmp socket", true); + if (recvBufSize > 0) + { + if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &recvBufSize, sizeof(recvBufSize)) == -1) + syslog(LOG_WARNING, "SO_RCVBUF %d: %s", recvBufSize, strerror(errno)); + } + if (sndBufSize > 0) + { + if (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndBufSize, sizeof(sndBufSize)) == -1) + syslog(LOG_WARNING, "SO_SNDBUF %d: %s", sndBufSize, strerror(errno)); + } + +#ifdef WIN32 + /* non-blocking not used on Windows for now */ +#else + int flags = fcntl(fd, F_GETFL, 0); + if (flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) + syslog(LOG_WARNING, "O_NONBLOCK: %s", strerror(errno)); +#endif + bufferSize = maxPayloadSize + headerSize(); sendBuffer.resize(bufferSize); receiveBuffer.resize(bufferSize); @@ -56,7 +76,7 @@ int Echo::headerSize() return sizeof(IpHeader) + sizeof(EchoHeader); } -void Echo::send(int payloadLength, uint32_t realIp, bool reply, uint16_t id, uint16_t seq) +bool Echo::send(int payloadLength, uint32_t realIp, bool reply, uint16_t id, uint16_t seq) { struct sockaddr_in target; target.sin_family = AF_INET; @@ -75,7 +95,8 @@ void Echo::send(int payloadLength, uint32_t realIp, bool reply, uint16_t id, uin int result = sendto(fd, sendBuffer.data() + sizeof(IpHeader), payloadLength + sizeof(EchoHeader), 0, (struct sockaddr *)&target, sizeof(struct sockaddr_in)); if (result == -1) - syslog(LOG_ERR, "error sending icmp packet: %s", strerror(errno)); + return false; + return true; } int Echo::receive(uint32_t &realIp, bool &reply, uint16_t &id, uint16_t &seq) @@ -86,8 +107,14 @@ int Echo::receive(uint32_t &realIp, bool &reply, uint16_t &id, uint16_t &seq) int dataLength = recvfrom(fd, receiveBuffer.data(), bufferSize, 0, (struct sockaddr *)&source, (socklen_t *)&source_addr_len); if (dataLength == -1) { +#ifdef WIN32 + return -1; +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) + return -1; syslog(LOG_ERR, "error receiving icmp packet: %s", strerror(errno)); return -1; +#endif } if (dataLength < sizeof(IpHeader) + sizeof(EchoHeader)) diff --git a/src/echo.h b/src/echo.h index 944a299..651474a 100644 --- a/src/echo.h +++ b/src/echo.h @@ -27,12 +27,12 @@ class Echo { public: - Echo(int maxPayloadSize); + Echo(int maxPayloadSize, int recvBufSize = 256 * 1024, int sndBufSize = 256 * 1024); ~Echo(); int getFd() { return fd; } - void send(int payloadLength, uint32_t realIp, bool reply, uint16_t id, uint16_t seq); + bool send(int payloadLength, uint32_t realIp, bool reply, uint16_t id, uint16_t seq); int receive(uint32_t &realIp, bool &reply, uint16_t &id, uint16_t &seq); char *sendPayloadBuffer(); diff --git a/src/echo6.cpp b/src/echo6.cpp new file mode 100644 index 0000000..cf7efe8 --- /dev/null +++ b/src/echo6.cpp @@ -0,0 +1,235 @@ +/* + * Hans - IP over ICMP + * Copyright (C) 2009 Friedrich Schöller + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "echo6.h" +#include "exception.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef IPPROTO_IPV6 +#define IPPROTO_IPV6 41 +#endif + +#ifndef IPPROTO_ICMPV6 +#define IPPROTO_ICMPV6 58 +#endif +#ifndef ICMP6_ECHO_REQUEST +#define ICMP6_ECHO_REQUEST 128 +#endif +#ifndef ICMP6_ECHO_REPLY +#define ICMP6_ECHO_REPLY 129 +#endif +#ifndef IPV6_CHECKSUM +#define IPV6_CHECKSUM 7 +#endif + +Echo6::Echo6(int maxPayloadSize, int recvBufSize, int sndBufSize) +{ + fd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); + if (fd == -1) + throw Exception("creating icmp6 socket", true); + + /* offset 2 = checksum field in ICMPv6 header; kernel fills it when supported */ + int csum_offset = 2; + kernelChecksum_ = (setsockopt(fd, IPPROTO_IPV6, IPV6_CHECKSUM, &csum_offset, sizeof(csum_offset)) == 0); + if (!kernelChecksum_) + syslog(LOG_WARNING, "IPV6_CHECKSUM not supported (%s), using userspace checksum", strerror(errno)); + cachedSrcValid_ = false; + + if (recvBufSize > 0) + { + if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &recvBufSize, sizeof(recvBufSize)) == -1) + syslog(LOG_WARNING, "SO_RCVBUF %d: %s", recvBufSize, strerror(errno)); + } + if (sndBufSize > 0) + { + if (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndBufSize, sizeof(sndBufSize)) == -1) + syslog(LOG_WARNING, "SO_SNDBUF %d: %s", sndBufSize, strerror(errno)); + } + +#ifdef WIN32 +#else + int flags = fcntl(fd, F_GETFL, 0); + if (flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) + syslog(LOG_WARNING, "O_NONBLOCK: %s", strerror(errno)); +#endif + + bufferSize = maxPayloadSize + headerSize(); + sendBuffer.resize(bufferSize); + receiveBuffer.resize(bufferSize); +} + +Echo6::~Echo6() +{ + if (fd >= 0) + close(fd); +} + +int Echo6::headerSize() +{ + return sizeof(Icmp6Header); +} + +/* One's complement sum for ICMPv6 checksum (RFC 2463): pseudo-header + ICMPv6 message */ +uint16_t Echo6::icmp6Checksum(const struct in6_addr &src, const struct in6_addr &dst, + const void *msg, size_t msgLen) +{ + size_t i; + const unsigned char *p; + uint32_t sum = 0; + + /* Pseudo-header: src (16) + dst (16) + upper_layer_len (4) + zero (3) + next_header (1) */ + p = (const unsigned char *)&src; + for (i = 0; i < 16; i += 2) + sum += (uint32_t)((p[i] << 8) | p[i + 1]); + p = (const unsigned char *)&dst; + for (i = 0; i < 16; i += 2) + sum += (uint32_t)((p[i] << 8) | p[i + 1]); + sum += (uint32_t)(msgLen >> 16) + (uint32_t)(msgLen & 0xFFFF); + sum += (uint32_t)IPPROTO_ICMPV6; + + p = (const unsigned char *)msg; + for (i = 0; i + 1 < msgLen; i += 2) + sum += (uint32_t)((p[i] << 8) | p[i + 1]); + if (i < msgLen) + sum += (uint32_t)(p[i] << 8); + + while (sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + return (uint16_t)(~sum); +} + +bool Echo6::getSourceForDest(const struct in6_addr &dest, struct in6_addr &srcOut) +{ + if (cachedSrcValid_ && memcmp(&cachedDest_, &dest, sizeof(dest)) == 0) + { + srcOut = cachedSrc_; + return true; + } + int s = socket(AF_INET6, SOCK_DGRAM, 0); + if (s == -1) + return false; + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_addr = dest; + addr.sin6_port = htons(80); + if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) + { + close(s); + return false; + } + struct sockaddr_in6 local; + socklen_t len = sizeof(local); + if (getsockname(s, (struct sockaddr *)&local, &len) != 0) + { + close(s); + return false; + } + close(s); + cachedDest_ = dest; + cachedSrc_ = local.sin6_addr; + cachedSrcValid_ = true; + srcOut = cachedSrc_; + return true; +} + +bool Echo6::send(int payloadLength, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq) +{ + struct sockaddr_in6 target; + memset(&target, 0, sizeof(target)); + target.sin6_family = AF_INET6; + target.sin6_addr = realIp; + + if (payloadLength + sizeof(Icmp6Header) > bufferSize) + throw Exception("packet too big"); + + Icmp6Header *header = (Icmp6Header *)sendBuffer.data(); + header->type = reply ? ICMP6_ECHO_REPLY : ICMP6_ECHO_REQUEST; + header->code = 0; + header->id = htons(id); + header->seq = htons(seq); + header->chksum = 0; + + if (!kernelChecksum_) + { + struct in6_addr src; + if (!getSourceForDest(realIp, src)) + return false; + header->chksum = htons(icmp6Checksum(src, realIp, sendBuffer.data(), payloadLength + sizeof(Icmp6Header))); + } + + int result = sendto(fd, sendBuffer.data(), payloadLength + sizeof(Icmp6Header), 0, + (struct sockaddr *)&target, sizeof(target)); + if (result == -1) + return false; + return true; +} + +int Echo6::receive(struct in6_addr &realIp, bool &reply, uint16_t &id, uint16_t &seq) +{ + struct sockaddr_in6 source; + socklen_t source_len = sizeof(source); + + int dataLength = recvfrom(fd, receiveBuffer.data(), bufferSize, 0, + (struct sockaddr *)&source, &source_len); + if (dataLength == -1) + { +#ifdef WIN32 + return -1; +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) + return -1; + syslog(LOG_ERR, "error receiving icmp6 packet: %s", strerror(errno)); + return -1; +#endif + } + + if (dataLength < (int)sizeof(Icmp6Header)) + return -1; + + Icmp6Header *header = (Icmp6Header *)receiveBuffer.data(); + if ((header->type != ICMP6_ECHO_REQUEST && header->type != ICMP6_ECHO_REPLY) || header->code != 0) + return -1; + + realIp = source.sin6_addr; + reply = header->type == ICMP6_ECHO_REPLY; + id = ntohs(header->id); + seq = ntohs(header->seq); + + return dataLength - sizeof(Icmp6Header); +} + +char *Echo6::sendPayloadBuffer() +{ + return sendBuffer.data() + headerSize(); +} + +char *Echo6::receivePayloadBuffer() +{ + return receiveBuffer.data() + headerSize(); +} diff --git a/src/echo6.h b/src/echo6.h new file mode 100644 index 0000000..3c0865e --- /dev/null +++ b/src/echo6.h @@ -0,0 +1,68 @@ +/* + * Hans - IP over ICMP + * Copyright (C) 2009 Friedrich Schöller + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef ECHO6_H +#define ECHO6_H + +#include +#include +#include + +class Echo6 +{ +public: + Echo6(int maxPayloadSize, int recvBufSize = 256 * 1024, int sndBufSize = 256 * 1024); + ~Echo6(); + + int getFd() { return fd; } + + bool send(int payloadLength, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq); + int receive(struct in6_addr &realIp, bool &reply, uint16_t &id, uint16_t &seq); + + char *sendPayloadBuffer(); + char *receivePayloadBuffer(); + + static int headerSize(); +protected: + struct Icmp6Header + { + uint8_t type; + uint8_t code; + uint16_t chksum; + uint16_t id; + uint16_t seq; + }; // size = 8 + + /* When IPV6_CHECKSUM setsockopt is unsupported (e.g. WSL/Docker), we fill checksum in userspace */ + bool kernelChecksum_; + struct in6_addr cachedDest_; + struct in6_addr cachedSrc_; + bool cachedSrcValid_; + + static uint16_t icmp6Checksum(const struct in6_addr &src, const struct in6_addr &dst, + const void *msg, size_t msgLen); + bool getSourceForDest(const struct in6_addr &dest, struct in6_addr &srcOut); + + int fd; + int bufferSize; + std::vector sendBuffer; + std::vector receiveBuffer; +}; + +#endif diff --git a/src/hmac.cpp b/src/hmac.cpp new file mode 100644 index 0000000..e2cf220 --- /dev/null +++ b/src/hmac.cpp @@ -0,0 +1,38 @@ +/* + * Hans - IP over ICMP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + */ + +#include "hmac.h" +#include +#include +#include +#include + +std::vector Hmac::sign(const std::string &key, const char *data, size_t dataLen) +{ + std::vector out(SHA256_SIZE); + unsigned int len = 0; + unsigned char *p = HMAC(EVP_sha256(), key.data(), key.size(), + reinterpret_cast(data), dataLen, + reinterpret_cast(out.data()), &len); + if (!p || len != SHA256_SIZE) + out.clear(); + return out; +} + +bool Hmac::verify(const std::string &key, const char *data, size_t dataLen, + const char *signature, size_t sigLen) +{ + if (sigLen != SHA256_SIZE) + return false; + std::vector expected = sign(key, data, dataLen); + if (expected.empty()) + return false; + return memcmp(expected.data(), signature, SHA256_SIZE) == 0; +} diff --git a/src/hmac.h b/src/hmac.h new file mode 100644 index 0000000..5e95666 --- /dev/null +++ b/src/hmac.h @@ -0,0 +1,28 @@ +/* + * Hans - IP over ICMP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + */ + +#ifndef HMAC_H +#define HMAC_H + +#include +#include +#include + +class Hmac +{ +public: + static const size_t SHA256_SIZE = 32; + + static std::vector sign(const std::string &key, const char *data, size_t dataLen); + static bool verify(const std::string &key, const char *data, size_t dataLen, + const char *signature, size_t sigLen); +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index ff22550..cfc16bc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ // #include #include #include +#include #include #include #include @@ -59,6 +60,12 @@ static void sig_int_handler(int) worker->stop(); } +static void sig_usr1_handler(int) +{ + if (worker) + worker->dumpStats(); +} + static void usage() { std::cerr << @@ -88,8 +95,13 @@ static void usage() " routers. May impact performance with others.\n" " -q Change echo sequence number on every echo request. May help with\n" " buggy routers. May impact performance with others.\n" + " -B buf Socket buffer sizes: recv,snd in bytes (e.g. 262144,262144).\n" + " -R rate Pacing: max send rate in Kbps (0 = disabled).\n" + " -W packets Max buffered packets per client (server only). Default 20.\n" + " -6 Use IPv6 (client only). Connect to server via AAAA.\n" " -f Run in foreground.\n" - " -v Print debug information.\n"; + " -v Print debug information.\n" + " SIGUSR1 Dump packet stats to syslog.\n"; } int main(int argc, char *argv[]) @@ -111,11 +123,16 @@ int main(int argc, char *argv[]) bool changeEchoId = false; bool changeEchoSeq = false; bool verbose = false; + int recvBufSize = 256 * 1024; + int sndBufSize = 256 * 1024; + int rateKbps = 0; + int maxBufferedPackets = 20; + bool useIPv6 = false; openlog(argv[0], LOG_PERROR, LOG_DAEMON); int c; - while ((c = getopt(argc, argv, "fru:d:p:s:c:m:w:qiva:")) != -1) + while ((c = getopt(argc, argv, "fru:d:p:s:c:m:w:qiva:B:R:W:6")) != -1) { switch(c) { case 'f': @@ -162,6 +179,28 @@ int main(int argc, char *argv[]) case 'a': clientIp = ntohl(inet_addr(optarg)); break; + case 'B': { + int r = 256 * 1024, s = 256 * 1024; + if (sscanf(optarg, "%d,%d", &r, &s) >= 1) + { + recvBufSize = r > 0 ? r : recvBufSize; + sndBufSize = s > 0 ? s : sndBufSize; + } + break; + } + case 'R': + rateKbps = atoi(optarg); + if (rateKbps < 0) + rateKbps = 0; + break; + case 'W': + maxBufferedPackets = atoi(optarg); + if (maxBufferedPackets < 1) + maxBufferedPackets = 20; + break; + case '6': + useIPv6 = true; + break; default: usage(); return 1; @@ -212,43 +251,82 @@ int main(int argc, char *argv[]) signal(SIGTERM, sig_term_handler); signal(SIGINT, sig_int_handler); +#ifndef WIN32 + signal(SIGUSR1, sig_usr1_handler); +#endif try { if (isServer) { worker = new Server(mtu, device.empty() ? NULL : &device, passphrase, - network, answerPing, uid, gid, 5000); + network, answerPing, uid, gid, 5000, + maxBufferedPackets, recvBufSize, sndBufSize, rateKbps); } else { struct addrinfo hints = {0}; struct addrinfo *res = NULL; + uint32_t serverIp = 0; + struct in6_addr serverIp6; + memset(&serverIp6, 0, sizeof(serverIp6)); - hints.ai_family = AF_INET; - hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG; - - int err = getaddrinfo(serverName.data(), NULL, &hints, &res); - if (err) + if (useIPv6) { - syslog(LOG_ERR, "getaddrinfo: %s", gai_strerror(err)); - return 1; + hints.ai_family = AF_INET6; + hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG; + int err = getaddrinfo(serverName.data(), NULL, &hints, &res); + if (err) + { + syslog(LOG_ERR, "getaddrinfo IPv6: %s", gai_strerror(err)); + return 1; + } + if (res->ai_family == AF_INET6) + serverIp6 = reinterpret_cast(res->ai_addr)->sin6_addr; + else + { + syslog(LOG_ERR, "no IPv6 address for %s", serverName.data()); + freeaddrinfo(res); + return 1; + } + worker = new Client(mtu, device.empty() ? NULL : &device, + 0, maxPolls, passphrase, uid, gid, + changeEchoId, changeEchoSeq, clientIp, + recvBufSize, sndBufSize, rateKbps, + true, &serverIp6); + } + else + { + hints.ai_family = AF_INET; + hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG; + int err = getaddrinfo(serverName.data(), NULL, &hints, &res); + if (err) + { + syslog(LOG_ERR, "getaddrinfo: %s", gai_strerror(err)); + return 1; + } + sockaddr_in *sockaddr = reinterpret_cast(res->ai_addr); + serverIp = sockaddr->sin_addr.s_addr; + worker = new Client(mtu, device.empty() ? NULL : &device, + ntohl(serverIp), maxPolls, passphrase, uid, gid, + changeEchoId, changeEchoSeq, clientIp, + recvBufSize, sndBufSize, rateKbps, + false, NULL); } - - sockaddr_in *sockaddr = reinterpret_cast(res->ai_addr); - uint32_t serverIp = sockaddr->sin_addr.s_addr; - - worker = new Client(mtu, device.empty() ? NULL : &device, - ntohl(serverIp), maxPolls, passphrase, uid, gid, - changeEchoId, changeEchoSeq, clientIp); - freeaddrinfo(res); } if (!foreground) { syslog(LOG_INFO, "detaching from terminal"); +#if defined(__APPLE__) || defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif daemon(0, 0); +#if defined(__APPLE__) || defined(__clang__) +#pragma clang diagnostic pop +#endif } worker->run(); diff --git a/src/pacer.cpp b/src/pacer.cpp new file mode 100644 index 0000000..87018cd --- /dev/null +++ b/src/pacer.cpp @@ -0,0 +1,66 @@ +/* + * Hans - IP over ICMP + * Copyright (C) 2009 Friedrich Schöller + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "pacer.h" + +Pacer::Pacer() + : enabled(false) + , tokens(0) + , refillRate(0) + , burstBytes(0) +{ +} + +Pacer::Pacer(int rateKbps, int burstBytes_) + : enabled(rateKbps > 0) + , tokens(static_cast(burstBytes_)) + , refillRate(rateKbps > 0 ? (rateKbps * 1000.0 / 8.0) / 1000.0 : 0) /* bytes/ms */ + , burstBytes(burstBytes_) + , lastRefill(Time::now()) +{ +} + +void Pacer::refill(Time now) +{ + if (!enabled) + return; + Time delta = now - lastRefill; + if (delta < Time::ZERO) + { + lastRefill = now; + return; + } + double ms = delta.getTimeval().tv_sec * 1000.0 + delta.getTimeval().tv_usec / 1000.0; + tokens += refillRate * ms; + if (tokens > burstBytes) + tokens = burstBytes; + lastRefill = now; +} + +bool Pacer::allowSend(int payloadBytes) +{ + if (!enabled) + return true; + if (tokens >= payloadBytes) + { + tokens -= payloadBytes; + return true; + } + return false; +} diff --git a/src/pacer.h b/src/pacer.h new file mode 100644 index 0000000..b4ab0c0 --- /dev/null +++ b/src/pacer.h @@ -0,0 +1,42 @@ +/* + * Hans - IP over ICMP + * Copyright (C) 2009 Friedrich Schöller + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef PACER_H +#define PACER_H + +#include "time.h" + +class Pacer +{ +public: + Pacer(); /* disabled */ + Pacer(int rateKbps, int burstBytes = 4500); + + void refill(Time now); + bool allowSend(int payloadBytes); + +private: + bool enabled; + double tokens; /* bytes */ + double refillRate; /* bytes per millisecond */ + int burstBytes; + Time lastRefill; +}; + +#endif diff --git a/src/server.cpp b/src/server.cpp index d427bd9..82b1624 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -21,6 +21,11 @@ #include "client.h" #include "config.h" #include "utility.h" +#include "hmac.h" + +#ifndef NUM_CHANNELS +#define NUM_CHANNELS 1 +#endif #include #include @@ -35,12 +40,31 @@ using std::endl; const Worker::TunnelHeader::Magic Server::magic("hans"); +/* Hash inner IP packet (5-tuple for TCP/UDP, else 3-tuple) to flow index 0..HANS_NUM_FLOW_QUEUES-1. */ +static int getFlowIdFromPayload(const char *data, int dataLength) +{ + if (HANS_NUM_FLOW_QUEUES <= 1 || dataLength < 20) + return 0; + const unsigned char *p = (const unsigned char *)data; + unsigned int hash = (unsigned int)p[12] << 24 | (unsigned int)p[13] << 16 | (unsigned int)p[14] << 8 | p[15]; + hash += ((unsigned int)p[16] << 24 | (unsigned int)p[17] << 16 | (unsigned int)p[18] << 8 | p[19]) * 31u; + hash += (unsigned int)p[9] * 31u; + if (dataLength >= 24 && (p[9] == 6 || p[9] == 17)) + { + hash += ((unsigned int)p[20] << 8 | p[21]) * 31u; + hash += ((unsigned int)p[22] << 8 | p[23]) * 31u; + } + return (int)(hash % (unsigned int)HANS_NUM_FLOW_QUEUES); +} + Server::Server(int tunnelMtu, const string *deviceName, const string &passphrase, - uint32_t network, bool answerEcho, uid_t uid, gid_t gid, int pollTimeout) - : Worker(tunnelMtu, deviceName, answerEcho, uid, gid), auth(passphrase) + uint32_t network, bool answerEcho, uid_t uid, gid_t gid, int pollTimeout, + int maxBufferedPackets, int recvBufSize, int sndBufSize, int rateKbps) + : Worker(tunnelMtu, deviceName, answerEcho, uid, gid, recvBufSize, sndBufSize, rateKbps, true, true), auth(passphrase) { this->network = network & 0xffffff00; this->pollTimeout = pollTimeout; + this->maxBufferedPackets = maxBufferedPackets > 0 ? maxBufferedPackets : 20; this->latestAssignedIpOffset = FIRST_ASSIGNED_IP_OFFSET - 1; tun.setIp(this->network + 1, this->network + 2); @@ -57,11 +81,15 @@ void Server::handleUnknownClient(const TunnelHeader &header, int dataLength, uin { ClientData client; client.realIp = realIp; + memset(&client.realIp6, 0, sizeof(client.realIp6)); + client.isV6 = false; + client.useHmac = false; client.maxPolls = 1; pollReceived(&client, echoId, echoSeq); - if (header.type != TunnelHeader::TYPE_CONNECTION_REQUEST || dataLength != sizeof(ClientConnectData)) + if (header.type != TunnelHeader::TYPE_CONNECTION_REQUEST || + (dataLength != sizeof(ClientConnectDataLegacy) && dataLength != sizeof(ClientConnectData))) { syslog(LOG_DEBUG, "invalid request (type %d) from %s", header.type, Utility::formatIp(realIp).c_str()); @@ -69,11 +97,23 @@ void Server::handleUnknownClient(const TunnelHeader &header, int dataLength, uin return; } - ClientConnectData *connectData = (ClientConnectData *)echoReceivePayloadBuffer(); - - client.maxPolls = connectData->maxPolls; + uint32_t desiredIp = 0; + if (dataLength == sizeof(ClientConnectDataLegacy)) + { + ClientConnectDataLegacy *connectData = (ClientConnectDataLegacy *)echoReceivePayloadBuffer(); + client.maxPolls = connectData->maxPolls; + desiredIp = ntohl(connectData->desiredIp); + client.useHmac = false; + } + else + { + ClientConnectData *connectData = (ClientConnectData *)echoReceivePayloadBuffer(); + client.maxPolls = connectData->maxPolls; + desiredIp = ntohl(connectData->desiredIp); + client.useHmac = (connectData->version >= 2); + } client.state = ClientData::STATE_NEW; - client.tunnelIp = reserveTunnelIp(connectData->desiredIp); + client.tunnelIp = reserveTunnelIp(desiredIp); syslog(LOG_DEBUG, "new client %s with tunnel address %s\n", Utility::formatIp(client.realIp).data(), @@ -86,6 +126,10 @@ void Server::handleUnknownClient(const TunnelHeader &header, int dataLength, uin // add client to list clientList.push_front(client); + clientList.front().pendingByFlow.resize(HANS_NUM_FLOW_QUEUES); + clientList.front().lastSentFlow = 0; + clientList.front().pollIdsByChannel.resize(NUM_CHANNELS > 0 ? NUM_CHANNELS : 1); + clientList.front().nextChannelToSend = 0; clientRealIpMap[realIp] = clientList.begin(); clientTunnelIpMap[client.tunnelIp] = clientList.begin(); } @@ -96,6 +140,67 @@ void Server::handleUnknownClient(const TunnelHeader &header, int dataLength, uin } } +void Server::handleUnknownClient6(const TunnelHeader &header, int dataLength, const struct in6_addr &realIp, uint16_t echoId, uint16_t echoSeq) +{ + ClientData client; + client.realIp = 0; + client.realIp6 = realIp; + client.isV6 = true; + client.useHmac = false; + client.maxPolls = 1; + + pollReceived(&client, echoId, echoSeq); + + if (header.type != TunnelHeader::TYPE_CONNECTION_REQUEST || + (dataLength != sizeof(ClientConnectDataLegacy) && dataLength != sizeof(ClientConnectData))) + { + syslog(LOG_DEBUG, "invalid request (type %d) from %s", header.type, Utility::formatIp6(realIp).c_str()); + sendReset(&client); + return; + } + + uint32_t desiredIp6 = 0; + if (dataLength == sizeof(ClientConnectDataLegacy)) + { + ClientConnectDataLegacy *connectData = (ClientConnectDataLegacy *)echoReceivePayloadBuffer(); + client.maxPolls = connectData->maxPolls; + desiredIp6 = ntohl(connectData->desiredIp); + client.useHmac = false; + } + else + { + ClientConnectData *connectData = (ClientConnectData *)echoReceivePayloadBuffer(); + client.maxPolls = connectData->maxPolls; + desiredIp6 = ntohl(connectData->desiredIp); + client.useHmac = (connectData->version >= 2); + } + client.state = ClientData::STATE_NEW; + client.tunnelIp = reserveTunnelIp(desiredIp6); + + syslog(LOG_DEBUG, "new IPv6 client %s with tunnel address %s\n", + Utility::formatIp6(client.realIp6).data(), + Utility::formatIp(client.tunnelIp).data()); + + if (client.tunnelIp != 0) + { + client.challenge = auth.generateChallenge(CHALLENGE_SIZE); + sendChallenge(&client); + + clientList.push_front(client); + clientList.front().pendingByFlow.resize(HANS_NUM_FLOW_QUEUES); + clientList.front().lastSentFlow = 0; + clientList.front().pollIdsByChannel.resize(NUM_CHANNELS > 0 ? NUM_CHANNELS : 1); + clientList.front().nextChannelToSend = 0; + clientRealIp6Map[realIp] = clientList.begin(); + clientTunnelIpMap[client.tunnelIp] = clientList.begin(); + } + else + { + syslog(LOG_WARNING, "server full"); + sendEchoToClient(&client, TunnelHeader::TYPE_SERVER_FULL, 0); + } +} + void Server::sendChallenge(ClientData *client) { syslog(LOG_DEBUG, "sending authentication request to %s\n", @@ -110,27 +215,46 @@ void Server::sendChallenge(ClientData *client) void Server::removeClient(ClientData *client) { syslog(LOG_DEBUG, "removing client %s with tunnel ip %s\n", - Utility::formatIp(client->realIp).data(), + client->isV6 ? Utility::formatIp6(client->realIp6).data() : Utility::formatIp(client->realIp).data(), Utility::formatIp(client->tunnelIp).data()); releaseTunnelIp(client->tunnelIp); - ClientList::iterator it = clientRealIpMap[client->realIp]; - - clientRealIpMap.erase(client->realIp); + ClientList::iterator it; + if (client->isV6) + { + it = clientRealIp6Map[client->realIp6]; + clientRealIp6Map.erase(client->realIp6); + } + else + { + it = clientRealIpMap[client->realIp]; + clientRealIpMap.erase(client->realIp); + } clientTunnelIpMap.erase(client->tunnelIp); - clientList.erase(it); } void Server::checkChallenge(ClientData *client, int length) { - Auth::Response rightResponse = auth.getResponse(client->challenge); + bool valid = false; + if (client->useHmac) + { + if (length == (int)Hmac::SHA256_SIZE && + auth.verifyChallengeResponseHMAC(client->challenge, echoReceivePayloadBuffer(), length)) + valid = true; + } + else + { + Auth::Response rightResponse = auth.getResponse(client->challenge); + if (length == (int)sizeof(Auth::Response) && memcmp(&rightResponse, echoReceivePayloadBuffer(), length) == 0) + valid = true; + } - if (length != sizeof(Auth::Response) || memcmp(&rightResponse, echoReceivePayloadBuffer(), length) != 0) + if (!valid) { syslog(LOG_DEBUG, "wrong challenge response from %s\n", - Utility::formatIp(client->realIp).data()); + client->isV6 ? Utility::formatIp6(client->realIp6).data() : Utility::formatIp(client->realIp).data()); sendEchoToClient(client, TunnelHeader::TYPE_CHALLENGE_ERROR, 0); @@ -138,10 +262,15 @@ void Server::checkChallenge(ClientData *client, int length) return; } - uint32_t *ip = (uint32_t *)echoSendPayloadBuffer(); - *ip = htonl(client->tunnelIp); - - sendEchoToClient(client, TunnelHeader::TYPE_CONNECTION_ACCEPT, sizeof(uint32_t)); + char *buf = echoSendPayloadBuffer(); + *(uint32_t *)buf = htonl(client->tunnelIp); + int acceptLen = sizeof(uint32_t); + if (NUM_CHANNELS > 1 && NUM_CHANNELS <= 255) + { + buf[4] = (char)NUM_CHANNELS; + acceptLen = 5; + } + sendEchoToClient(client, TunnelHeader::TYPE_CONNECTION_ACCEPT, acceptLen); client->state = ClientData::STATE_ESTABLISHED; @@ -182,8 +311,9 @@ bool Server::handleEchoData(const TunnelHeader &header, int dataLength, uint32_t return true; } - while (client->pollIds.size() > 1) - client->pollIds.pop(); + for (size_t c = 0; c < client->pollIdsByChannel.size(); c++) + while (client->pollIdsByChannel[c].size() > 0) + client->pollIdsByChannel[c].pop(); syslog(LOG_DEBUG, "reconnecting %s", Utility::formatIp(realIp).data()); sendReset(client); @@ -221,6 +351,69 @@ bool Server::handleEchoData(const TunnelHeader &header, int dataLength, uint32_t return true; } +bool Server::handleEchoData6(const TunnelHeader &header, int dataLength, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq) +{ + if (reply) + return false; + + if (header.magic != Client::magic) + return false; + + ClientData *client = getClientByRealIp6(realIp); + if (client == NULL) + { + handleUnknownClient6(header, dataLength, realIp, id, seq); + return true; + } + + pollReceived(client, id, seq); + + switch (header.type) + { + case TunnelHeader::TYPE_CONNECTION_REQUEST: + if (client->state == ClientData::STATE_CHALLENGE_SENT) + { + sendChallenge(client); + return true; + } + for (size_t c = 0; c < client->pollIdsByChannel.size(); c++) + while (client->pollIdsByChannel[c].size() > 0) + client->pollIdsByChannel[c].pop(); + syslog(LOG_DEBUG, "reconnecting %s", Utility::formatIp6(realIp).data()); + sendReset(client); + removeClient(client); + return true; + case TunnelHeader::TYPE_CHALLENGE_RESPONSE: + if (client->state == ClientData::STATE_CHALLENGE_SENT) + { + checkChallenge(client, dataLength); + return true; + } + break; + case TunnelHeader::TYPE_DATA: + if (client->state == ClientData::STATE_ESTABLISHED) + { + if (dataLength == 0) + { + syslog(LOG_WARNING, "received empty data packet"); + return true; + } + sendToTun(dataLength); + return true; + } + break; + case TunnelHeader::TYPE_POLL: + return true; + default: + break; + } + + syslog(LOG_DEBUG, "invalid packet from: %s, type: %d, state: %d", + Utility::formatIp6(realIp).data(), header.type, client->state); + + return true; +} + Server::ClientData *Server::getClientByTunnelIp(uint32_t ip) { ClientIpMap::iterator it = clientTunnelIpMap.find(ip); @@ -239,6 +432,15 @@ Server::ClientData *Server::getClientByRealIp(uint32_t ip) return &*it->second; } +Server::ClientData *Server::getClientByRealIp6(const struct in6_addr &ip6) +{ + ClientIp6Map::iterator it = clientRealIp6Map.find(ip6); + if (it == clientRealIp6Map.end()) + return NULL; + + return &*it->second; +} + void Server::handleTunData(int dataLength, uint32_t, uint32_t destIp) { if (destIp == network + 255) // ignore broadcasts @@ -256,23 +458,72 @@ void Server::handleTunData(int dataLength, uint32_t, uint32_t destIp) sendEchoToClient(client, TunnelHeader::TYPE_DATA, dataLength); } +bool Server::getNextPollFromChannels(ClientData *client, uint16_t &outId, uint16_t &outSeq) +{ + const int N = (int)client->pollIdsByChannel.size(); + if (N <= 0) + return false; + for (int i = 0; i < N; i++) + { + int c = (client->nextChannelToSend + i) % N; + if (client->pollIdsByChannel[c].size() > 0) + { + ClientData::EchoId e = client->pollIdsByChannel[c].front(); + client->pollIdsByChannel[c].pop(); + client->nextChannelToSend = (c + 1) % N; + outId = e.id; + outSeq = e.seq; + return true; + } + } + return false; +} + +bool Server::getNextPollPeek(ClientData *client, uint16_t &outId, uint16_t &outSeq) +{ + const int N = (int)client->pollIdsByChannel.size(); + for (int c = 0; c < N; c++) + { + if (client->pollIdsByChannel[c].size() > 0) + { + outId = client->pollIdsByChannel[c].front().id; + outSeq = client->pollIdsByChannel[c].front().seq; + return true; + } + } + return false; +} + void Server::pollReceived(ClientData *client, uint16_t echoId, uint16_t echoSeq) { unsigned int maxSavedPolls = client->maxPolls != 0 ? client->maxPolls : 1; + const int numCh = (int)client->pollIdsByChannel.size(); + if (numCh <= 0) + return; + int channel = (int)((unsigned int)echoId % (unsigned int)numCh); - client->pollIds.push(ClientData::EchoId(echoId, echoSeq)); - if (client->pollIds.size() > maxSavedPolls) - client->pollIds.pop(); - DEBUG_ONLY(cout << "poll -> " << client->pollIds.size() << endl); + client->pollIdsByChannel[channel].push(ClientData::EchoId(echoId, echoSeq)); + if (client->pollIdsByChannel[channel].size() > maxSavedPolls) + client->pollIdsByChannel[channel].pop(); + DEBUG_ONLY(cout << "poll -> channel " << channel << endl); - if (client->pendingPackets.size() > 0) + const int N = (int)client->pendingByFlow.size(); + if (N > 0) { - Packet &packet = client->pendingPackets.front(); - memcpy(echoSendPayloadBuffer(), &packet.data[0], packet.data.size()); - client->pendingPackets.pop(); - - DEBUG_ONLY(cout << "pending packet: " << packet.data.size() << " bytes\n"); - sendEchoToClient(client, packet.type, packet.data.size()); + for (int i = 0; i < N; i++) + { + int q = (client->lastSentFlow + 1 + i) % N; + if (client->pendingByFlow[q].size() > 0) + { + Packet &packet = client->pendingByFlow[q].front(); + memcpy(echoSendPayloadBuffer(), &packet.data[0], packet.data.size()); + client->pendingByFlow[q].pop(); + client->lastSentFlow = q; + DEBUG_ONLY(cout << "pending packet: " << packet.data.size() << " bytes (flow " << q << ")\n"); + sendEchoToClient(client, packet.type, packet.data.size()); + break; + } + } } client->lastActivity = now; @@ -280,36 +531,61 @@ void Server::pollReceived(ClientData *client, uint16_t echoId, uint16_t echoSeq) void Server::sendEchoToClient(ClientData *client, TunnelHeader::Type type, int dataLength) { + uint16_t outId = 0, outSeq = 0; if (client->maxPolls == 0) { - sendEcho(magic, type, dataLength, client->realIp, true, client->pollIds.front().id, client->pollIds.front().seq); + if (getNextPollPeek(client, outId, outSeq)) + { + if (client->isV6) + { + memcpy(echoSendPayloadBuffer6() - sizeof(TunnelHeader), echoSendPayloadBuffer() - sizeof(TunnelHeader), dataLength + sizeof(TunnelHeader)); + sendEcho6(magic, type, dataLength, client->realIp6, true, outId, outSeq); + } + else + sendEcho(magic, type, dataLength, client->realIp, true, outId, outSeq); + } return; } - if (client->pollIds.size() != 0) + if (getNextPollFromChannels(client, outId, outSeq)) { - ClientData::EchoId echoId = client->pollIds.front(); - client->pollIds.pop(); - - DEBUG_ONLY(cout << "sending -> " << client->pollIds.size() << endl); - sendEcho(magic, type, dataLength, client->realIp, true, echoId.id, echoId.seq); + DEBUG_ONLY(cout << "sending (channel round-robin)" << endl); + if (client->isV6) + { + memcpy(echoSendPayloadBuffer6() - sizeof(TunnelHeader), echoSendPayloadBuffer() - sizeof(TunnelHeader), dataLength + sizeof(TunnelHeader)); + sendEcho6(magic, type, dataLength, client->realIp6, true, outId, outSeq); + } + else + sendEcho(magic, type, dataLength, client->realIp, true, outId, outSeq); return; } - if (client->pendingPackets.size() == MAX_BUFFERED_PACKETS) + const int N = (int)client->pendingByFlow.size(); + if (N <= 0) + return; + /* TUN data is in echoSendPayloadBuffer(); ICMP receive payload is in echoReceivePayloadBuffer(). */ + char *payloadSrc = (type == TunnelHeader::TYPE_DATA) ? echoSendPayloadBuffer() : echoReceivePayloadBuffer(); + int flowId = (type == TunnelHeader::TYPE_DATA && N > 1) + ? getFlowIdFromPayload(payloadSrc, dataLength) : 0; + int maxPerFlow = (maxBufferedPackets > 0) ? (maxBufferedPackets + N - 1) / N : maxBufferedPackets; + if (maxPerFlow < 1) + maxPerFlow = 1; + + if ((int)client->pendingByFlow[flowId].size() >= maxPerFlow) { - client->pendingPackets.pop(); - syslog(LOG_WARNING, "packet to %s dropped", - Utility::formatIp(client->tunnelIp).data()); + client->pendingByFlow[flowId].pop(); + stats.incDroppedQueueFull(); + syslog(LOG_WARNING, "packet to %s dropped (queue full, flow %d)", + Utility::formatIp(client->tunnelIp).data(), flowId); } - DEBUG_ONLY(cout << "packet queued: " << dataLength << " bytes\n"); + DEBUG_ONLY(cout << "packet queued: " << dataLength << " bytes (flow " << flowId << ")\n"); - client->pendingPackets.push(Packet()); - Packet &packet = client->pendingPackets.back(); + client->pendingByFlow[flowId].push(Packet()); + Packet &packet = client->pendingByFlow[flowId].back(); packet.type = type; packet.data.resize(dataLength); - memcpy(&packet.data[0], echoReceivePayloadBuffer(), dataLength); + memcpy(&packet.data[0], payloadSrc, dataLength); } void Server::releaseTunnelIp(uint32_t tunnelIp) @@ -327,7 +603,7 @@ void Server::handleTimeout() if (client.lastActivity + KEEP_ALIVE_INTERVAL * 2 < now) { syslog(LOG_DEBUG, "client %s timed out\n", - Utility::formatIp(client.realIp).data()); + client.isV6 ? Utility::formatIp6(client.realIp6).data() : Utility::formatIp(client.realIp).data()); removeClient(&client); } } diff --git a/src/server.h b/src/server.h index 7e2b6c7..dc8351c 100644 --- a/src/server.h +++ b/src/server.h @@ -29,16 +29,26 @@ #include #include #include +#include +#include class Server : public Worker { public: Server(int tunnelMtu, const std::string *deviceName, const std::string &passphrase, - uint32_t network, bool answerEcho, uid_t uid, gid_t gid, int pollTimeout); + uint32_t network, bool answerEcho, uid_t uid, gid_t gid, int pollTimeout, + int maxBufferedPackets = 20, int recvBufSize = 256 * 1024, int sndBufSize = 256 * 1024, int rateKbps = 0); virtual ~Server(); + struct ClientConnectDataLegacy + { + uint8_t maxPolls; + uint32_t desiredIp; + }; + struct ClientConnectData { + uint8_t version; uint8_t maxPolls; uint32_t desiredIp; }; @@ -70,23 +80,37 @@ class Server : public Worker }; uint32_t realIp; + struct in6_addr realIp6; + bool isV6; uint32_t tunnelIp; - std::queue pendingPackets; + /* Per-flow queues (round-robin send for fairness); size = HANS_NUM_FLOW_QUEUES. If 1, single FIFO. */ + std::vector > pendingByFlow; + int lastSentFlow; int maxPolls; - std::queue pollIds; + /* Per-channel POLL queues for multiplexing; size = NUM_CHANNELS. Channel = echoId % NUM_CHANNELS. */ + std::vector > pollIdsByChannel; + int nextChannelToSend; Time lastActivity; State state; + bool useHmac; Auth::Challenge challenge; }; typedef std::list ClientList; typedef std::map ClientIpMap; + struct In6AddrCompare { + bool operator()(const struct in6_addr &a, const struct in6_addr &b) const { + return memcmp(&a, &b, sizeof(a)) < 0; + } + }; + typedef std::map ClientIp6Map; virtual bool handleEchoData(const TunnelHeader &header, int dataLength, uint32_t realIp, bool reply, uint16_t id, uint16_t seq); + virtual bool handleEchoData6(const TunnelHeader &header, int dataLength, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq); virtual void handleTunData(int dataLength, uint32_t sourceIp, uint32_t destIp); virtual void handleTimeout(); @@ -95,6 +119,7 @@ class Server : public Worker void serveTun(ClientData *client); void handleUnknownClient(const TunnelHeader &header, int dataLength, uint32_t realIp, uint16_t echoId, uint16_t echoSeq); + void handleUnknownClient6(const TunnelHeader &header, int dataLength, const struct in6_addr &realIp, uint16_t echoId, uint16_t echoSeq); void removeClient(ClientData *client); void sendChallenge(ClientData *client); @@ -105,11 +130,15 @@ class Server : public Worker void pollReceived(ClientData *client, uint16_t echoId, uint16_t echoSeq); + bool getNextPollFromChannels(ClientData *client, uint16_t &outId, uint16_t &outSeq); + bool getNextPollPeek(ClientData *client, uint16_t &outId, uint16_t &outSeq); + uint32_t reserveTunnelIp(uint32_t desiredIp); void releaseTunnelIp(uint32_t tunnelIp); ClientData *getClientByTunnelIp(uint32_t ip); ClientData *getClientByRealIp(uint32_t ip); + ClientData *getClientByRealIp6(const struct in6_addr &ip6); Auth auth; @@ -118,9 +147,11 @@ class Server : public Worker uint32_t latestAssignedIpOffset; Time pollTimeout; + int maxBufferedPackets; ClientList clientList; ClientIpMap clientRealIpMap; + ClientIp6Map clientRealIp6Map; ClientIpMap clientTunnelIpMap; }; diff --git a/src/stats.cpp b/src/stats.cpp new file mode 100644 index 0000000..7a250e8 --- /dev/null +++ b/src/stats.cpp @@ -0,0 +1,68 @@ +/* + * Hans - IP over ICMP + * Copyright (C) 2009 Friedrich Schöller + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "stats.h" + +#include +#include + +Stats::Stats() + : packets_sent(0) + , packets_received(0) + , bytes_sent(0) + , bytes_received(0) + , packets_dropped_send_fail(0) + , packets_dropped_queue_full(0) +{ +} + +void Stats::incPacketsSent(int bytes) +{ + packets_sent++; + if (bytes > 0) + bytes_sent += bytes; +} + +void Stats::incPacketsReceived(int bytes) +{ + packets_received++; + if (bytes > 0) + bytes_received += bytes; +} + +void Stats::incDroppedSendFail() +{ + packets_dropped_send_fail++; +} + +void Stats::incDroppedQueueFull() +{ + packets_dropped_queue_full++; +} + +void Stats::dumpToSyslog() const +{ + syslog(LOG_INFO, "stats: packets_sent=%" PRIu64 " packets_received=%" PRIu64 " bytes_sent=%" PRIu64 " bytes_received=%" PRIu64 " dropped_send_fail=%" PRIu64 " dropped_queue_full=%" PRIu64, + packets_sent, + packets_received, + bytes_sent, + bytes_received, + packets_dropped_send_fail, + packets_dropped_queue_full); +} diff --git a/src/stats.h b/src/stats.h new file mode 100644 index 0000000..7dc2a9f --- /dev/null +++ b/src/stats.h @@ -0,0 +1,46 @@ +/* + * Hans - IP over ICMP + * Copyright (C) 2009 Friedrich Schöller + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef STATS_H +#define STATS_H + +#include + +class Stats +{ +public: + Stats(); + + void incPacketsSent(int bytes = 0); + void incPacketsReceived(int bytes = 0); + void incDroppedSendFail(); + void incDroppedQueueFull(); + + void dumpToSyslog() const; + +private: + uint64_t packets_sent; + uint64_t packets_received; + uint64_t bytes_sent; + uint64_t bytes_received; + uint64_t packets_dropped_send_fail; + uint64_t packets_dropped_queue_full; +}; + +#endif diff --git a/src/utility.cpp b/src/utility.cpp index 87521a7..03be2d3 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -23,6 +23,7 @@ #include #include #include +#include std::string Utility::formatIp(uint32_t ip) { @@ -34,6 +35,14 @@ std::string Utility::formatIp(uint32_t ip) return s.str(); } +std::string Utility::formatIp6(const struct in6_addr &ip6) +{ + char buf[INET6_ADDRSTRLEN]; + if (inet_ntop(AF_INET6, &ip6, buf, sizeof(buf)) == NULL) + return std::string("::"); + return std::string(buf); +} + int Utility::rand() { static bool init = false; diff --git a/src/utility.h b/src/utility.h index ed1359c..7f5f7bc 100644 --- a/src/utility.h +++ b/src/utility.h @@ -22,11 +22,13 @@ #include #include +#include class Utility { public: static std::string formatIp(uint32_t ip); + static std::string formatIp6(const struct in6_addr &ip6); static int rand(); }; diff --git a/src/worker.cpp b/src/worker.cpp index c6f8dc5..509f860 100644 --- a/src/worker.cpp +++ b/src/worker.cpp @@ -29,10 +29,13 @@ #include #include #include +#include using std::cout; using std::endl; +const int Worker::RECV_BATCH_MAX = HANS_RECV_BATCH_MAX; + Worker::TunnelHeader::Magic::Magic(const char *magic) { memset(data, 0, sizeof(data)); @@ -50,8 +53,14 @@ bool Worker::TunnelHeader::Magic::operator!=(const Magic &other) const } Worker::Worker(int tunnelMtu, const std::string *deviceName, bool answerEcho, - uid_t uid, gid_t gid) - : echo(tunnelMtu + sizeof(TunnelHeader)), tun(deviceName, tunnelMtu) + uid_t uid, gid_t gid, + int recvBufSize, int sndBufSize, int rateKbps, + bool useIPv4, bool useIPv6) + : echo(useIPv4 ? new Echo(tunnelMtu + sizeof(TunnelHeader), recvBufSize, sndBufSize) : NULL), + echo6(useIPv6 ? new Echo6(tunnelMtu + sizeof(TunnelHeader), recvBufSize, sndBufSize) : NULL), + currentRecvFrom6(false), + tun(deviceName, tunnelMtu), + pacer(rateKbps > 0 ? rateKbps : 0, 4500) { this->tunnelMtu = tunnelMtu; this->answerEcho = answerEcho; @@ -60,13 +69,30 @@ Worker::Worker(int tunnelMtu, const std::string *deviceName, bool answerEcho, this->privilegesDropped = false; } -void Worker::sendEcho(const TunnelHeader::Magic &magic, TunnelHeader::Type type, +Worker::~Worker() +{ + delete echo; + echo = NULL; + delete echo6; + echo6 = NULL; +} + +bool Worker::sendEcho(const TunnelHeader::Magic &magic, TunnelHeader::Type type, int length, uint32_t realIp, bool reply, uint16_t id, uint16_t seq) { + if (!echo) + return false; if (length > payloadBufferSize()) throw Exception("packet too big"); - TunnelHeader *header = (TunnelHeader *)echo.sendPayloadBuffer(); + int totalLen = length + sizeof(TunnelHeader); + if (!pacer.allowSend(totalLen)) + { + stats.incDroppedSendFail(); + return false; + } + + TunnelHeader *header = (TunnelHeader *)echo->sendPayloadBuffer(); header->magic = magic; header->type = type; @@ -74,7 +100,41 @@ void Worker::sendEcho(const TunnelHeader::Magic &magic, TunnelHeader::Type type, cout << "sending: type " << type << ", length " << length << ", id " << id << ", seq " << seq << endl); - echo.send(length + sizeof(TunnelHeader), realIp, reply, id, seq); + if (!echo->send(totalLen, realIp, reply, id, seq)) + { + stats.incDroppedSendFail(); + return false; + } + stats.incPacketsSent(totalLen); + return true; +} + +bool Worker::sendEcho6(const TunnelHeader::Magic &magic, TunnelHeader::Type type, + int length, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq) +{ + if (!echo6) + return false; + if (length > payloadBufferSize()) + throw Exception("packet too big"); + + int totalLen = length + sizeof(TunnelHeader); + if (!pacer.allowSend(totalLen)) + { + stats.incDroppedSendFail(); + return false; + } + + TunnelHeader *header = (TunnelHeader *)echo6->sendPayloadBuffer(); + header->magic = magic; + header->type = type; + + if (!echo6->send(totalLen, realIp, reply, id, seq)) + { + stats.incDroppedSendFail(); + return false; + } + stats.incPacketsSent(totalLen); + return true; } void Worker::sendToTun(int length) @@ -82,6 +142,20 @@ void Worker::sendToTun(int length) tun.write(echoReceivePayloadBuffer(), length); } +char *Worker::echoSendPayloadBuffer() +{ + if (echo) + return echo->sendPayloadBuffer() + sizeof(TunnelHeader); + if (echo6) + return echo6->sendPayloadBuffer() + sizeof(TunnelHeader); + return NULL; +} + +char *Worker::echoSendPayloadBuffer6() +{ + return echo6 ? echo6->sendPayloadBuffer() + sizeof(TunnelHeader) : NULL; +} + void Worker::setTimeout(Time delta) { nextTimeout = now + delta; @@ -92,7 +166,11 @@ void Worker::run() now = Time::now(); alive = true; - int maxFd = echo.getFd() > tun.getFd() ? echo.getFd() : tun.getFd(); + int maxFd = tun.getFd(); + if (echo && echo->getFd() > maxFd) + maxFd = echo->getFd(); + if (echo6 && echo6->getFd() > maxFd) + maxFd = echo6->getFd(); while (alive) { @@ -101,7 +179,10 @@ void Worker::run() FD_ZERO(&fs); FD_SET(tun.getFd(), &fs); - FD_SET(echo.getFd(), &fs); + if (echo) + FD_SET(echo->getFd(), &fs); + if (echo6) + FD_SET(echo6->getFd(), &fs); if (nextTimeout != Time::ZERO) { @@ -121,6 +202,7 @@ void Worker::run() return; } now = Time::now(); + pacer.refill(now); // timeout if (result == 0) @@ -130,21 +212,36 @@ void Worker::run() continue; } - // icmp data - if (FD_ISSET(echo.getFd(), &fs)) + // icmp data (batch read up to RECV_BATCH_MAX) + if (echo && FD_ISSET(echo->getFd(), &fs)) { - bool reply; - uint16_t id, seq; - uint32_t ip; - - int dataLength = echo.receive(ip, reply, id, seq); - if (dataLength != -1) + int batchCount = 0; + while (batchCount < RECV_BATCH_MAX) { + bool reply; + uint16_t id, seq; + uint32_t ip; + + currentRecvFrom6 = false; + int dataLength = echo->receive(ip, reply, id, seq); + if (dataLength == -1) + { +#ifndef WIN32 + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; +#endif + break; + } + batchCount++; + stats.incPacketsReceived(dataLength); +#ifdef WIN32 + break; +#endif bool valid = dataLength >= sizeof(TunnelHeader); if (valid) { - TunnelHeader *header = (TunnelHeader *)echo.receivePayloadBuffer(); + TunnelHeader *header = (TunnelHeader *)echo->receivePayloadBuffer(); DEBUG_ONLY( cout << "received: type " << header->type @@ -156,8 +253,49 @@ void Worker::run() if (!valid && !reply && answerEcho) { - memcpy(echo.sendPayloadBuffer(), echo.receivePayloadBuffer(), dataLength); - echo.send(dataLength, ip, true, id, seq); + memcpy(echo->sendPayloadBuffer(), echo->receivePayloadBuffer(), dataLength); + echo->send(dataLength, ip, true, id, seq); + } + } + } + + if (echo6 && FD_ISSET(echo6->getFd(), &fs)) + { + int batchCount = 0; + while (batchCount < RECV_BATCH_MAX) + { + bool reply; + uint16_t id, seq; + struct in6_addr ip6; + + currentRecvFrom6 = true; + int dataLength = echo6->receive(ip6, reply, id, seq); + if (dataLength == -1) + { +#ifndef WIN32 + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; +#endif + break; + } + batchCount++; + stats.incPacketsReceived(dataLength); +#ifdef WIN32 + break; +#endif + bool valid = dataLength >= sizeof(TunnelHeader); + + if (valid) + { + TunnelHeader *header = (TunnelHeader *)echo6->receivePayloadBuffer(); + + valid = handleEchoData6(*header, dataLength - sizeof(TunnelHeader), ip6, reply, id, seq); + } + + if (!valid && !reply && answerEcho) + { + memcpy(echo6->sendPayloadBuffer(), echo6->receivePayloadBuffer(), dataLength); + echo6->send(dataLength, ip6, true, id, seq); } } } @@ -166,8 +304,10 @@ void Worker::run() if (FD_ISSET(tun.getFd(), &fs)) { uint32_t sourceIp, destIp; - - int dataLength = tun.read(echoSendPayloadBuffer(), sourceIp, destIp); + char *sendBuf = echoSendPayloadBuffer(); + if (!sendBuf) + sendBuf = echoSendPayloadBuffer6(); + int dataLength = sendBuf ? tun.read(sendBuf, sourceIp, destIp) : -1; if (dataLength == 0) throw Exception("tunnel closed"); @@ -211,16 +351,20 @@ bool Worker::handleEchoData(const TunnelHeader &, int, uint32_t, bool, uint16_t, return true; } +bool Worker::handleEchoData6(const TunnelHeader &, int, const struct in6_addr &, bool, uint16_t, uint16_t) +{ + return true; +} + void Worker::handleTunData(int, uint32_t, uint32_t) { } void Worker::handleTimeout() { } -char *Worker::echoSendPayloadBuffer() -{ - return echo.sendPayloadBuffer() + sizeof(TunnelHeader); -} - char *Worker::echoReceivePayloadBuffer() { - return echo.receivePayloadBuffer() + sizeof(TunnelHeader); + if (currentRecvFrom6 && echo6) + return echo6->receivePayloadBuffer() + sizeof(TunnelHeader); + if (echo) + return echo->receivePayloadBuffer() + sizeof(TunnelHeader); + return NULL; } diff --git a/src/worker.h b/src/worker.h index 3ac0367..8e67eeb 100644 --- a/src/worker.h +++ b/src/worker.h @@ -22,20 +22,28 @@ #include "time.h" #include "echo.h" +#include "echo6.h" #include "tun.h" +#include "stats.h" +#include "pacer.h" #include #include +#include class Worker { public: Worker(int tunnelMtu, const std::string *deviceName, bool answerEcho, - uid_t uid, gid_t gid); - virtual ~Worker() { } + uid_t uid, gid_t gid, + int recvBufSize = 256 * 1024, int sndBufSize = 256 * 1024, + int rateKbps = 0, + bool useIPv4 = true, bool useIPv6 = false); + virtual ~Worker(); virtual void run(); virtual void stop(); + void dumpStats() const { stats.dumpToSyslog(); } static int headerSize() { return sizeof(TunnelHeader); } @@ -63,7 +71,9 @@ class Worker TYPE_CHALLENGE_ERROR = 6, TYPE_DATA = 7, TYPE_POLL = 8, - TYPE_SERVER_FULL = 9 + TYPE_SERVER_FULL = 9, + TYPE_DATA_SEQ = 10, + TYPE_NACK = 11 }; Magic magic; @@ -72,25 +82,34 @@ class Worker virtual bool handleEchoData(const TunnelHeader &header, int dataLength, uint32_t realIp, bool reply, uint16_t id, uint16_t seq); + virtual bool handleEchoData6(const TunnelHeader &header, int dataLength, + const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq); virtual void handleTunData(int dataLength, uint32_t sourceIp, uint32_t destIp); // to echoSendPayloadBuffer virtual void handleTimeout(); - void sendEcho(const TunnelHeader::Magic &magic, TunnelHeader::Type type, + bool sendEcho(const TunnelHeader::Magic &magic, TunnelHeader::Type type, int length, uint32_t realIp, bool reply, uint16_t id, uint16_t seq); + bool sendEcho6(const TunnelHeader::Magic &magic, TunnelHeader::Type type, + int length, const struct in6_addr &realIp, bool reply, uint16_t id, uint16_t seq); void sendToTun(int length); // from echoReceivePayloadBuffer void setTimeout(Time delta); char *echoSendPayloadBuffer(); + char *echoSendPayloadBuffer6(); char *echoReceivePayloadBuffer(); int payloadBufferSize() { return tunnelMtu; } void dropPrivileges(); - Echo echo; + Echo *echo; + Echo6 *echo6; + bool currentRecvFrom6; Tun tun; + Stats stats; + Pacer pacer; bool alive; bool answerEcho; int tunnelMtu; @@ -101,9 +120,9 @@ class Worker bool privilegesDropped; Time now; -private: - int readIcmpData(int *realIp, int *id, int *seq); + static const int RECV_BATCH_MAX; +private: Time nextTimeout; };