VeillaxandClaude Sonnet 5.5 3cfe896fdd
CI / test (push) Successful in 7m14s
Release / release (push) Successful in 6m41s
Add Gitea Actions CI and release workflows and deployment assets
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 03:05:28 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:33:37 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00
2026-10-26 02:50:03 -06:00

Hammerhead

Self-hosted control plane for tunnelled ports and websites: a web dashboard, accounts and SSO, alerts, and a database, with no config files.

Hammerhead is the hub of the sharks stack. It runs the dashboard and API, and keeps your ports, domains, organizations, users, alerts and logs in an encrypted SQLite database. Edges and site agents connect to it and do whatever you set up in the UI.

Repo Role Runs on
Hammerhead (this repo) Control plane: dashboard, API, accounts, database anywhere the other two can reach
Blacktip Edge: owns public IPs, opens ports, WireGuard endpoint, enforces logins (optionally drives Traefik) public server(s)
Bamboo Site agent: WireGuard peer inside a private network, outbound only each private network
Reef Shared library imported by all three build time only

Every Blacktip also gets a built-in "this machine" site, so services on the same box (e.g. several Minecraft proxies) work without running Bamboo at all.

Quick start

go env -w GOPRIVATE=git.veillax.com      # once, so Go fetches straight from this server

# 1. control plane (this repo); open http://localhost:8080 and create the first admin
git clone https://git.veillax.com/Veillax/hammerhead && cd hammerhead
go build -o bin/ ./cmd/hammerhead
cp .env.example .env
bin/hammerhead -genkey                   # paste the output into .env as HAMMERHEAD_MASTER_KEY=
bin/hammerhead -listen :8080 -url https://hammerhead.example.com

# 2. Server -> Edges -> add an edge (name + public host:UDP-port, e.g. 203.0.113.10:51820);
#    the UI shows the command to run on the public server
go install git.veillax.com/Veillax/blacktip/cmd/blacktip@latest
blacktip -hammerhead https://hammerhead.example.com -token <edge token> -tunnel :51820 -docker

# 3. optional, for another network: Sites -> add a site, run on a machine there
go install git.veillax.com/Veillax/bamboo/cmd/bamboo@latest
bamboo -hammerhead https://hammerhead.example.com -token <site token> -docker

Open the edge's WireGuard UDP port (and any ports you open) in its firewall. Bamboo needs no inbound ports. Then open ports on the Resources page.

Deploying

Releases are built by Gitea Actions. Pushing a tag such as v0.2.0 runs .gitea/workflows/release.yml, which tests the code, builds Hammerhead for Linux (amd64, arm64, armv7), Windows (amd64) and macOS (amd64, arm64), publishes the archives with a SHA256SUMS file on the Releases page, and pushes a multi-architecture container image to this server's registry (git.veillax.com/veillax/hammerhead). Every push and pull request runs .gitea/workflows/ci.yml: formatting, vet, tests, and a check that every release platform still compiles.

On a Linux server (systemd)

curl -LO https://git.veillax.com/Veillax/hammerhead/releases/download/v0.1.0/hammerhead_v0.1.0_linux_amd64.tar.gz
curl -LO https://git.veillax.com/Veillax/hammerhead/releases/download/v0.1.0/SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
tar xzf hammerhead_v0.1.0_linux_amd64.tar.gz && cd hammerhead_v0.1.0_linux_amd64
sudo ./install.sh                 # installs the binary and a hardened systemd service
sudoedit /etc/hammerhead/hammerhead.env   # your settings and secrets
sudo systemctl start hammerhead

Use linux_arm64 or linux_arm for Raspberry Pi and other ARM machines. To upgrade, repeat the same steps: your settings and data are kept. sudo ./install.sh --uninstall removes the service and binary but leaves /etc/hammerhead and /var/lib/hammerhead alone. hammerhead -version tells you which build is running.

With Docker

cp .env.example .env              # fill it in
docker compose -f deploy/docker-compose.yml up -d

The image is git.veillax.com/veillax/hammerhead:<version> (and :latest for full releases). The compose file is in the repository and inside each Linux archive.

Setting up the build (once)

  • A Gitea Actions runner with the label ubuntu-latest, and Docker on it if you want the container image.
  • Nothing else is required: the workflows use the automatic job token. If your server doesn't let that token publish releases or packages, add the secrets RELEASE_TOKEN and REGISTRY_TOKEN (access tokens with release and package write permission) and optionally the variable REGISTRY_USER.
  • To skip the container image, set the repository variable PUBLISH_DOCKER to false.
  • Build locally with sh deploy/build.sh (archives in dist/).

Configuration and secrets

All three programs read a .env file from their working directory before they start (or the file named by -env path). Real environment variables always win over the file. Copy .env.example, which lists every setting, and keep .env out of version control (.gitignore already does).

What belongs in it: HAMMERHEAD_MASTER_KEY, the first admin's credentials, the edge and site ..._TOKEN values, HAMMERHEAD_METRICS_TOKEN, and DNS-provider credentials for wildcard certificates.

Secrets at rest. Hammerhead keeps everything in one SQLite file (hammerhead.db in its data folder) and encrypts the secrets inside it with AES-256-GCM: edge and site tokens, 2FA secrets, OIDC client secrets, SMTP and relay passwords, DKIM private keys, webhook URLs. Passwords are bcrypt hashes; PINs and resource passwords likewise. With the wrong key Hammerhead refuses to start rather than reading garbage. If you don't set HAMMERHEAD_MASTER_KEY, a key is generated into <data>/master.key so it works out of the box, but then a backup of the folder carries the key too; copy the file's contents into .env and delete it. Back the key up separately: without it the encrypted secrets cannot be recovered.

Database

SQLite (pure Go, no C compiler needed), in WAL mode. Configuration is written incrementally in one transaction per change, so it survives crashes and power loss. Logs, traffic history and alerts are proper indexed tables: access logs are searchable and paginated, old entries are pruned (HAMMERHEAD_LOG_DAYS, default 30; audit log a year). An existing state.json and JSONL logs from an earlier version are imported automatically on first start and kept as *.migrated.

Back up by copying the data folder while Hammerhead is stopped (or use sqlite3 .backup), plus your master key.

The tunnel: WireGuard

Blacktip and Bamboo are WireGuard peers, using wireguard-go with a userspace network stack (the same approach Newt uses), so there is no root requirement, no kernel interface and no driver; it works on Windows, macOS and Linux alike.

  • Hammerhead allocates each edge and site an address in 100.89.0.0/16 (it exists only inside the tunnel, so it can't clash with any LAN or VPN) and distributes public keys. Private keys never leave the machine that made them (wg.key in each agent's data directory).
  • Bamboo dials out to every edge it is used from and keeps the NAT mapping open (keepalive 25s), so sites behind NAT or CGNAT work. Each peer is limited to its own /32.
  • TCP goes over a TCP connection inside the tunnel; UDP uses native datagrams (no UDP-over-TCP head-of-line blocking, good for game servers).
  • Adding or removing a site changes peers in place; existing tunnels are not reset.

Two ways to serve web resources

built-in proxy (default) Traefik (-traefik /path/to/traefik)
http/https routing, HTTP/2, websockets yes yes
Let's Encrypt TLS-ALPN-01, per-subdomain certificates for wildcard hostnames TLS-ALPN-01, or DNS-01 with -acme-dns <provider> for true wildcard certificates
Logins, rules, share links in Blacktip same logic, called by Traefik's forward-auth
Dependency none a Traefik v3 binary

With Traefik, Blacktip supervises the process: it writes Traefik's static config, restarts it only when the set of web ip:port listeners changes (Traefik declares its ports statically), and hot-reloads routes, logins and targets through the file provider. Targets are reached through a loopback relay port per resource into the WireGuard tunnel, so health checks and failover work the same. Raw TCP, UDP and TLS-passthrough ports never go through Traefik: it can't open ports without a restart, and opening ports from the UI instantly is the point of this project, so Blacktip's own listeners handle those.

-no-acme makes Traefik serve its built-in self-signed certificate (useful on private networks). DNS-challenge credentials are passed as environment variables to Blacktip, exactly as Traefik documents for each provider (e.g. CF_DNS_API_TOKEN for cloudflare); .env is the place for them.

Features

Ports

  • The same port on different IPs (x.y.z.104:51000 and x.y.z.209:51000 go to different targets). Blacktip lists the IPs it finds; you can also type one.
  • Port ranges for tcp and udp: 2456-2458 opens all three, each sent to the target port plus the same offset (up to 1000 ports per entry; a busy port inside a range is named in the status).
  • TCP, UDP, "TCP + UDP" in one click, HTTP and HTTPS reverse proxy, and TLS passthrough by SNI.
  • PROXY protocol (v1/v2) on tcp and tls ports: the target is told the real client address, so IP bans, rate limits, spam checks and logs see the visitor instead of the tunnel. Your server must be set to expect it and to trust only the edge or site: Velocity/BungeeCord haproxy-protocol, Postfix postscreen_upstream_proxy_protocol = haproxy, Dovecot haproxy_trusted_networks, nginx proxy_protocol.
  • Many hostnames share one ip:port; *.example.com wildcards work.
  • Several targets per port: round-robin load balancing with automatic failover.
  • Health checks (TCP connect or an HTTP path); unhealthy targets are skipped and shown in the UI.
  • Conflict detection (same ip/port/protocol, overlapping ranges, or 0.0.0.0 overlapping a specific IP).
  • Traffic totals and a 24-hour sparkline per port.

HTTPS: automatic Let's Encrypt certificates, optional port-80 redirect, X-Forwarded-* headers, Host rewrite, HTTPS to the backend, certificate status per port.

Login in front of any web port (like Pangolin's resource auth)

  • Sign in with a Hammerhead account (limited to roles or everyone in the org), a shared password, a PIN, or an emailed one-time code.
  • Expiring, use-limited share links that skip the login.
  • Path and IP/CIDR rules that skip the login or block requests.
  • The backend gets X-Hammerhead-User (client-supplied copies and header aliases are removed) and never sees the session cookie.
  • Sessions can be revoked. Visitor cookies are stateless tokens carrying a per-resource epoch. Hammerhead bumps it, and edges refuse older tokens within seconds, when a member is removed, a role changes or is deleted, a user is deleted, the password/PIN or allowed methods change, a share link is revoked, or you press Sign out visitors.

Accounts

  • Organizations, roles (admin and custom), invitations (by link or email), member management.
  • Two-factor authentication (TOTP) with backup codes. Password changes sign out other sessions.
  • Signed-in devices on the Account page: see where you're signed in and end any session, or all the others; admins can sign a user out everywhere.
  • OpenID Connect single sign-on (Google, Authentik, Keycloak, Entra ID…) with auto-provisioning.
  • Org-scoped API keys, a "My apps" page for members, brute-force throttling.
  • Domains are owned per org and checked against DNS; hostnames must sit under your own domains.

Alerts

  • Email or webhook (JSON, Slack, Discord, ntfy) when an edge or site goes quiet, a port can't bind, a target fails its health check, a certificate errors, or a mail relay isn't deliverable (reverse DNS, HELO, port 25, DKIM record missing).
  • A problem must last the debounce time (default 2 minutes, per organization) before you hear about it, so restarts and blips stay quiet. One message when it starts, one when it clears, optional reminders. Active alerts are stored, so restarting Hammerhead doesn't repeat them, and an alert is only marked resolved once the thing it describes has been re-checked.
  • A dead edge silences its own ports' alerts (the edge alert already says it). Channels are per organization; server channels get edge alerts.
  • Webhooks can't be pointed at internal addresses (no metadata service, no LAN) unless a server admin allows it for the organization, and redirects are not followed.
  • /metrics (Prometheus) when HAMMERHEAD_METRICS_TOKEN is set: edge/site/port/target/mail-check gauges, traffic counters, alerts firing. Off (404) otherwise.

Mail egress (send your mail server's outgoing mail from the edge's IP)

  • Receiving servers judge mail by the IP it comes from: its reverse DNS, your HELO name and your SPF record all have to agree. A mail server at home usually can't satisfy that (residential IP, port 25 blocked). Mail egress lets it send from a VPS IP you control instead.
  • Bamboo opens a small SMTP relay on the mail server's network (default 127.0.0.1:2525). Point your MTA at it (Postfix: relayhost = [127.0.0.1]:2525). Each message crosses the WireGuard tunnel and Blacktip delivers it straight to the recipient's server (MX lookup, MX fallback rules, opportunistic STARTTLS) from the source IP you choose, announcing the HELO name you set. The recipient's replies go straight back, so your server's queue, retries and bounces behave normally. Or set a smarthost to relay through a provider that allows your edge IP.
  • DKIM signing at the edge. Create a key per sending domain on the Mail egress page (RSA 2048, generated by Hammerhead, private half encrypted at rest and held only by the edge); publish the TXT record it shows. Mail is signed by From domain (a parent domain's key covers subdomains); a message your mail server already signed for that domain is left alone, and a signing problem sends the mail unsigned rather than dropping it. Hammerhead checks the DNS record and alerts if it's missing or wrong.
  • MTA-STS enforcement. When a recipient domain publishes an enforce policy, mail only goes to the servers it lists, over TLS with a verified certificate; otherwise it's a temporary failure so your server retries. (testing mode doesn't block.) Policies are cached for their max_age. The policy fetch can't reach internal addresses or follow redirects.
  • The page shows an edge-side deliverability checklist: reverse DNS matches the HELO name, the HELO name resolves back to the IP, outbound port 25 is open, plus the SPF record to publish.
  • Guard rails: only the owning site can use an egress; recipients' servers on internal address ranges are refused; a per-minute send limit (default 60) protects the IP's reputation if the mail server is compromised; the relay requires a password if it listens beyond 127.0.0.1; optional "require verified TLS"; every delivery and refusal is in the access log (recipient domain, server used, SMTP status; never message content).
  • One transaction goes to one recipient domain; extra domains get 452 and your MTA resends them separately, as MTAs expect. 50 MB limit.
  • Deliberately no queue at the edge. Delivery is synchronous, so your mail server's own queue does the retrying. Accepting mail at the edge and bouncing it later would create backscatter (bounces to forged senders, which gets IPs blacklisted) and leave stored mail on a public server.
  • Not included: smarthost authentication (smarthosts must allow the edge IP), DANE, and receiving mail (use a normal TCP port with PROXY protocol for that).

Operations

  • Searchable, paginated access log (per request for web ports, per connection for TCP, per message for mail) and audit log.
  • Docker container discovery on any site or edge (-docker): pick a container as a target.
  • Server admins decide which orgs may use each edge, and whether an org may target the edge machine itself (off by default for extra orgs, since that includes Hammerhead).

How logins work

Blacktip redirects an unauthenticated visitor to Hammerhead's login page. Hammerhead signs a short-lived token with a per-edge key and sends the visitor back; Blacktip verifies it and sets a cookie. The same decision function runs in both proxy modes.

Limits and notes

  • No client-to-site VPN (Pangolin's Olm/private resources) and no blueprints/YAML import.
  • Hammerhead serves plain HTTP; put it behind HTTPS (set -url) or keep it on a private network.
  • SQLite means one Hammerhead instance (no clustering); fine for personal and small-team use.
  • PROXY protocol covers tcp and tls ports, not udp.
  • A Blacktip can only bind IPs that exist on its machine. Cloud elastic IPs that are NATed must be bound by their private address.
  • Let's Encrypt needs port 443 on the edge reachable from the internet and DNS pointing at it.
  • Traefik's access log is imported into Hammerhead; the file is truncated when it passes 64 MB.
  • Tests: go test ./.... The e2e/ folder runs Hammerhead, Blacktip and Bamboo together (it pulls the other two in as test dependencies); the Traefik test runs when TRAEFIK_BIN points at a Traefik binary.
S
Description
Self-hosted control plane for tunnelled ports and websites: a web dashboard, accounts and SSO, alerts, and a database, with no config files.
Readme AGPL-3.0
619 KiB
0 Stars 1 Watchers 0 Forks
v0.1.20
Latest
2026-10-31 02:13:22 -06:00
Languages
Go 70.6%
HTML 24.9%
Shell 2.9%
CSS 1.1%
JavaScript 0.4%
Other 0.1%