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 start --help
  • mullgate status --help
  • mullgate doctor --help
  • mullgate config --help

Install forms

Mullgate has three truthful invocation forms, in order of preference:

  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 verified by pnpm verify:m003-install-path
  3. Contributor/source-checkout pathnode dist/cli.js ... or pnpm exec tsx src/cli.ts ...

This guide uses installed mullgate ... commands by default. If you are working from a checkout instead, replace mullgate ... with either node dist/cli.js ... after pnpm build, or pnpm exec tsx src/cli.ts ... while developing.

Platform support posture

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

Platformconfig pathstatus / 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
Proxy password--password <secret>MULLGATE_PROXY_PASSWORD
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 / first route bind IP--bind-host <host>MULLGATE_BIND_HOST
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
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.
  • Non-loopback exposure requires one explicit bind IP per routed location.
  • Multi-route non-loopback exposure requires distinct bind IPs.

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 config hosts

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
  • 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_ROUTE_BIND_IPS=192.168.10.10,192.168.10.11
export MULLGATE_EXPOSURE_MODE=private-network
export MULLGATE_EXPOSURE_BASE_DOMAIN=proxy.example.com

mullgate setup --non-interactive
mullgate config exposure

What to expect:

  • route hostnames become sweden-gothenburg.proxy.example.com and austria-vienna.proxy.example.com
  • config exposure prints the DNS A records operators must publish
  • each hostname must resolve to its own bind 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 config exposure \
  --mode private-network \
  --clear-base-domain \
  --route-bind-ip 192.168.10.10 \
  --route-bind-ip 192.168.10.11

In that posture:

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

Hostname and direct-IP access

Mullgate exposes the same per-route listeners in two parallel forms:

  • hostname entrypoints — route-aware names such as sweden-gothenburg, austria-vienna, or sweden-gothenburg.proxy.example.com
  • direct-IP entrypoints — the bind IP for each route, such as 127.0.0.1, 127.0.0.2, or 192.168.10.10

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 config hosts
  • published DNS records when you use --base-domain
  • some other trusted name-resolution system that preserves the same hostname-to-bind-IP mapping

Why the mapping matters for multi-route proof

Mullgate's routing layer dispatches by destination bind IP, not by a late application-level hint. That means:

  • every remote route needs its own bind IP
  • hostname proof only works when name resolution lands on the correct IP first
  • if two hostnames resolve to the same bind IP, they collapse to the same route
  • if a hostname resolves somewhere else, mullgate doctor reports hostname drift and points you back to mullgate config hosts

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 config exposure, start, status, and runtime-manifest.json.

Editing exposure after setup

Use mullgate config exposure instead of editing JSON by hand.

Inspect the current posture

mullgate config exposure

The report includes:

  • mode (loopback, private-network, or public)
  • base domain and whether LAN access is expected
  • per-route hostname and bind IP inventory
  • DNS guidance when a base domain is set
  • hostname and direct-IP listener URLs with credentials redacted
  • whether the current runtime state needs a restart or refresh

Update the posture

Example: switch from loopback to private-network hostnames:

mullgate config exposure \
  --mode private-network \
  --base-domain proxy.example.com \
  --route-bind-ip 192.168.10.10 \
  --route-bind-ip 192.168.10.11

Example: switch to direct-IP public exposure:

mullgate config exposure \
  --mode public \
  --clear-base-domain \
  --route-bind-ip 203.0.113.10 \
  --route-bind-ip 203.0.113.11

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

mullgate config validate --refresh
# or
mullgate 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 config validate

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

mullgate config validate --help
mullgate config validate --refresh

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

mullgate start

Use mullgate 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 start
runtime-manifest.jsonTruthful route/endpoint manifest, including redacted published URLs and backend topology
last-start.jsonSecret-safe success/failure report for the most recent mullgate 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

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 config hosts output and saved hostname → bind IP mappings
  • mullgate start, mullgate status, and mullgate doctor
  • authenticated SOCKS5, HTTP, and HTTPS traffic through the published listeners
  • direct host-route invariance before/after mullgate start
  • distinct exits for the first two routed hostnames when they resolve locally to distinct bind IPs

On this page