Mullgate
Guides

Usage

Full Mullgate usage guide, organized from the original repository source.

This guide expands on the consumer-facing repository README. It keeps the deeper setup, platform, runtime, verifier, and troubleshooting detail out of the landing page while staying anchored to the CLI help contract exposed by:

  • mullgate --help
  • mullgate setup --help
  • mullgate proxy start --help
  • mullgate proxy logs --help
  • mullgate proxy status --help
  • mullgate proxy doctor --help
  • mullgate proxy relay --help
  • mullgate proxy relay recommend --help
  • mullgate config --help
  • mullgate version --help
  • mullgate completions --help

Install forms

Mullgate has two operator-facing install forms:

  1. Installed mullgate command — the published npm package installed via npm, pnpm, bun, or the convenience installer scripts
  2. Packed release asset — the GitHub release .tgz artifact or extracted standalone binary

This guide uses installed mullgate ... commands by default. If you are using a release artifact instead, invoke the extracted mullgate binary with the same flags and subcommands shown here.

Platform support posture

Mullgate reports platform support truthfully on Linux, macOS, and Windows.

Platformpathstatus / doctorCurrent runtime execution
LinuxSupportedSupportedFully supported
macOSSupportedSupportedLimited — Docker Desktop host networking does not match Linux
WindowsSupportedSupportedLimited — Docker Desktop host networking does not match Linux

Use Linux for the full setup/runtime/probe workflow. On macOS and Windows, treat the CLI and runtime manifest as supported config and diagnostic surfaces, but use a Linux host or Linux VM when you need the shipped multi-route Docker runtime to behave truthfully end to end.

Non-interactive setup inputs

mullgate setup --non-interactive fails instead of prompting when required inputs are missing. You can supply values with flags, environment variables, or a mix of both.

Required for a normal non-interactive run

PurposeFlagEnvironment variable
Mullvad account number--account-number <digits>MULLGATE_ACCOUNT_NUMBER
Proxy username--username <name>MULLGATE_PROXY_USERNAME
One or more routed locations--location <alias> (repeatable)MULLGATE_LOCATION or MULLGATE_LOCATIONS

Common optional inputs

PurposeFlagEnvironment variable
Device label--device-name <name>MULLGATE_DEVICE_NAME
Bind host / shared private-network host IP--bind-host <host>MULLGATE_BIND_HOST
Shared private-network host IP or public per-route bind IPs--route-bind-ip <ip> (repeatable)MULLGATE_ROUTE_BIND_IPS
Exposure mode--exposure-mode <mode>MULLGATE_EXPOSURE_MODE
Base domain--base-domain <domain>MULLGATE_EXPOSURE_BASE_DOMAIN
Proxy password--password <secret>MULLGATE_PROXY_PASSWORD
SOCKS5 port--socks-port <port>MULLGATE_SOCKS_PORT
HTTP port--http-port <port>MULLGATE_HTTP_PORT
HTTPS port--https-port <port>MULLGATE_HTTPS_PORT
HTTPS certificate path--https-cert-path <path>MULLGATE_HTTPS_CERT_PATH
HTTPS key path--https-key-path <path>MULLGATE_HTTPS_KEY_PATH
Mullvad provisioning endpoint--mullvad-wg-url <url>MULLGATE_MULLVAD_WG_URL
Mullvad relay metadata endpoint--mullvad-relays-url <url>MULLGATE_MULLVAD_RELAYS_URL

Notes:

  • MULLGATE_LOCATION is shorthand for route 1.
  • MULLGATE_LOCATIONS is the ordered, comma-separated form for multi-route setup.
  • MULLGATE_ROUTE_BIND_IPS is also ordered and comma-separated.
  • If you omit MULLGATE_PROXY_PASSWORD, setup saves an empty password.
  • private-network uses one shared trusted-network host IP. If Tailscale is available during setup and you do not override it, Mullgate should default to the host's 100.x address.
  • public plus the default published-routes access mode still requires one explicit bind IP per routed location, and multi-route public exposure still requires distinct bind IPs.
  • inline-selector uses one shared host IP in every exposure mode because route selection moves to the proxy username.

Setup examples

Example: local two-route loopback setup

This is the easiest way to prove the CLI/runtime flow on one Linux machine:

export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD='replace-me'
export MULLGATE_LOCATIONS=sweden-gothenburg,austria-vienna
export MULLGATE_DEVICE_NAME=mullgate-loopback

mullgate setup --non-interactive
mullgate proxy access

What to expect:

  • route 1 gets 127.0.0.1
  • route 2 gets 127.0.0.2
  • the configured hostnames default to the route aliases in loopback mode
  • direct bind-IP entrypoints work immediately and are the canonical local access path
  • hostname access on the local machine works only after you install the emitted hosts block

Private-network exposure demo

Example: private-network hostnames with a base domain

Use this when clients should reach the proxies from your LAN, VPN, or overlay network and you want real hostnames per route:

export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD='replace-me'
export MULLGATE_LOCATIONS=sweden-gothenburg,austria-vienna
export MULLGATE_EXPOSURE_MODE=private-network
export MULLGATE_BIND_HOST=100.124.44.113
export MULLGATE_EXPOSURE_BASE_DOMAIN=proxy.example.com

mullgate setup --non-interactive
mullgate proxy access

What to expect:

  • route hostnames become sweden-gothenburg.proxy.example.com and austria-vienna.proxy.example.com
  • both hostnames resolve to the same shared private-network host IP
  • route 1 keeps the base ports, route 2 moves to the next ports (1081, 8081, 8444 when HTTPS is enabled)
  • mullgate proxy access prints the DNS A records operators must publish
  • each hostname must resolve to that shared host IP before hostname-based routing proof can work remotely

Example: direct-IP exposure with no base domain

If you do not set a base domain in private-network or public mode, Mullgate falls back to the bind IPs as the hostnames clients should use:

mullgate proxy access \
  --mode private-network \
  --clear-base-domain \
  --route-bind-ip 100.124.44.113

In that posture:

  • there are no DNS records to publish
  • direct host-IP entrypoints are the intended access path
  • mullgate proxy access is still useful for local-only hostname testing, but it is no longer required for remote clients

Example: private-network inline-selector access

Use this when trusted-network clients should connect to one shared host and select the Mullvad exit inline in the proxy username:

export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD=''
export MULLGATE_LOCATIONS=sweden-gothenburg,austria-vienna
export MULLGATE_EXPOSURE_MODE=private-network
export MULLGATE_BIND_HOST=100.124.44.113

mullgate setup --non-interactive
mullgate proxy access --access-mode inline-selector

What to expect:

  • one shared SOCKS5 listener and one shared HTTP listener on the trusted-network host
  • selector examples such as socks5://se:@100.124.44.113:1080 and socks5://se-got:@100.124.44.113:1080
  • exact relay selectors when the user wants a specific server, for example socks5://se-got-wg-101:@100.124.44.113:1080
  • mullgate proxy export does not emit route URLs in inline-selector mode

Use the guaranteed scheme://selector:@host:port form. The shorter scheme://selector@host:port form is best-effort only because some clients treat a missing password inconsistently.

For the supported selector families and patterns, see Inline Selector Reference.

Unsupported local config versions

The current CLI only operates on the current v2 config/runtime shape.

If an installed mullgate command reports an unsupported config version:

  • treat the reported config/runtime paths as stale local state
  • back up or remove the config file and runtime directory named in the error
  • rerun mullgate setup
  • rerun mullgate proxy start

Do not expect the current CLI to keep operating on an older runtime bundle in place.

Runtime control and support utilities

Use these commands after setup when you need to preflight, stop, restart, inspect, or hand support metadata to someone else.

Dry-run the rendered runtime bundle

mullgate proxy start --dry-run

Use this before touching Docker when you want Mullgate to rerender and validate the runtime artifacts but stop short of launching containers.

Stop or restart the saved bundle

mullgate proxy stop
mullgate proxy restart
  • proxy stop is the canonical clean shutdown for the saved runtime bundle
  • proxy restart is the canonical "stop, rerender, and start again" shortcut after a config or relay change

Read the saved Docker Compose logs

mullgate proxy logs --tail 200
mullgate proxy logs --tail 50 --follow

Use mullgate proxy logs right after mullgate proxy status when a runtime looks unhealthy and you want evidence from the saved bundle instead of guessing.

mullgate version
mullgate completions bash > ~/.local/share/bash-completion/completions/mullgate
  • mullgate version prints the CLI version, config schema, Node version, platform, architecture, and hostname for support reports
  • mullgate completions <bash|zsh|fish> prints shell completion scripts to stdout so you can install them in the shell-specific location you already use

Exporting proxy lists for clients

Use mullgate proxy export when you want a ready-to-paste inventory of authenticated route URLs instead of assembling client config by hand.

mullgate proxy export currently supports published-routes mode only. If the saved access mode is inline-selector, use mullgate proxy access to inspect the shared listener and selector examples instead.

Discover the curated region groups

mullgate proxy export --regions

This prints the built-in groups accepted by --region, such as europe and asia-pacific.

Guided proxies.txt export

mullgate proxy export --guided

The guided flow defaults to writing proxies.txt, only prompts for values you did not already pass on the CLI, and works both on a real terminal and with piped stdin. It now starts from actual country and region pick-lists, then lets you refine each selected batch with optional city, server, and provider filters.

Deterministic selector batches

mullgate proxy export \
  --country se --city got --count 1 \
  --region europe --provider m247 --count 2 \
  --output proxies.txt

Rules worth knowing:

  • selectors are consumed in CLI order
  • --count applies to the immediately preceding --country or --region
  • --city, --server, and repeated --provider values refine only the immediately preceding selector batch
  • each matched route is exported at most once, so later selectors only see routes that earlier selectors did not already consume
  • auto-generated filenames reflect the protocol plus selector batch order when you do not pass --output

Preview or stream the export

mullgate proxy export --dry-run --protocol http --country us --server us-nyc-wg-001 --owner rented
mullgate proxy export --stdout --protocol socks5 --region europe
  • --dry-run prints the real proxy URLs it would export and does not write a file
  • --stdout writes the real proxy URLs to stdout and prints the export summary on stderr
  • --force is required when you want to overwrite an existing export file

Relay discovery, recommendation, and exit verification

Use these commands when you want to move from broad selector intent to exact relay choices you can justify and verify.

Relay recommendation demo

Find relays that match a policy

mullgate proxy relay list \
  --country Sweden \
  --owner mullvad \
  --run-mode ram \
  --min-port-speed 9000

Use cases:

  • prefer Mullvad-owned infrastructure over rented servers
  • filter down to RAM-disk relays for a stricter operational posture
  • exclude slower relays before you probe anything

Probe likely candidates before pinning one

mullgate proxy relay probe --country Sweden --count 2

What this does:

  • starts from the selector you requested
  • picks a spread of likely candidates
  • runs ping against those relay IPs
  • ranks the successes by latency

Preview or apply exact relay recommendations

mullgate proxy relay recommend --country Sweden --count 1
mullgate proxy relay recommend --country Sweden --count 1 --apply

Use mullgate proxy relay recommend when you want Mullgate to translate a broad country or region request into exact relay hostnames.

  • without --apply, it stays advisory and prints the exact route it would use
  • with --apply, it pins the recommended relay hostname into saved config and refreshes the derived runtime artifacts
  • ordered selectors still matter, so you can mix country and region batches and keep deterministic intent

Verify a configured route really exits through Mullvad

mullgate proxy relay verify --route sweden-gothenburg

This verifies the configured route across the published proxy protocols and checks the JSON response from https://am.i.mullvad.net/json by default.

Use it when:

  • a recommended relay has been applied and you want a quick truth check
  • an operator needs to prove that SOCKS5, HTTP, and HTTPS surfaces all still exit through Mullvad
  • a route looks suspicious and you want a concrete per-protocol failure report instead of guessing

Hostname and direct-IP access

Mullgate exposes proxy entrypoints in two access models:

  • published-routes — route-aware hostnames or per-route ports such as sweden-gothenburg.proxy.example.com:1080 or 100.124.44.113:1081
  • inline-selector — one shared listener per protocol, with the selector in the username such as socks5://se:@100.124.44.113:1080

For the full selector syntax and supported selector families, see Inline Selector Reference.

When hostname access works

Hostname-based access works only when the client resolves each configured hostname to the bind IP Mullgate assigned to that route.

That mapping can come from:

  • local /etc/hosts entries produced by mullgate proxy access
  • published DNS records when you use --base-domain
  • some other trusted name-resolution system that preserves the same hostname-to-bind-IP mapping

In loopback mode without a base domain, those hostname mappings are optional. Direct bind-IP entrypoints remain the canonical local path even when you skip the hosts block.

Why the mapping matters for multi-route proof

Mullgate's routing layer uses the access mode you chose:

  • in published-routes + loopback, route selection still happens by destination bind IP
  • in published-routes + private-network, routes can share one host IP because each route gets its own published port
  • in published-routes + public, each route still needs its own bind IP
  • in inline-selector, routes share one listener and the username selector chooses the backend

Hostname proof still depends on correct name resolution. If the configured hostname points at the wrong host IP, mullgate proxy doctor reports hostname drift and points you back to mullgate proxy access.

Example curl forms

SOCKS5 with a hostname-mapped local route:

curl \
  --proxy socks5h://sweden-gothenburg:1080 \
  --proxy-user "$MULLGATE_PROXY_USERNAME:$MULLGATE_PROXY_PASSWORD" \
  https://am.i.mullvad.net/json

HTTP using a direct bind IP:

curl \
  --proxy http://127.0.0.2:8080 \
  --proxy-user "$MULLGATE_PROXY_USERNAME:$MULLGATE_PROXY_PASSWORD" \
  https://am.i.mullvad.net/json

If HTTPS proxy support is configured with a cert, key, and HTTPS port, the same route inventory appears in mullgate proxy access, start, status, and runtime-manifest.json.

Editing access and exposure after setup

Use mullgate proxy access instead of editing JSON by hand.

Inspect the current posture

mullgate proxy access

The report includes:

  • mode (loopback, private-network, or public)
  • access mode (published-routes or inline-selector)
  • base domain and whether LAN access is expected
  • the shared host or per-route hostname and bind IP inventory
  • DNS guidance when a base domain is set
  • hostname and direct-IP listener URLs with full credentials in published-routes
  • selector syntax and selector examples in inline-selector
  • whether the current runtime state needs a restart or refresh

Update the posture

Example: switch from loopback to private-network hostnames:

mullgate proxy access \
  --mode private-network \
  --base-domain proxy.example.com \
  --route-bind-ip 100.124.44.113

Example: switch to direct-IP public exposure:

mullgate proxy access \
  --mode public \
  --clear-base-domain \
  --route-bind-ip 203.0.113.10 \
  --route-bind-ip 203.0.113.11

Example: switch the same private-network host to selector-driven access:

mullgate proxy access \
  --mode private-network \
  --access-mode inline-selector \
  --route-bind-ip 100.124.44.113

If you intentionally expose inline-selector on a public host with an empty password, add:

--unsafe-public-empty-password

Without that explicit override, Mullgate blocks public + inline-selector + empty password.

After an exposure edit, Mullgate marks the runtime state as unvalidated. Do one of these before trusting the saved and runtime surfaces again:

mullgate proxy validate --refresh
# or
mullgate proxy start

Runtime inspection demo

The runtime status and diagnostics surfaces are easiest to read when you see them on a seeded healthy install:

Status and doctor demo

Validation and runtime lifecycle

mullgate proxy validate

Use this when you want to refresh or verify derived runtime artifacts without starting Docker immediately.

mullgate proxy validate --help
mullgate proxy validate --refresh

It validates the rendered wireproxy config and persists validation metadata under the runtime directory.

mullgate proxy start

Use mullgate proxy start when you are ready to render the runtime and bring the local proxy environment up.

Operational guidance

In day-to-day use, the safest pattern is:

  1. configure through CLI commands
  2. inspect exposure and route inventory
  3. validate derived runtime state
  4. start the runtime
  5. verify route behavior with status, doctor, and a real probe

Inspectable files and what they mean

The fastest way to find the active XDG paths is:

mullgate config path

That report also prints:

  • the resolved platform id and whether it came from the real host or MULLGATE_PLATFORM
  • the current platform support level (full on Linux, partial on macOS/Windows)
  • the runtime story and host-networking limitations for the current platform
  • platform guidance and warnings that match the wording used by status, doctor, and runtime-manifest.json

Important files:

FileMeaning
config.jsonCanonical Mullgate config with routed locations, exposure settings, and persisted runtime status
wireproxy.confPrimary rendered wireproxy config path recorded in the canonical config
wireproxy-configtest.jsonPersisted config validation report
docker-compose.ymlRendered runtime bundle entrypoint for mullgate proxy start
runtime-manifest.jsonTruthful route/endpoint manifest, including authenticated published URLs and backend topology
last-start.jsonSecret-safe success/failure report for the most recent mullgate proxy start attempt

Integrated release verifier

Use the integrated Linux-first proof command when you want one end-to-end check for the assembled setup/runtime flow instead of running setup, start, status, doctor, and curl probes manually.

Required environment variables

PurposeEnvironment variable
Mullvad account numberMULLGATE_ACCOUNT_NUMBER
Proxy usernameMULLGATE_PROXY_USERNAME
Proxy passwordMULLGATE_PROXY_PASSWORD
Deterministic Mullvad device nameMULLGATE_DEVICE_NAME

Optional verifier and setup inputs

PurposeEnvironment variable / flag
Routed locations (default sweden-gothenburg,austria-vienna)MULLGATE_LOCATIONS
Direct-route check target (default 1.1.1.1)MULLGATE_VERIFY_ROUTE_CHECK_IP / --route-check-ip
Exit-check URL (default https://am.i.mullvad.net/json)MULLGATE_VERIFY_TARGET_URL / --target-url
HTTPS port (default 8443 when the verifier generates TLS assets)MULLGATE_HTTPS_PORT
Existing HTTPS cert/key pathsMULLGATE_HTTPS_CERT_PATH, MULLGATE_HTTPS_KEY_PATH
Preserve the temp XDG home even on success--keep-temp-home
Reuse a preserved temp XDG home and skip provisioning a new Mullvad device--reuse-temp-home <path>

Command

export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD='replace-me'
export MULLGATE_DEVICE_NAME=mullgate-s06-proof
pnpm verify:s06

What the verifier proves

  • non-interactive mullgate setup against the real CLI
  • mullgate proxy access output and saved hostname → bind IP mappings
  • mullgate proxy start, mullgate proxy status, and mullgate proxy doctor
  • authenticated SOCKS5, HTTP, and HTTPS traffic through the published listeners
  • direct host-route invariance before/after mullgate proxy start
  • distinct exits for the first two routed hostnames when they resolve locally to distinct bind IPs

Shared-device prerequisite

A fresh verifier run needs one free Mullvad WireGuard device slot total because setup provisions one shared Mullvad entry device for the whole proof, not one device per route. If you rerun against a preserved home with --reuse-temp-home <path>, the verifier reuses that saved shared device and does not consume another slot.

Scaling note

The built-in pnpm verify:s06 path still keeps its end-to-end probe contract to the first two saved routes so the default proof stays fast and inspectable.

That does not mean Mullgate consumes one Mullvad slot per route. The current shared-entry runtime still uses one shared Mullvad device for the saved route set.

On this page