-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
You are about to put a port on the public internet and point it at a machine inside your home network. It's fair to want to know exactly what's protecting it.
Agent ↔ gateway: TLS 1.3, and nothing else. Both sides refuse anything older. The tunnel between your machine and the gateway is encrypted and authenticated.
Players ↔ gateway: whatever Minecraft does. proxyforward doesn't change that. The player-facing leg is ordinary Minecraft traffic on an ordinary TCP port.
Trust comes from a pin, not a CA. On first start, the gateway generates a self-signed ECDSA P-256 certificate. There is no certificate authority, no Let's Encrypt, no third party in the trust path — and so nothing to leak, expire or get tricked into issuing. Instead:
- the certificate's SHA-256 fingerprint is carried out of band, inside the pairing code;
- the agent pins that exact fingerprint, and verifies the gateway against it on every connection;
- a mismatch is a hard failure with a message telling you to re-pair.
The certificate is long-lived on purpose. Its expiry isn't what's protecting you — the pin is.
pf1://mc.example.com:8474/<token>#sha256:<64 hex>
│ │ │ │
└ gateway host ┘ │ └ the certificate to pin
└ the shared auth token
Two secrets, one string, delivered by a channel you already trust (you copy it between your own two machines).
Treat the pairing code like a password. Whoever holds it can authenticate to your gateway and register tunnels on it. Don't paste it into a public issue, a Discord channel, or a screenshot. If it leaks, rotate the token (Settings → Security) — every agent then has to re-pair.
Both the token and the fingerprint are compared in constant time, so an attacker can't recover them a byte at a time by measuring how long a rejection takes.
Your gateway's control port will be found by scanners. It's built for that.
- The unauthenticated prologue is on a short clock. Everything before authentication — the TCP accept, the TLS handshake, the first frame — must complete within a hard deadline or the connection is dropped. Nobody gets to hold a socket open doing nothing.
- Pre-auth frames are capped far smaller than post-auth frames, and the size is checked before any memory is allocated. A scanner cannot make the gateway allocate a large buffer by claiming a huge frame.
- Failed authentication is rate-limited per IP, fail2ban style. Successful authentications never count toward the limit — so an agent on a flaky connection that reconnects fifty times is never locked out, while a token brute-forcer is.
- Connection gates, globally and per source IP, cap how many public connections can exist at once (defaults in Configuration).
-
A port allowlist (
gateway.port_allowlist) can restrict which public ports an agent is allowed to ask for. Empty means any — set it if you don't fully trust the holder of the pairing code.
The exact deadlines, caps and limits are compile-time constants, listed in
docs/agent/architecture.md → "The numbers".
They're deliberately not restated here, so this page can't quietly go stale.
Every parser that an unauthenticated stranger can reach — the control frames, the Minecraft handshake, the login sniffer — is continuously fuzzed in CI.
The agent has a persistent random identity, separate from the token.
- The same agent reconnecting supersedes its old session. That's what makes a reconnect after a network blip clean, rather than leaving a zombie session holding your port.
- A different agent presenting the same token is rejected, with a clear message. Without this, two machines sharing a pairing code would fight, each kicking the other off forever.
If you see "another agent is already connected", that's this rule doing its job — see Troubleshooting.
Authentication failures that can never succeed by retrying — a bad token, an agent conflict, an incompatible protocol version — stop the agent instead of making it hammer the gateway forever. The reason is surfaced in the UI rather than buried in a log.
- The named pipe the GUI uses to talk to the engine is local-only, and its permissions admit only Administrators, SYSTEM and the interactive user. It is not reachable from the network.
- The Windows Firewall rule is scoped to the program, not to a port — so it doesn't quietly leave a port open after you change your config.
- Diagnostics exports are redacted: secrets, hostnames, IP addresses and player identities are stripped, with peer IPs replaced by stable pseudonyms. There's an automated test that fails the build if anything leaks.
Be clear-eyed:
-
Your config file holds the auth token in plain text. Anyone who can read
%APPDATA%\proxyforward\config.tomlhas it. - The gateway machine sees all player traffic in the clear as it relays it. It's a proxy — that's inherent. Run your gateway somewhere you actually control.
- proxyforward does not authenticate players. That's Minecraft's job (online mode). An offline-mode server behind a tunnel is still an offline-mode server.
- The binaries are unsigned. Verify a release's provenance attestation instead — see Installation.
proxyforward · Issues · GPL-3.0 — Windows only, TCP only. Check Not Yet Implemented before filing a bug.