Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f76977f
feat: add WireWorm NAT hole punching PoC
thebalaa Jan 15, 2026
e8f01d0
feat: add interactive wireworm explorer
thebalaa Jan 15, 2026
e9afe5e
fix: relocate test utils to resolve package main conflict and update …
thebalaa Jan 15, 2026
f6a869e
fix: improve shell compatibility for interactive prompts
thebalaa Jan 15, 2026
beba019
fix: add input validation and sanitization to interactive PoC
thebalaa Jan 15, 2026
7a80db4
fix: resolve hole punching 'signal mismatch' and add connection monitor
thebalaa Jan 15, 2026
2cc7012
feat: simplify peer exchange with unified connection string
thebalaa Jan 15, 2026
6ae3745
fix: randomize local port for discovery to avoid conflicts and verify…
thebalaa Jan 15, 2026
ccfb6fb
feat: add P2P encrypted chat mode to WireWorm
thebalaa Jan 15, 2026
aff079b
fix: resolve handshake monitor shell errors and redundant logging
thebalaa Jan 15, 2026
adc9539
refactor: run chat in foreground for cleaner exit and terminal control
thebalaa Jan 15, 2026
f76a2d5
feat: add /ping command to chat for latency measurement
thebalaa Jan 15, 2026
04d66cc
feat: add Dockerfile for WireWorm and update documentation
thebalaa Jan 15, 2026
9f665da
fix: build stuntman from source in Dockerfile
thebalaa Jan 15, 2026
c561661
fix: resolve docker build error and monitor shell syntax
thebalaa Jan 15, 2026
75f135e
fix: use git clone for stuntman source to fix docker build
thebalaa Jan 15, 2026
2491bfd
fix: restore mode selection logic in interactive script
thebalaa Jan 15, 2026
338f664
feat: improve error diagnostics and log reporting in interactive script
thebalaa Jan 15, 2026
2680a23
fix: resolve Exec format error by excluding host binaries from image
thebalaa Jan 15, 2026
26bcf15
feat: support fixed UDP port for Docker on Mac/Windows
thebalaa Jan 15, 2026
75ac159
fix: cross-platform timeout for stun discovery on macOS
thebalaa Jan 15, 2026
f6568b1
perf: optimize stun discovery with basic mode and faster timeouts
thebalaa Jan 15, 2026
96feb95
fix(wireworm): improve STUN reliability with IPv4 forcing, debug logs…
thebalaa Jan 15, 2026
e7fe66d
docs: add analysis of Docker Desktop NAT limitations
thebalaa Jan 15, 2026
4b1176e
feat: add NAT hole punching for peer-to-peer WireGuard tunnels
thebalaa Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
.github
.gitignore
Dockerfile
Dockerfile.wireworm
LICENSE
README.md
wireproxy
wireworm.conf
wireproxy.log
*.txt
*.bin
56 changes: 56 additions & 0 deletions DOCKER_NAT_ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Docker NAT Hole Punching Analysis

## The Issue: "Symmetric NAT" in Docker Desktop

We successfully hole-punched natively on macOS, but failed consistently when running the same logic inside a Docker container on Docker Desktop (Mac/Windows).

### Root Cause Analysis

UDP Hole Punching relies on a critical assumption: **Cone NAT**.
> The router must map `InternalIP:Port` to the **same** `ExternalIP:Port` regardless of the destination address.

1. **Native Execution**: macOS Kernel -> Router (Cone NAT) -> Internet.
* Request to STUN Server -> Source Port `51820` preserved (or mapped consistently).
* Request to Peer -> Source Port `51820` preserved.
* **Result**: Success.

2. **Docker Desktop Execution**: Container -> **Linux VM Bridge** -> **VPNKit Userland Proxy** -> Host OS -> Router -> Internet.
* This translation layer often behaves as **Symmetric NAT**.
* Request to STUN Server: Mapped to External Port `32001`.
* Request to Peer: Mapped to External Port `45002`.
* **Result**: The peer tries to reply to `32001` (what STUN saw), but your firewall expects traffic on `45002`. Packet dropped.

## Proposed Solutions

To successfully containerize this utility, we must bypass the Docker Desktop networking abstraction.

### 1. Host Networking (Linux Only)
On native Linux, `--network host` shares the host's networking stack directly.
* **Feasibility**: High (Linux), Zero (Mac/Windows).
* **Command**: `docker run --network host ...`
* **Limitation**: On Docker Desktop, this only shares the *Linux VM's* network, not the Mac/Windows Host network, so it remains double-natted.

### 2. Macvlan Network Driver
Giving the container its own IP address on the local LAN, bypassing the host's NAT entirely.
* **Feasibility**: Medium. Requires network configuration access.
* **Command**:
```bash
docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=en0 pub_net
docker run --net pub_net ...
```
* **Pros**: Makes the container appear as a physical device on the network.
* **Cons**: Wireless adapters (WiFi) often reject macvlan traffic due to security features (only one MAC address allowed per client).

### 3. UDP Port Preservation (High Difficulty)
We need a way to force Docker's outbound NAT to preserve the source port.
* **Method**: Utilize `iptables` inside the Docker VM (if accessible) to set SNAT rules.
* **Complexity**: Docker Desktop does not easily allow modifying the VM's `iptables`.

### 4. Hybrid Approach (The "Sidecar")
Run the `wireproxy` logic in the container, but run the networking/socket layer on the host.
* **Method**: This effectively defeats the purpose of containerization (portability).

## Conclusion
For **UDP Hole Punching**, the physical network layer is leaking into the abstraction. Docker Desktop's default networking mode is fundamentally incompatible with the requirement of "Endpoint-Independent Mapping" needed for P2P connection establishment without a relay.

**Recommendation**: Detect the environment. If Docker is detected, warn the user that hole punching may fail unless they are on native Linux or using advanced network drivers (Macvlan).
57 changes: 57 additions & 0 deletions Dockerfile.wireworm
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Multi-stage build for a clean, efficient image
FROM docker.io/golang:1.21-bookworm AS build-wireproxy
WORKDIR /usr/src/wireproxy
COPY . .
RUN make

# Build Stuntman from source in a dedicated stage
FROM docker.io/debian:bookworm AS build-stuntman
RUN apt-get update && apt-get install -y \
g++ \
make \
libboost-dev \
libssl-dev \
git \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/stuntman
RUN git clone https://github.com/jselbie/stunserver.git . && make

# Final image
FROM docker.io/golang:1.21-bookworm

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
wireguard-tools \
bash \
curl \
make \
iproute2 \
procps \
libssl3 \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy binaries from build stages
# Placing wireproxy in /app ensures the bash script finds the correct Linux binary
COPY --from=build-wireproxy /usr/src/wireproxy/wireproxy /app/wireproxy
COPY --from=build-stuntman /usr/src/stuntman/stunclient /usr/bin/stunclient
COPY --from=build-stuntman /usr/src/stuntman/stunserver /usr/bin/stunserver

# Copy source for 'go run' utilities
# Use a specific list or ensure we don't overwrite the correctly built binary
COPY . /app/
# Re-copy the correct binary JUST in case the 'COPY . /app/' overwrote it with a host binary
COPY --from=build-wireproxy /usr/src/wireproxy/wireproxy /app/wireproxy

# Ensure scripts are executable
RUN chmod +x /app/wireworm_interactive.sh /app/wireworm.sh

# Metadata
LABEL org.opencontainers.image.title="WireWorm"
LABEL org.opencontainers.image.description="NAT Hole Punching PoC with userspace WireGuard and built-in STUN client"

# Entrypoint setup
ENTRYPOINT ["/bin/bash", "./wireworm_interactive.sh"]
92 changes: 92 additions & 0 deletions GO_WINDOWS_DNS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Specification: Go Windows P2P DNS Library (winp2pns)

## 1. Core Objectives
* **Namespace Scoping:** Intercept ONLY developer-defined domains (e.g., `*.p2p.local`) to ensure zero interference with the user's other web traffic.
* **Port Transparency:** Use DNS SRV records to map a friendly domain name to a dynamic local proxy port, removing the need for users to type ports in the game client.
* **Seamless Failover:** Implement a 0-TTL (Time-to-Live) policy to allow the utility to switch between P2P and Relay IPs/Ports instantly.
* **System Integration:** Utilize the Windows Name Resolution Policy Table (NRPT) for "Split-Horizon" DNS without modifying global network adapter settings.

---

## 2. Component Architecture

### 2.1 NRPT Manager (Registry Integration)
The library manipulates the Windows NRPT to tell the OS: *"If a query ends in .p2p.local, ask the DNS server at 127.0.0.1; otherwise, use the ISP."*

**Registry Path:**
`HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig\{GUID}`

| Value Name | Type | Value / Purpose |
| :--- | :--- | :--- |
| `Name` | REG_MULTI_SZ | The namespace suffix (e.g., `.p2p.local`) |
| `GenericDNSServers` | REG_SZ | `127.0.0.1` |
| `ConfigOptions` | REG_DWORD | `1` (Enables the rule) |

### 2.2 DNS Responder (UDP 53)
A lightweight DNS server implemented using the `github.com/miekg/dns` package.

* **Listener:** Binds to `127.0.0.1:53` (UDP).
* **Authoritative Flag:** All responses must have the `Authoritative` bit set to true.
* **Caching Policy:** All Resource Records (RRs) must have a `TTL` of `0`.

### 2.3 State Provider (Interface)
The library consumes an interface to retrieve real-time tunnel information.

```go
type ProxyState struct {
LocalPort uint16 // The local port the proxy is currently listening on
IsActive bool // If false, the DNS server returns RCODE_NAME_ERROR or RCODE_REFUSED
}

type TunnelProvider interface {
GetProxyState(hostname string) (ProxyState, error)
}
```

---

## 3. Technical Workflow

### 3.1 Initialization Sequence
1. **Privilege Validation:** Check if the process has Administrative/System privileges.
2. **Namespace Registration:** Generate a unique GUID and create the NRPT registry subkey.
3. **Socket Binding:** Attempt to bind to UDP port 53 on `127.0.0.1`.
* *Fallback:* If port 53 is blocked, notify the user or attempt to bind to `127.0.0.2`.
4. **Service Start:** Start the `dns.Server` loop.

### 3.2 DNS Query Resolution Logic
When a DNS query is received:

1. **Suffix Validation:** If the query does not match the registered namespace, ignore or return `RCODE_REFUSED`.
2. **Minecraft SRV Handling:**
* **Query:** `_minecraft._tcp.[server].p2p.local` (Type: SRV)
* **Response (Answer):** `SRV 0 0 [ProxyState.LocalPort] tunnel.[server].p2p.local`
* **Response (Additional):** `tunnel.[server].p2p.local A 127.0.0.1`
3. **Standard A-Record Handling:**
* **Query:** `[any].p2p.local` (Type: A)
* **Response:** `A 127.0.0.1`



### 3.3 Cleanup Sequence
On application exit (Signal or Graceful):
1. Stop the DNS listener loop.
2. Delete the specific GUID subkey from `HKLM\...\DnsPolicyConfig`.
3. Flush the Windows DNS cache via `dnscache` service or `ipconfig /flushdns` command.

---

## 4. Proposed Go Package Structure

```text
winp2pns/
├── nrpt_windows.go # NRPT logic using golang.org/x/sys/windows/registry
├── dns_handler.go # UDP server logic using github.com/miekg/dns
├── provider.go # Interface and state struct definitions
└── privilege.go # Manifest/Token checks for Admin rights
```

## 5. Implementation Constraints
* **Conflict Management:** The library must handle cases where the NRPT registry key already exists from a previous crash by overwriting it with the new local configuration.
* **Response Latency:** Since Minecraft checks DNS before connecting, the `TunnelProvider.GetProxyState` call must be non-blocking or extremely low-latency.
* **Bedrock Support:** For Bedrock edition, if SRV records are ignored, the utility should ideally attempt to use the default port `19132` locally to ensure a "port-less" experience.
92 changes: 92 additions & 0 deletions HOLEPUNCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# NAT Hole Punching for WireProxy

This feature adds NAT hole punching capability to wireproxy, enabling peer-to-peer WireGuard tunnel establishment without manual IP/port configuration.

## Quick Start

### Host (expose a service like Minecraft)
```bash
./wireproxy --holepunch --expose 25565
```

You'll see:
```
🔍 Discovering NAT mapping...
✓ NAT discovered: 203.0.113.5:51820

═══════════════════════════════════
Share this code with your peer:
93-raven-honey
═══════════════════════════════════
```

### Joiner (connect to friend's service)
```bash
./wireproxy --holepunch --code 93-raven-honey --local 25565
```

Then connect to `localhost:25565` to reach your friend's server.

---

## Manual Testing Steps

### Prerequisites
```bash
go build ./cmd/wireproxy
go build ./cmd/rendezvous
```

### Test 1: STUN Discovery
```bash
./wireproxy --holepunch --expose 25565
# Should show your public IP and a wormhole code
# Press Ctrl+C to exit
```

### Test 2: Full Exchange (Local)

**Terminal 1 - Rendezvous Server:**
```bash
./rendezvous
```

**Terminal 2 - Host:**
```bash
./wireproxy --holepunch --expose 8080
# Note the code shown (e.g., "42-banana-sunset")
```

**Terminal 3 - Joiner:**
```bash
./wireproxy --holepunch --code 42-banana-sunset --local 8080
```

### Test 3: Manual Fallback (No Server)

If rendezvous is unavailable, the tool falls back to manual mode:
```
⚠️ Rendezvous server unavailable. Using manual exchange.

Your connection string:
hp://ABC123...@203.0.113.5:51820

Paste peer's connection string: _
```

---

## CLI Reference

| Flag | Description |
|------|-------------|
| `--holepunch` | Enable NAT hole punching mode |
| `--expose <port>` | Host mode: expose this local port |
| `--code <code>` | Join mode: peer's wormhole code |
| `--local <port>` | Join mode: local port to bind |

---

## Known Limitations

⚠️ **Docker Desktop**: NAT hole punching does NOT work inside Docker containers on Mac/Windows due to VPNKit/gVisor symmetric NAT. Run wireproxy natively on the host instead.
61 changes: 61 additions & 0 deletions WIREWORM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# WireWorm PoC: Userspace WireGuard File Transfer

WireWorm is a Proof of Concept (PoC) demonstrating how to leverage **wireproxy** (a userspace WireGuard client) for secure, end-to-end encrypted file transfers between two peers behind consumer NATs using **NAT Hole Punching**.

## How it Works

Standard file transfer tools often rely on a central relay (like Magic Wormhole's transit relay) to bypass NAT. WireWorm instead uses WireGuard's native ability to punch holes in UDP firewalls.

1. **Consistent Port Binding**: Both peers bind to a specific local UDP port (e.g., 51820).
2. **UDP Hole Punching**: Both peers simultaneously attempt to send packets to each other's public IP/Port. This "punches" a hole in the NAT firewall.
3. **Userspace Networking**: Using `wireproxy` with `gVisor netstack`, a full TCP/IP stack is established over the WireGuard tunnel entirely in userspace.
4. **Tunnels**:
* The **Sender** exposes a local HTTP file server via a `[TCPServerTunnel]` on the WireGuard interface.
* The **Receiver** maps the sender's WireGuard IP/Port to a local port using a `[TCPClientTunnel]`.

## Features

- **P2P File Transfer**: High-speed, secure file transfers using standard HTTP over WireGuard.
- **Instant Chat**: Secure, private, end-to-end encrypted chat session between peers.
- **No Root Required**: Everything runs in userspace.
- **NAT Traversal**: Automatic UDP hole punching to bypass restrictive firewalls.

## Usage (Interactive)

The easiest way to use WireWorm is via the interactive script:

```bash
cd wireproxy
bash wireworm_interactive.sh
```

### Usage (Docker)

You can also run WireWorm in a container.

#### On Linux (Native Docker):
```bash
docker run -it --rm --network host wireworm
```

#### On macOS / Windows (Docker Desktop):
On Mac and Windows, Docker runs inside a virtual machine, so `--network host` doesn't provide direct access to your Mac's network. You must use explicit port mapping:

```bash
docker run -it --rm \
-e WIRE_PORT=51820 \
-p 51820:51820/udp \
wireworm
```

**Why this is necessary:**
- **The Chain**: `Internet (Public Port)` $\to$ `Router` $\to$ `Mac/Win (Host Port)` $\to$ `Docker VM` $\to$ `Container (Local Port)`.
- Without `-p`, your Mac doesn't know to forward incoming P2P packets from the internet into the Docker VM.
- **WIRE_PORT** ensures the script inside Docker actually uses the port you've opened on your host.

1. **Select Mode**: Choose between File Transfer or Chat.
2. **Exchange Connection String**: The script will provide a single string (e.g., `IP:PORT:PUBKEY`) to share with your peer.
3. **Establish Secure Tunnel**: Once both peers enter each other's strings, the NAT hole is punched and the WireGuard handshake begins.
4. **Interact**:
* **In Chat Mode**: The chat session will begin automatically in your terminal.
* **In File Mode**: Use the provided `curl` command to download the file.
Loading