This guide walks through bringing up a Veil server end-to-end on a single VPS, registering a client, and connecting through a local SOCKS5 proxy. The flow targets the CLI path; the GUI installer arrives in a later phase.
The instructions assume a Linux server (Debian/Ubuntu/Alpine all work) and any modern desktop OS for the client.
Status: Veil is pre-alpha. The wire protocol, configuration formats, and CLI surface will change without notice until the v1.0 release. Do not deploy Veil for anyone whose access matters until the v1.0 audit is complete.
git clone https://github.com/redstone-md/veil.git
cd veil/core
go build -o /usr/local/bin/veil ./cmd/veil
veil versionThe pre-built image runs the server side only. Clients must still
install a veil binary locally.
cd veil/deploy/docker
cp server.example.yaml server.yaml # edit if needed
cp authorized_keys.example authorized_keys # legacy file path; new
# deployments use the
# SQLite store instead
docker compose up -d
docker compose logs -f veil(See deploy/docker/README.md for the full container recipe.)
Veil's server can listen on several wire-level transports simultaneously. A reasonable starter mix is QUIC for fast clients and Reality for clients on networks where active TLS probing is a concern. Add WSS as a fall-back if you want to handle networks that strip UDP.
A minimal server.yaml for a Reality-fronted server pointing at
www.microsoft.com:
transports:
- type: reality
listen: "0.0.0.0:443"
target_sni: "www.microsoft.com"
target_addr: "www.microsoft.com:443"
static_key_path: "/var/lib/veil/server.key"
user_db_path: "/var/lib/veil/users.db"Choosing
target_sni. Pick a host that is reachable from every network your clients will run from.www.cloudflare.comis a common example but is blocked or DNS-poisoned in RU and CN; a probe in those locales lands on a dead splice and Reality looks broken.www.microsoft.com,apple.com, orupdate.microsoft.comare safer global defaults — they serve real Microsoft / Apple infrastructure that no censor blocks without visible collateral damage. The TLS-layer cover is identical either way.
The first time you run veil serve, a fresh static keypair is
generated at static_key_path. The user database is created on
demand the first time veil user add or veil admin user-create
runs against user_db_path.
veil admin user-create \
--db /var/lib/veil/users.db \
--username root
# (interactive password prompt)veil serve --config /etc/veil/server.yamlYou should see:
INFO server static key ready public_key_b64=…
INFO user store opened path=/var/lib/veil/users.db active_users=0
INFO listening transport=reality addr=0.0.0.0:443
Copy the public_key_b64 line — clients need it to authenticate
the server.
The admin HTTP server is a separate process so you can put it on its own systemd unit, behind a different network policy.
veil admin serve \
--db /var/lib/veil/users.db \
--addr 127.0.0.1:8443The admin endpoint binds to 127.0.0.1 by default. Reach it from
your laptop with an SSH local-forward:
ssh -L 8443:127.0.0.1:8443 youruser@your-vps
# then point a browser at https://localhost:8443/ (or http while
# the embedded TLS story is still pending)The browser will challenge for HTTP Basic credentials — use the admin login you created in step 2b.
The client generates a keypair the first time it tries to connect. You can also generate one offline by starting the client once with a placeholder server entry — the first log line prints the public key:
INFO client static key ready public_key_b64=BASE64STRINGHERE …
Save that base64 string.
From the server (or from the admin Web UI):
veil user add \
--db /var/lib/veil/users.db \
--name alice \
--pubkey 'BASE64STRINGFROMTHECLIENT'Optional knobs:
veil user set-quota --db … --bytes 5368709120 <id> # 5 GB / month
veil user set-expiry --db … --at 2026-12-31T23:59:59Z <id>
veil user revoke --db … <id>
veil user restore --db … <id>
veil user list --db …Either hand-write client.yaml or have the server emit one for you:
veil user show-config \
--db /var/lib/veil/users.db \
--server-pubkey "PUBLIC_KEY_FROM_STEP_2C" \
--server-addr "your-vps.example.com:443" \
--transport reality \
--sni "www.microsoft.com" \
<user-id>Paste the output into client.yaml on the client machine.
veil connect --config client.yamlYou should see:
INFO transport connected transport=reality remote=…
INFO session established
INFO socks5 listening addr=127.0.0.1:1080
Test it:
curl --proxy socks5h://127.0.0.1:1080 https://example.comConfigure your browser to use socks5h://127.0.0.1:1080 as a SOCKS
proxy and you are done.
Per-user monthly byte quotas reset 30 days after each user's
quota_period_start. Reset is lazy: it happens the next time
veil user list (or any other store-touching command) runs against
a user whose period has expired. (A scheduled flush is on the
Phase 4 roadmap.)
Back up the entire /var/lib/veil/ directory. It contains:
server.key— the long-term Noise XK static keypair. Lose this and every client must update itsserver_static_key_b64.users.db— the SQLite user store and admin logins.users.db-wal,users.db-shm— SQLite WAL companions; back up alongside the main file.
git pull
go build -o /usr/local/bin/veil ./cmd/veil
systemctl restart veil veil-adminveil: user: --db path is required— pass--db PATHto the CLI subcommand. Subcommand-level flags do not inherit from parent commands in this version ofurfave/cli/v3.reality transport: TargetSNI is required— the server config is missingtarget_sni. Reality can not splice probe traffic without a real origin to point them at.actively refusedfrom a freshly-started client — the server bound the QUIC port but you are dialling its TCP port (or vice versa). UDP is the QUIC transport; TCP is WSS / Reality.unauthorizedin server logs — the client's public key is not in the user store. Add it withveil user add.
- The Tauri GUI installer that does steps 1–3 in a single window.
- Automatic ACME (Let's Encrypt) certificate provisioning for WSS.
- iOS / Android client apps.
- A mechanism for the server to push user revocations to the database without a process restart.
These land in subsequent phases. See the PRD roadmap for the order.