Sign inSign up

ulfrasark/masque-vpn

By ulfrasark

Updated about 1 month ago

MASQUE-style HTTP/3 CONNECT proxy for MikroTik RouterOS x86_64 containers.

Image
Networking
Security
1

3.1K

ulfrasark/masque-vpn repository overview

ulfrasark/masque-server

MASQUE-style HTTP/3 CONNECT proxy for MikroTik RouterOS x86_64 containers.

Based on Alpine Linux. Self-generated CA + mutual TLS. Env-driven user management. Designed for the NekoBox / sing-box MASQUE client.


Image size
LayerSize
Compressed (Docker Hub)~7 MB
Unpacked (on device)~20 MB

Built with multi-stage build — only the Go binary + Alpine + openssl in the final image. No Go toolchain, no build artifacts.


How it works
  • Pure HTTP/3 CONNECT proxy over QUIC (ALPN h3). TCP tunneling only.
  • Mutual TLS: the server presents a cert signed by its own CA; each client must present a client cert whose fingerprint is on a live allow-list.
  • Users are driven by the CLIENT_NAMES env var — the single source of truth. On every start the container issues certs for new names, revokes names that were removed, and writes a ready-to-import .masque bundle per user.
  • No hardcoded IPs or passwords. Everything via env.

MikroTik settings
/interface/veth add address=172.16.168.2/24 gateway=172.16.168.1 name=veth-masque
/interface/bridge/port add bridge=Bridge-Docker interface=veth-masque

Environment variables
/container/envs/add list=masque-env key=LISTEN_ADDR        value=0.0.0.0:4433
/container/envs/add list=masque-env key=SERVER_PUBLIC_ADDR value=your-ip-or-domain:4433
/container/envs/add list=masque-env key=CERT_CN            value=masque.local
/container/envs/add list=masque-env key=CLIENT_NAMES       value=phone,laptop
VariableRequiredDescription
SERVER_PUBLIC_ADDRYesPublic ip:port or domain:port written into client .masque
CLIENT_NAMESYesComma-separated user names. Empty = no users, server refuses all
CERT_CNNoCert CN + client server_name. Default masque.local
CERT_SANNoCert SAN list. Default DNS:$CERT_CN,DNS:localhost,IP:127.0.0.1
LISTEN_ADDRNoListen address. Default 0.0.0.0:4433
CLIENT_MTUNoMTU written into client .masque. Default 1280

Using a domain instead of an IP is recommended: on a server move you only update the DNS A-record — client .masque files stay valid.


Mounts (required for persistence)
/container/mounts/add list=masque-cert    src=/usb1/masque/cert    dst=/app/cert
/container/mounts/add list=masque-clients src=/usb1/masque/clients dst=/app/clients

Without these mounts the CA, server cert and per-user .masque files are regenerated on every restart — clients would break after each restart.


Install container
/container/add \
  remote-image=ulfrasark/masque-server:latest \
  interface=veth-masque \
  envlist=masque-env \
  mountlists=masque-cert,masque-clients \
  root-dir=/usb1/masque \
  dns=1.1.1.1 \
  start-on-boot=yes \
  logging=yes

Start
/container/start [find image~"masque-server"]

Check logs
/log print where topics~"container"

On startup the container prints the CA fingerprint, issues client certs, and writes each .masque bundle:

[entrypoint] CA fingerprint: sha256 Fingerprint=...
[entrypoint] issuing client 'phone'
[entrypoint]   wrote /app/clients/phone.masque
[entrypoint] allow: phone = a1b2c3d4...
HTTP/3 CONNECT proxy on 0.0.0.0:4433 (mTLS, allow-list /app/clients/allowed.txt)

Managing users
ActionHow
AddAdd name to CLIENT_NAMES, restart. New <name>.masque is generated.
RevokeRemove name from CLIENT_NAMES, restart. Cert + bundle deleted, dropped from allow-list — the old .masque can no longer connect.
FetchGrab /usb1/masque/clients/<name>.masque (WinBox → Files).

Revocation is enforced at the TLS handshake: a removed user's certificate is no longer in the allow-list, so the server rejects it even though it still chains to the CA.


Port forwarding
PortProtocolRequired
4433UDPAlways (QUIC / HTTP/3 transport)

Client config (NekoBox / sing-box)

Import the generated .masque bundle, or fill the MASQUE node fields from it:

server_addr = "your-ip-or-domain:4433"
server_name = "masque.local"
insecure_skip_verify = false
ca_pem   = """ ...server CA...     """
cert_pem = """ ...client cert...   """
key_pem  = """ ...client key...    """

Set insecure_skip_verify = false so the client verifies the server, and keep cert_pem/key_pem so the server verifies the client (mutual TLS).


DNS (important)

This server tunnels TCP only, so plain UDP DNS through the proxy will fail. Use DoH in your client — it travels as HTTPS/TCP through the tunnel:

remoteDns = https://1.1.1.1/dns-query

DoH keeps DNS encrypted, routes it through the tunnel (no DNS leak), and defeats DNS-based blocking. A local DNS also works but resolves outside the tunnel.


Notes / limits
  • TCP only. App UDP/QUIC (game traffic, browser HTTP/3) is not tunneled; browsers fall back to TCP. Use DoH for DNS as above.
  • Transport is encrypted end-to-end (QUIC / TLS 1.3) with mutual authentication.

GitHub

Source and Dockerfile: coming soon

Tag summary

Content type

Image

Digest

sha256:129d4f3ad

Size

7 MB

Last updated

about 1 month ago

docker pull ulfrasark/masque-vpn