Download the appropriate binary for your platform from the Releases page:
| Platform | Architecture | Download |
|---|---|---|
| Linux | x86_64 | weft-linux-amd64 |
| Linux | ARM64 | weft-linux-arm64 |
| macOS | Intel | weft-darwin-amd64 |
| macOS | Apple Silicon | weft-darwin-arm64 |
| Windows | x86_64 | weft-windows-amd64.exe |
# Example: Download and install on Linux x86_64
curl -LO https://github.com/aquaduct-dev/weft/releases/latest/download/weft-linux-amd64
chmod +x weft-linux-amd64
sudo mv weft-linux-amd64 /usr/local/bin/weftdocker pull ghcr.io/aquaduct-dev/weft:latestWeft is a Layer 4/Layer 7 tunnelling proxy built around wireguard-go and designed for scalable, secure hosting of internet-facing resources from environments which lack public internet access.
Start a Weft server on a host with at least one publically routable IP:
weft server
Optional arguments:
--bind-ip: Comma separated list of IPs that the server will listen on (defaults to auto-discovery).--port: Server connection port (default 9092).--email: Email address for LetsEncrypt ACME registration.--connection-secret: Manually set the connection secret.--secret-file: Path to write the generated connection secret to.--certs-cache-path: Path to cache ACME certificates.--bind-interface: Interface to bind the IP to (e.g.,eth0). If not set, the IP is not bound (the existing system IPs are used).--usage-reporting-url: URL to post usage reports to.--cloudflare-token: Cloudflare API Token for DNS updates.
The server will print a connection secret on startup.
Start a Weft tunnel to expose a local service:
weft tunnel weft://{connection-secret}@{your-server-ip} [local url] [remote url]
Supported protocols:
tcp>tcp(plain proxy)tcp>http(proxy HTTP requests from domain to TCP server)tcp>https(terminate and proxy HTTPS requests from domain to TCP server)http>http(proxy HTTP requests from domain to HTTP server)http>https(terminate and proxy HTTPS requests from domain to HTTP server)https>https(proxy HTTPS requests from domain to HTTPS server, stripping encryption and re-encrypting)udp>udp(proxy UDP)
Optional arguments:
--tunnel-name: Logical name for the tunnel (defaults to hash of src|dst).--verbose: Log detailed connection information.
Example:
weft tunnel weft://secret@1.2.3.4 http://localhost:8080 https://my-app.example.com
The tunnel supports URL syntax for HTTP routes which add significant functionality.
The tunnel can rewrite HTTP paths. For example, take this command:
weft tunnel weft://secret@1.2.3.4/test http://localhost:8080 https://my-app.example.com/api/v1
A request to 'https://my-app.example.com/api/v1/create` will be tunneled to http://localhost:8080/test/create.
The tunnel can check query parameters and route based on them. For example:
weft tunnel weft://secret@1.2.3.4 http://localhost:8080 https://my-app.example.com?token=[token-regex]
A request to 'https://my-app.example.com/api/v1/create` will be tunneled to http://localhost:8080/api/v1/create - but only if the query string contains a key named token with a value that matches [token-regex]. Regexes are parsed using RE2 syntax.
The tunnel can check request headers and route based on them. For example:
weft tunnel weft://secret@1.2.3.4 http://localhost:8080 https://my-app.example.com#Authorization=[authorization-regex]&Set-Cookie=.*
A request to 'https://my-app.example.com/api/v1/create` will be tunneled to http://localhost:8080/api/v1/create - but only if the request has an Authorization header matching [authorization-regex] AND a Set-Cookie header matching .* (i.e. any value).
The tunnel can check request methods and route based on them. For example:
weft tunnel weft://secret@1.2.3.4 http://localhost:8080 https://my-app.example.com#POST&Authorization=[authorization-regex]
A request to 'https://my-app.example.com/api/v1/create` will be tunneled to http://localhost:8080/api/v1/create - but only if the request has an Authorization header matching [authorization-regex] AND is a POST request.
The tunnel can modify incoming request headers. For example:
weft tunnel weft://secret@1.2.3.4 http://localhost:8080/#Forwarded=true&X-Forwarded-For=!del&Authorization=+auth https://my-app.example.com
A request to 'https://my-app.example.com/api/v1/create` will be tunneled to http://localhost:8080/api/v1/create. The request that is sent to http://localhost:8080 will have header Forwarded set to true, X-Forwarded-For removed, and Authorization set to auth if it was not previously set.
It is even possible to set up a tunnel which does not tunnel at all, but just configures an HTTP redirect. For example:
weft tunnel weft://secret@1.2.3.4 http://localhost:8080/#redirect https://my-app.example.com
A request to 'https://my-app.example.com/api/v1/create` will get an HTTP 302 redirect to http://localhost:8080.
The weft implementation will evaluate rules in order from most specific to least specific. Route A is considered more specific than route B if route A has more matchers.
Matchers can be easily combined just by including the URL components. For example:
weft tunnel weft://secret@1.2.3.4 http://localhost:8080/api?query=true#X-Forwarded-For=!del&Authorization=+auth https://my-app.example.com/api/v1/
List Tunnels:
weft list [server url]
Lists active tunnels on the server.
--connection-secret: Connection secret (if not in URL).--human-readable,-l: Print bytes in human-readable format.
Probe ACME:
weft probe [domain]
Checks if the server can answer an ACME challenge for the given domain.
--bind-ip: IP to bind the probe listener to.
One-off Proxy:
weft proxy [src url] [dst url]
Starts a standalone proxy between two URLs.
--proxy-name: Logical name for the proxy.
Weft uses wireguard-go to implement tunnels. The server reserves a 10.1.0.0/16 block of addresses for the internal WireGuard network, and keeps track of which proxies have been assigned which addresses. Each assigned address may only be used by the client to which it has been assigned; this is enforced in the peer config of the server Wireguard proxy. The client side config allows the server to access from any IP.
When clients are removed, the address is freed and returned to the pool. Subsequent clients may re-use the same address.
Both the server and the tunnel must run proxies. The /connect method must establish a remote port listener on the server - a TCP/UDP proxy if necessary, or a VHostProxy if the connection uses HTTP/HTTPS.
TCP and UDP proxies are simple on the server side: they simply proxy to the tunnel-side TCP/UDP proxy, which then forwards the request to whatever local target is being tunneled.
HTTP and HTTPS proxies are more complicated. Each server-side VHostProxy must target the tunnel proxy port. On the tunnel side, the proxy port should only forward TCP to the local HTTP server.
The Weft server uses a self-signed TLS certificate. To prevent man-in-the-middle attacks, Weft uses an encrypted challenge-response protocol that also securely delivers the server's certificate to the client.
Authentication Flow:
- Client
GETs/login(usingInsecureSkipVerifyfor this initial request only). - Server generates a random nonce and returns the string "server-" encrypted with the connection secret.
- Client decrypts and verifies that the "server-" prefix is present — proving the server has the secret.
- Client encrypts the nonce and
POSTs it to/login. - Server decrypts the received ciphertext and verifies it matches the stored challenge.
- On success, Server issues:
- A short-lived JWT (~30m)
- The server's TLS certificate PEM, encrypted with the connection secret
- Client decrypts the certificate and uses it as the trusted root CA for all subsequent TLS connections.
- Client includes the JWT in the
Authorizationheader for subsequent requests.
Why This Prevents MITM:
- A man-in-the-middle attacker cannot decrypt the certificate because they don't have the connection secret.
- After the initial handshake, all connections are validated against the encrypted-and-delivered certificate.
- Even if an attacker intercepts the initial TLS handshake, they cannot produce a valid encrypted certificate that the client will trust.
Notes:
- All AES operations use the shared connection secret as the key. The nonce is single-use.
- The protocol allows both sides to prove they have the same secret without ever exchanging it.
- The JWT is used for all subsequent requests. When it expires, the client repeats the challenge to obtain a new token.
To ensure code quality and build consistency, you can set up a git pre-commit hook that automatically runs gazelle fix and bazel test //... before each commit.
To install the hook, run:
./scripts/setup-pre-commit-hook.sh