Config reference
The core reads a JSON file as source of truth. The panel changes it via the control API, never by editing the file.
{ "version": 1, "log": {"level": "info"}, "dns": {"servers": ["1.1.1.1:53"]}, "groups": [], "inbounds": [], "outbounds": [], "exits": [], "routing": {"default": "main", "rules": []}, "users": []}Required: version, dns, inbounds, outbounds, exits, routing and users. Optional: log, groups, notices, public_addresses, replay, links and xray.
This page covers the node’s config. Endpoints, gateway keys, mirrors, relays and backups belong to the panel and are not keys of this document. See the panel manual.
Rules that apply everywhere
Section titled “Rules that apply everywhere”- Strict JSON. Unknown and duplicate keys are errors at every level, as are a byte order mark, data after the document, invalid UTF-8 and a NUL inside a string. An integer must be an integer token (
1, never1.0). - Growth. Version 1 only grows by new optional keys and new enum values. A build that meets an unknown key or value refuses the file and says the config may be newer.
- Tags. Inbounds, outbounds, exits, routing rules, DNS sets, links, tunnels and Xray inbounds each have a
tagmatching[a-z0-9][a-z0-9_-]{0,63}, unique within its section. These words cannot be tags:all,any,block,reject,drop,local,direct,user. - References. Every tag, user name and group name used anywhere must exist.
- Lists. A list never holds the same entry twice. A condition in a routing rule must not be an empty list.
- Canonical spelling. Numbers inside strings (a port, a prefix length, a
lo-hirange) have no sign and no leading zeros. A timestamp usesTbetween date and time. - Addresses. IPv4-mapped IPv6 addresses (
::ffff:a.b.c.d) and addresses with a zone (%eth0) are errors everywhere. So is a network with host bits set (10.7.0.5/24), except in an inbound’saddress, where the host part is the gateway. Port 0 is an error everywhere. - Secrets. These are secrets:
private_key,preshared_key,transport_keys,transport_secrets,transport_secret,username,password,server_keyand thesecretof an Xray client. No error, warning, log line or API response shows a value, only its path (outbounds[1].password) and what was expected. On Unix,veilway runrefuses a config file that group or others can read, unless you start it with--allow-readable-config.veilway checkwarns. - Keys. A WireGuard key is standard base64 with padding, exactly 44 characters, decoding to 32 bytes. Keys are compared as decoded bytes. An all-zero key is an error.
- Paths. A file path is relative to the config file’s directory. Absolute paths and any
..component are errors. - Limits. File: at most 64 MiB and 8,000,000 JSON values. At most 64 inbounds with 8 transports each, 256 outbounds, 256 exits, 256 groups, 32 DNS sets, 4096 routing rules, 1024 entries in any list inside a rule, 65,536 users, 64 devices per user, 4 notices, 8 public addresses, 64 links, 64 tunnels and 16 Xray inbounds.
- Errors and warnings. Validation reports the path of each bad field, with at most 100 errors and 100 warnings, then a count of the rest. An error refuses the config. A warning lets it load and is logged. Warnings are advice, not part of the format.
version
Section titled “version”Must be the integer 1.
| Key | Type | Default | Meaning |
|---|---|---|---|
level |
error, warn, info, debug, trace |
info |
Lowest level written. RUST_LOG overrides it when set. |
destinations |
bool | false |
Whether log lines at any level may hold the destinations and DNS names users reach. |
groups
Section titled “groups”A list of group names, each matching [a-z0-9][a-z0-9._-]{0,63}. Declare a group here before a user or a rule names it.
The node answers DNS for its tunnels at each inbound’s gateway address.
| Key | Type | Default | Meaning |
|---|---|---|---|
servers |
list of ip:port, 1 to 4 |
required | Upstream resolvers, tried in order with 5 seconds each. |
hijack |
bool | true |
Answer every UDP and TCP port 53 flow from a tunnel here, whatever address it was sent to. With false, only queries sent to a gateway address are answered. Other port 53 traffic is routed like any flow. |
sets |
list of at most 32 sets | [] |
Other lists of servers, each for the inbounds, groups and users it names. See DNS sets below. |
A server address may not be loopback, link-local, unspecified, broadcast, multicast, in 0.0.0.0/8, or inside any inbound pool. An IPv6 address that embeds such an IPv4 address (IPv4-compatible, NAT64, 6to4, Teredo) is refused too. The destination gate refuses them at run time, so such a server would resolve nothing.
Engine:
- A forwarded query is a flow from that user to the chosen server on port 53. The rules route it like any flow, matching
domainconditions against the queried name. It must pass the destination gate of its exit. Ablock,rejector not-entitled result answersREFUSED. - Per user, the node remembers which names it resolved to which addresses (one name, one address per entry): at most 4096 entries, each kept for the answer’s TTL clamped to 60 to 3600 seconds. It records only answers its own resolver obtained, never answers that hold pool, gateway, loopback or link-local addresses. Nothing is cached across users.
- Each upstream query goes out under an id the node picks, and only an answer with that id is accepted. The first server whose rules refuse the query ends it with
REFUSED. Only a silent server moves on to the next. - A query name that is not a valid rule domain (for example one with an underscore label) is still forwarded, but no
domaincondition matches it and it is not remembered. - At most 64 queries of one user wait at once. More get
SERVFAIL. Over TCP a query may be 1500 bytes and a connection carries at most 128 queries. - With
hijackon, rules withport: [53]never see tunnel DNS, and port 53 traffic that is not DNS is dropped. DNS over TLS and DNS over HTTPS pass untouched, anddomainrules cannot see those names.
DNS sets
Section titled “DNS sets”A set is a named list of upstream servers for some users, for example a filtering resolver for a family group, an ad blocker for a few users, or another resolver for one inbound.
"dns": { "servers": ["1.1.1.1:53"], "sets": [ {"tag": "family", "servers": ["1.1.1.3:53", "1.0.0.3:53"], "groups": ["family"]}, {"tag": "adblock", "servers": ["192.0.2.53:53"], "users": ["alice", "bob"]}, {"tag": "lab", "servers": ["192.0.2.54:53"], "inbounds": ["office"]} ]}| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | Unique among the sets. |
servers |
list of ip:port, 1 to 4 |
required | Tried in order with 5 seconds each. The address rules of dns.servers apply. One server may stand in several lists. |
inbounds |
list of inbound tags | [] |
Queries that come through these inbounds use the set. |
groups |
list of group names | [] |
Users in these groups use the set. |
users |
list of user names | [] |
These users use the set. |
A query goes to the first match, most specific first:
- the set that names the user;
- otherwise the set that names one of the user’s groups;
- otherwise the set that names the inbound the query came through;
- otherwise
dns.servers.
Steps 1 and 2 belong to the user, step 3 to the query. A user with devices on two inbounds, whom no set names by user or group, uses the list of whichever inbound a query comes through.
Validation does not depend on the order of the sets:
- Error: two sets name the same inbound, group or user, or one list names it twice. The error gives the path of the first entry.
- Error: a user whom no set names is in groups that two different sets name. The error is at that user’s
groupsand gives both paths. Name the user in theusersof one set, or keep the groups in one set. - Error: a name no inbound, group or user has.
- Error:
hijackisfalseandsetsis not empty. Withhijackoff, a device can reach any resolver by querying an address other than the gateway, so a set would decide nothing. - Warning: a set no query goes to. It names nobody, or the groups it names have no user and every user of the inbounds it names goes by a set that names them or one of their groups.
A set covers the DNS the node answers: with hijack on, every UDP and TCP port 53 flow of a tunnel. A device that uses DNS over TLS or DNS over HTTPS steps around its set, as around domain rules. Where a filtering set has to hold, refuse those too, for example with a rule that rejects TCP and UDP port 853 and rules for the addresses of the public DoH resolvers.
Engine:
- A set’s servers follow everything said about
dns.servers: the flow, rules, destination gate, entitlement, name memory, limits, ids and TCP fallback. - A set never falls back. When none of its servers answers, the query gets
SERVFAIL. When the rules refuse a server, it getsREFUSED. The node never asksdns.serversor another set instead: a filtering resolver that could be skipped whenever it is down would filter nothing. hijackis one setting for the whole node. A config with sets needs it on.- The name memory belongs to the user, whichever list answered. See Reloading for what happens to it when a user’s servers change.
- A log line about a query that names a user also names the set used. The info lines about a server that stopped answering, or answers again, name the list and the server’s place in it (
server 2 of 2 of dns set family). A server in two lists is counted in each by itself. No line shows a queried name or a server address unlesslog.destinationsis on.
inbounds
Section titled “inbounds”An inbound is one tunnel: one key, one address pool, reached over one or more transports. A device keeps the same tunnel address on every transport.
| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | |
type |
wireguard or veilway |
required | wireguard: stock WireGuard peers. veilway: the same tunnel, with the control channel at its gateway address, see “The control channel” below. Changing the type restarts nothing. |
enabled |
bool | true |
false switches the inbound off, see “Switching an inbound or a transport off” below. |
private_key |
key | required | The tunnel’s static key. Secret. Unique across inbounds, compared by the public key derived from it. |
address |
list of 1 or 2 networks | required | The gateway address with its prefix, at most one IPv4 and one IPv6, for example ["10.7.0.1/24", "fd7a:115c:a1e0::1/64"]. The network is the tunnel’s address pool. |
mtu |
integer 1280 to 1500 | 1420, or 1390 for a veilway inbound with a udp-obfs transport |
MTU inside the tunnel. Above 1420 is a warning: the outer packet fragments on a normal path, or is lost on a veilway inbound, which never fragments. With a udp-obfs transport the warning starts above 1390, because a full-size packet then does not fit a 1500 byte path over IPv6. |
padding |
object | see below | Upper bounds of the padding the node adds to what it sends over udp-obfs, tls, ws and fronted. Both are optional: wide from 512 to 1023 (default 1023) and normal from 64 to 255 (default 255). On an inbound with none of these transports the key is a warning. See udp-obfs below. |
transport_keys |
list of 1 or 2 keys | none | Secret. The node’s transport keys, private halves. Required for an inbound with a tls, ws or fronted transport (a ws-plain transport has its own transport_keys, see below). Devices seal their proof to one of them. veilway genkey --transport prints one with its public half, which is what a profile carries. Two keys allow rotation: removing one ends the sessions opened under it. No WireGuard key of the config is one, no transport secret is one, and no two inbounds share one. On an inbound without such a transport the key is a warning. See tls below. |
upgrade_cookie |
1 to 32 characters from A-Z a-z 0-9 _ - |
none | The name of the cookie on the upgrade answer of a tls or ws transport, and of the cookie that carries the token of a ws-plain transport, where the answer sets no cookie. Required with one of them. It has no default, so that not every node sends the same name. Choose a name your own site could use. An inbound with fronted alone needs none. |
upgrade_headers |
list of at most 16 header field names | ["Server", "Date"] for tls, none for ws |
The header fields the upgrade answer of a tls or ws transport copies from the decoy origin, in this order. Behind a web server the list is empty unless you set it, because that web server adds its own fields. See tls below. |
transports |
list of 1 to 8 | required | See below. |
Rules for address:
- IPv4 prefix from /16 to /30, inside
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16or100.64.0.0/10. IPv6 prefix from /48 to /126, insidefc00::/7. - The gateway is a host address in the pool, never the network address and, for IPv4, never the broadcast address.
- Pools of different inbounds do not overlap.
An entry of inbounds with the type socks5, http or mixed is a proxy inbound, not a tunnel. See Proxy inbounds below. Everything above is about tunnel inbounds.
Addresses alone decide who may use an inbound: a device belongs to the inbound whose pool holds its address. It may have one IPv4 and one IPv6 address in each inbound, so one device can use several inbounds, wireguard and veilway alike. From v0.2.18 the panel also gives a device an address in the range of every plain wireguard inbound, at the same offset as in its own pool (see the panel manual), so a device whose keys the panel made can get a WireGuard file. Once the config has any device, veilway check warns about a veilway inbound with transports that no device has an address in and no link uses: every connection to it would be treated as a stranger’s.
Switching an inbound or a transport off
Section titled “Switching an inbound or a transport off”"enabled": false on an inbound, a transport or a proxy inbound switches it off without deleting it, as on a link, a tunnel or an upstream. Leaving the key out means true.
- It is validated like any other, and its addresses stay reserved: no other listener may take its address and no other pool may overlap its pool.
- It does not listen. The node binds nothing for it, and
veilway doctordoes not try its address. - Sessions on it end at the reload that switches it off. The devices of an inbound that is off are let go: sessions end, flows are reset, and they cannot connect through it until it is switched on again. A transport that is off leaves the inbound’s other transports as they are.
veilway checkskips an inbound that is off when it looks for aveilwayinbound that no device has an address in.- The control API’s
GET /v1/inboundssaysenabledfor each inbound and for each of its transports.
Devices learn of a switch only from the next published profile. At the next publish, the panel leaves an inbound that is off out of the profiles, and a transport that is off out of its inbound’s profile.
The control channel
Section titled “The control channel”Engine. On a veilway inbound the gateway address also answers the control channel of the protocol: TCP port 47873, and UDP port 47873 for MTU probes. The client engine speaks it inside the tunnel.
- Who reaches it. A device, from its own tunnel address, at the gateway of the inbound it is connected through. The gateway of any other inbound refuses the port, and so does every
wireguardinbound. The node handles it before the routing rules, like gateway DNS: no rule blocks, redirects or opens it,dns.hijackdoes not touch it, and its bytes count as usage. - Who is let in. A connection must prove one of the device’s
transport_secretswithin 10 seconds, in an exchange bound to that device’s and that inbound’s keys. Anything else is closed with no answer. A device may have 4 connections that have not proven it and 2 that have. One more replaces the oldest. - What it gives. The device’s addresses, gateway and MTU as the node holds them. The exits of its user’s
exits(tag,name,country, whether it carries every IP protocol, whether it is up). Its user’s usage and quota. Thenotices. A reload sends again what it changed. Usage is sent again when it changed, at most once every 30 to 90 seconds, never because of the channel’s own traffic. - Choosing an exit. A device may choose which of its user’s
exitsthe action"user"means for it. The choice belongs to that device only. It moves the flows that the rules send to"user"or that match no rule: they are reset and leave through the new exit from their next packet. A flow a rule sends to a named exit is not touched. A device without a choice leaves throughusers[].exit, thenrouting.default, and while that exit is not working, through the first exit ofrouting.fallbackthat works. A choice goes into the state file at its next regular write, not at once, so a node killed within those 10 seconds forgets it and the device chooses again. - MTU. A device may probe which packet size its path carries and set its own tunnel’s MTU, from 1280 (1080 for a device with IPv4 addresses only) to the inbound’s
mtu. The node then sends that device nothing larger. TCP the node ends itself keeps to the segment size the device announces. What else the node sends itself, it fragments, as it does IPv4 that it forwards and that allows it. The sender of anything else gets “fragmentation needed” or “packet too big”. The value is forgotten after 180 seconds without a session. A device with addresses in twoveilwayinbounds has one MTU, one chosen exit and one set of limits for both. A probe applies onudpandudp-obfs, and overudp-obfsit leaves with no padding. On Linux the node prohibits fragmentation of everything it sends from theudpandudp-obfssockets of aveilwayinbound, so a packet that does not fit the path is lost and a probe measures what the path carries. On another system it cannot, and warns at start: there a probe can pass a path that fragments it. - Being cut off. Before a device is cut off (quota, expiry, disabled or removed, or the node stops), the node writes it the reason. The cut-off does not wait for that.
- Limits. Per device: 10 requests at once and then one a second, 3 exit changes and then one in 5 seconds, one probe run a minute. Over a limit, a request is answered “rate limited” and does nothing.
- What is logged and counted. A channel that opens or closes, an exit that is chosen or dropped and an MTU that is set are logged at info with the user and device name: at most 6 such lines at once for one device, then one in 30 seconds. The rest go to debug and the next line says how many. None names a destination. Counters are in
controlof/v1/stats./v1/sessionsshows each device’s chosen exit, open control connections and MTU.
transports
Section titled “transports”type |
Keys | Meaning |
|---|---|---|
udp |
listen (ip:port) |
Plain WireGuard datagrams. |
ws |
listen (ip:port), path, optional tls with cert and key (both required) |
One WireGuard datagram per binary WebSocket frame. Without tls it serves plain ws for a reverse proxy that ends TLS. Validation warns when such a listener is not on a loopback address. |
udp-obfs |
listen (ip:port), optional limits with sessions (1 to 1,048,576, default 16,384) |
Every WireGuard datagram inside the udp-obfs envelope of the protocol, keyed by the device’s transport_secrets. Only on a veilway inbound. See udp-obfs below. |
tls |
listen (ip:port), path, tls with cert and key, decoy with origin and optional proxy_protocol, optional limits with sessions, undecided, undecided_per_source and relayed |
The WebSocket session of the protocol inside TLS that the node ends itself. Anyone who is not a user goes to the decoy origin. Only on a veilway inbound. See tls below. |
ws with a decoy |
listen (ip:port), path, decoy, trusted_proxies, optional client_address_header, optional limits. No tls key. |
The same WebSocket session behind your own web server, which ends TLS. Only on a veilway inbound. See “ws and fronted behind your web server” below. |
ws-plain |
the keys of ws with a decoy, and transport_keys |
The same WebSocket session over plain HTTP, with no TLS between the app and your web server. Only on a veilway inbound. The weakest transport. See “ws-plain” below. |
fronted |
the keys of ws with a decoy |
Polling with HTTP requests, for a CDN that allows neither WebSocket nor long-lived connections. Only on a veilway inbound. See the same section. |
fronted-shaped |
the keys of fronted, and shape |
fronted with a method, a place for the data and a downstream form that you pick. See “fronted-shaped” below. |
faketls |
listen (ip:port), site, transport_key and the limits of its section |
Alpha in v0.2.0. Borrows a real site. Only on a veilway inbound. A profile carries it as “The faketls transport” of the subscription format. A client dials it only after the user turns on alpha_transports. See “faketls” below. |
ws is two transports under one name, and its keys say which. Without decoy, trusted_proxies, client_address_header and limits it is the ws of a wireguard inbound, which carries WireGuard datagrams as they are. With any of them it is the native ws of a veilway inbound, which needs decoy and trusted_proxies and takes no tls. Validation refuses each on the other kind of inbound.
A veilway inbound takes udp, udp-obfs, tls, the native ws, ws-plain, fronted and, as alpha, faketls. A wireguard inbound takes udp and the ws without a decoy, because its devices have no transport secret for the others.
path starts with /, is at most 256 bytes from A-Z a-z 0-9 . _ ~ / -, and has no .. component. It is matched exactly against the request path before any ?, except for a fronted-shaped transport whose data rides in the path, which is matched as a prefix (see “fronted-shaped” below). On tls, the native ws, ws-plain, fronted and fronted-shaped, path is at least 12 bytes.
Every transport takes enabled (bool, default true) next to listen. A transport that is not enabled is validated like any other, does not listen, and keeps its address reserved. See “Switching an inbound or a transport off” above.
Engine: a ws connection that does not finish the WebSocket upgrade within 10 seconds is dropped. One source (an IPv4 address or an IPv6 /64) may have 64 unfinished upgrades at once, a reverse proxy in front of a plain ws listener included.
Listen conflicts, across all inbounds:
- IPv6 sockets are always IPv6-only, so
0.0.0.0:443and[::]:443do not conflict. A wildcard address conflicts with every address of its own family on the same port. - Two
udporudp-obfstransports never share an address and port, in any combination. - Two
wstransports may share an address and port when theirpathdiffers and theirtlsis identical. - Two
tlstransports may share an address and port when theirpathdiffers and theirtls,decoyandlimitsare identical: two paths of one listener. - The
wsandfrontedtransports ofveilwayinbounds may share an address and port when theirpathdiffers and theirdecoy,trusted_proxies,client_address_headerandlimitsare identical. That is one listener, to which the web server hands aGETfor one path and aPOSTfor another. - A
fronted-shapedtransport shares a listener on the same terms asfronted. Itspathmay not be a prefix of another transport’spathon the same listener, because a shape that puts the data in the path is matched by prefix. Validation refuses it. - A
faketlstransport never shares an address and port with another transport, and its key never equals the key of another transport of the node. - A
ws-plaintransport never shares an address and port with a transport that is notws-plain. Its decoy must answer like your plain site, so that a prober cannot tell its path from your other plain paths. Twows-plaintransports may share a listener on the same terms asws. - A listener serves one kind of peer. A
tlstransport, awstransport of awireguardinbound, and thewsandfrontedtransports ofveilwayinbounds never share an address and port with one another.
Any other pair that shares an address and port conflicts.
udp-obfs
Section titled “udp-obfs”Engine. Built on both sides. The transports page is the specification, an unreviewed draft. udp-obfs hides WireGuard’s fixed bytes and sizes from an observer without a subscription. It imitates nothing, and its rating is Detectable.
{"type": "udp-obfs", "listen": "0.0.0.0:51821", "limits": {"sessions": 16384}}- Who is let in. A device opens a session with one opening packet that only a holder of one of its
transport_secretscan make. The session then carries that device’s WireGuard and no other. The node sends nothing on a session until a WireGuard packet of that device has authenticated on it. - What a stranger gets. Nothing: no answer and no log line. It is counted by reason in
udp_obfsof/v1/stats. - Replay. An opening packet is accepted once, also across restarts. That needs the replay file, see
replaybelow. A node does not start a config with audp-obfstransport without a replay file it can write and lock. An opening packet from a device whose clock is ahead of the node’s limit is kept, at most 64 of them for at most 2 seconds, until the limit is written, and is then answered at its own source. One that a limit refuses is recorded as used all the same, so that nobody who saw it can use it later. - Being cut off. Whenever the node cuts a device off (its user is disabled, expired, over quota or removed, the device is removed, or its addresses, preshared key or key change), every
udp-obfssession of that device ends with its tunnel. So does a session whose transport secret was removed. - Packets that cannot be sent. A
veilwayinbound never fragments. The system refuses a packet larger than the host’s interface carries. It is counted insend_failedof/v1/statsand logged at info once a minute. Lower the inbound’smtuwhen that counter grows. - Limits that are fixed. A device may have 2 sessions that are not yet established and 8 that are, and 16 accepted opening packets at once and then one a second. One source address and port may have sessions of 4 devices. A session ends 10 seconds after its opening packet if WireGuard has not authenticated on it, and after 180 seconds without an authentic WireGuard packet. The idle time of the client’s keepalive is well below that.
limits.sessions. How many established sessions the listener holds at once. When it is full, new opening packets are refused. Nothing a stranger does takes a place.padding. The node pads what it sends, inside the encryption.widebounds the padding of the first 16 packets of a session, of every WireGuard handshake message and of every control channel packet.normalbounds the rest. A full-size packet carries none: no packet is larger thanmtu+ 62 bytes. Change them so that not every node shows the same ranges.mtu.udp-obfsadds 62 bytes around a tunnel packet, 30 more thanudp, so the defaultmtuof aveilwayinbound with audp-obfstransport is 1390. Adding the transport to an inbound with nomtuwritten changes itsmtufrom 1420 to 1390, which restarts the tunnel once.- The clock. A device and the node must agree on the time within 60 seconds. A node whose clock is wrong refuses its devices. A clock that ran ahead can leave the node refusing everybody after it is put right, see
replay. Run the node with a synchronized clock. - What is logged. At info: a listener that starts, sessions that a reload ended, a session that reached the packet limit of its keys. At warn or error: a tag table that was built late, and everything about the replay file. No line names a client address, a tag or a secret.
Engine. Built on both sides. The transports page is the specification, an unreviewed draft.
A device reaches its tunnel through a WebSocket connection inside TLS, on a name and certificate of your own. Everyone else who connects goes to your own web server, the decoy origin, and gets its answer.
tls is rated Detectable, and stays so in version 1. The TLS handshake a visitor sees is the node’s, not your web server’s. A prober needs no subscription to see that the handshake and the site behind it do not match. The decoy makes the HTTP answer your site’s, not the handshake. For a handshake that matches the site, use the native ws behind the web server that serves the site, see “ws and fronted behind your web server” below.
{ "tag": "home", "type": "veilway", "private_key": "...", "address": ["10.7.0.1/24"], "transport_keys": ["..."], "upgrade_cookie": "visit", "transports": [{ "type": "tls", "listen": "0.0.0.0:443", "path": "/assets/app-3f9c2e71.js", "tls": {"cert": "site.crt", "key": "site.key"}, "decoy": {"origin": "http://127.0.0.1:8080"} }]}| Key | Type | Default | Meaning |
|---|---|---|---|
listen |
ip:port |
required | Where the node ends TLS. |
path |
12 to 256 bytes from A-Z a-z 0-9 . _ ~ / -, starting with /, no .. component |
required | The path a device’s request names. Anyone with a subscription knows it. Choose one your site does not have. |
tls |
cert and key, both file paths |
required | The certificate chain and its key, as PEM files. Version 1 has no built-in issuance from an authority: get and renew the certificate with your own tooling. On a native tls transport, self_signed: true may replace cert and key. The node then makes its own self-signed key and certificate (files tls-<inbound tag>.key and .crt, next to the config file, mode 600) and renews the certificate with the same key. It reports the pin (the SHA-256 of the SubjectPublicKeyInfo) in veilway doctor and in the control API’s status. A renewal keeps the key, so the pin survives it. The panel puts this pin into the tls candidates of the profiles it makes, see the panel manual. name (optional, with self_signed only) is the host name in the certificate: a lowercase DNS name of at most 64 characters. Without it the node draws one. A running node never makes a new key. If the key file is gone or changed, it keeps the current certificate and logs a warning, since the pin would change. A node link may trust it through its pin alone, see links below. The node also works out the pin of a certificate read from cert and key, and veilway doctor and the control API show it. It remembers the pin of each tls transport in tls-pins.state next to the config file, and warns once when the key of a transport changes (from v0.3.6), see Pins and self-signed links. See the transports page of the protocol specification. |
decoy.origin |
http:// URL with an address or a name and an optional port, nothing behind it |
required | Your own web server. There is no default and no other kind of decoy. |
decoy.proxy_protocol |
bool | false |
Send the PROXY protocol header, version 2, at the start of every connection to the origin. |
limits.sessions |
integer 1 to 1,048,576 | 16384 |
Established sessions the listener holds at once. |
limits.undecided |
integer 1 to 65,536 | 512 |
Connections that have not sent their request line yet. |
limits.undecided_per_source |
integer 1 to undecided |
64 |
How many of those one source may have. A source is an IPv4 address or an IPv6 /64. |
limits.relayed |
integer 1 to 65,536 | 1024 |
Connections relayed to the origin. |
http2 |
off or on |
off |
Whether the node offers h2 as well as http/1.1. With on, the node decides every stream itself and relays a stranger’s streams to the origin as HTTP/1.1, so the node’s own HTTP/2 server answers a prober instead of the origin’s. Keep it off unless you need h2. See HTTP/2. Every tls transport on the same listener must have the same http2. |
What the decoy origin must be.
- Your own web server, on the same host or one you control, serving real content under your own name. Relaying strangers to somebody else’s site is not supported in this version. Validation warns when the origin is not a loopback or private address, and when it is a name.
- Reached over plain HTTP. An
https://origin is refused in this build. Put the origin on a loopback address: what strangers send travels to it as is. - Serving the site there, not redirecting to
https://. A stranger arrived over TLS and goes to a plain listener. A site’s usual plain listener redirects everything to itshttps://address, and through the node that redirect would come back on every request, which no web site does. The origin must be a plain listener that serves the site as its HTTPS listener does.veilway doctorwarns when the origin redirects tohttps://. - Serving the right site. A stranger’s request reaches the origin with the
Hostthe stranger sent. The node’s own requests, and those ofveilway doctor, carry the host of thedecoy.originURL. On a web server with several sites, make the site you mean the default of that listener, or the two may see different sites. - Not the node. Validation refuses an origin whose address and port the node itself listens on. A name is held to the same when it is resolved, at start and at every reload. A listener on a wildcard address refuses an origin on a loopback or wildcard address with its port, and not one on another host.
- Aware of an extra connection per user. For every connection it accepts, the node opens a connection to the origin before it knows who is there, and closes it unused when a user was there. It leaves no request in the origin’s log. With
proxy_protocol, it carries the user’s address. - Serving nothing at the path. A request for the path without a valid proof must get the site’s own not-found answer, so the origin must not hand the path back to the node. If your web server also forwards something to the node, make the origin a second listener of that web server without the forwarding rule.
veilway doctorasks the origin for the path and says what it finds. - Using its own limits. The node puts no limit on the size or lifetime of a relayed connection. Your site’s own timeouts and limits decide, as for its direct visitors. With
proxy_protocolthe site sees each stranger’s address and can limit per address. Turn it on only when the origin expects the header on that listener: an origin that does not expect it answers every relayed request with an error. The node’s own requests carry the node’s address.
What happens to a connection.
- Who is let in. A device sends a WebSocket upgrade request for
pathwith a proof in its query: a token that only a holder of one of the device’stransport_secretscan make, sealed to one of the inbound’stransport_keys, valid once and for about a minute. The node decides at the end of the request line. An accepted proof gets the upgrade answer, and the connection then carries that device’s WireGuard and no other. The node sends nothing more on it until a WireGuard packet of that device has authenticated. - What everybody else gets. The origin’s answer, and nothing of the node’s: no status line, no header field and no error page. Every byte the peer sent goes to the origin unchanged, and the node relays both directions blindly until either side closes. Everything the origin sends is passed on at once. When the origin closes, the peer is closed once it has been given all of it, however slowly it reads. An origin that resets the connection resets the peer’s. This holds for a browser, garbage, a wrong path, a wrong or used proof, and the proof of a device that was removed or whose user is disabled, expired or over quota. All look like a visitor who asked for a page the site does not have.
- What this does not hide. The TLS handshake is the node’s, as said above. An answer through the node comes later than one from the origin, by the hop between them. The node offers
http/1.1alone unlesshttp2ison, and TLS 1.2 and 1.3 as its library does. A client that offers onlyh2gets a TLS alert. Plain HTTP sent to the port gets the library’s alert, where a web server sends an error page. A proof under TLS 1.2 is never accepted. When the origin answers before it has read a whole upload and closes its side, the node then closes the peer’s connection, where a lingering web server would take the rest of the upload first. - An origin that gives a connection up first. The node watches the origin while a request line is incomplete. If the origin says something or closes in that time, as a web server does with a connection that sends nothing, the peer gets that at once, and the connection is not a user’s. A complete request line is always looked at first. An origin that closes connections before a request can arrive would cost users an attempt now and then. A web server does not, and its timeout for a request head should not be shorter than a second or two.
- When the origin is down. Whoever is not a user is closed with no answer, which tells the node from a web site for as long as it lasts. It is counted in
stream.decoy_downof/v1/stats, logged at warn once a minute, and logged at info when the origin is back. Users are not affected. - The upgrade answer. After the status line come the fields of
upgrade_headersin that order.Datecarries the node’s time. Every other field carries the value your origin sent for it, spelled as the origin spelled it, and a field the origin did not send is left out. Then come the three fields of the upgrade and one cookie namedupgrade_cookiewith a random value of random length. The node asks the origin once for its page at start and at every reload to learn those values. Until that works, the answer carries onlyDateof the list, which is logged at warn, and the node asks again every 60 seconds.upgrade_headersmay not nameUpgrade,Connection,Sec-WebSocket-Accept,Set-Cookie,Content-Length,Transfer-Encoding,Content-Type,Content-Encoding,TrailerorKeep-Alive, and no name twice.
Limits.
- The pools. A connection that has not sent its request line is undecided. When the listener has
limits.undecidedof them, or one source has its share, the oldest is decided as “not a user” at once to make room. One still inside its TLS handshake is closed. Whenlimits.relayedconnections are relayed, the one idle longest makes room, or the oldest when none has been idle for 60 seconds. It is closed at once, with nothing more sent. Whenlimits.sessionssessions are established, new proofs take the decoy path. Nothing a stranger does takes the place of a session. - Limits that are fixed. A TLS handshake has 10 seconds. A request line has 4,096 bytes and 10 seconds. A user’s request head has 16,384 bytes and 10 seconds. A device may have 2 sessions that are not yet established and 8 that are, and 16 accepted proofs at once and then one a second. These count across
udp-obfsandtlstogether. A session ends 10 seconds after its proof if WireGuard has not authenticated on it, and after 300 seconds without a chunk. Each connection has a send queue of 256 packets. What does not fit is dropped and counted, as on a real network. - Replay. A proof is accepted once, also across restarts, which needs the replay file, see
replay. A proof from a device whose clock is ahead of the node’s limit waits for the limit to be written, 2 seconds at most, and 64 of them at once for a listener. Each holds its connection outside the undecided pool while it waits. - Being cut off. Whenever the node cuts a device off, every
tlssession of that device ends within a second. So does a session whose transport secret was removed, and one opened under a node transport key the config no longer has. A session ends the same way for every cause: a WebSocket Close frame, then TLS and TCP in the normal order. paddingbounds the padding of what the node sends, as forudp-obfs. A stream has no packet size to keep to, so the inbound’smtudefault stays 1420 withtlsalone.- The address. A
tlstransport on an address that also has audp,udp-obfsorwslistener marks that address for whoever watches it. Put it on an address of its own if you can. - What is logged. At info: a listener that starts, sessions that a reload ended, an origin that is reached again. At warn: an origin that cannot be reached, a key file that group or others can read, header fields that could not be learned. Nothing is logged for a single connection, and no line names a connection’s client address, a path, a token or a key. Counters are in
streamof/v1/stats.
ws and fronted behind your web server
Section titled “ws and fronted behind your web server”Engine. Built on both sides. Tested only in containers on one machine, against small test servers and with nginx in front of the node and nginx or HAProxy behind it. Neither transport has run behind a CDN, on a real network, or in front of a real site. The transports page is the specification, an unreviewed draft.
Your own web server ends TLS with your site’s name and certificate. It hands two paths to the node over plain HTTP on the loopback and serves everything else as before. The node never ends TLS on these transports. A device reaches its tunnel through one of the two paths.
wsis the WebSocket session oftls, with the web server’s TLS in place of the node’s. Use it wherever a WebSocket connection gets through, with or without a CDN in front of the web server.frontedcarries the tunnel in the bodies ofPOSTrequests and their answers, for a CDN that allows neither WebSocket nor long-lived connections. It is slow, makes many requests, and is the transport of last resort. Check the CDN’s terms before you use it this way.
Both are rated Detectable. The handshake is your web server’s, which tls cannot offer. The transports page states what is left: a request for one fixed path with a long random query, a long-lived connection or a steady rhythm of small requests to one host, and the sizes and times of what flows. Nothing here has been measured, so claim no more than Detectable for either.
{ "tag": "home", "type": "veilway", "private_key": "...", "address": ["10.7.0.1/24"], "transport_keys": ["..."], "upgrade_cookie": "visit", "transports": [ {"type": "ws", "listen": "127.0.0.1:8443", "path": "/assets/live-7c1d0a52.js", "decoy": {"origin": "http://127.0.0.1:8080"}, "trusted_proxies": ["127.0.0.1"], "client_address_header": "X-Forwarded-For"}, {"type": "fronted", "listen": "127.0.0.1:8443", "path": "/assets/sync-90be44f1.js", "decoy": {"origin": "http://127.0.0.1:8080"}, "trusted_proxies": ["127.0.0.1"], "client_address_header": "X-Forwarded-For"} ]}| Key | Type | Default | Meaning |
|---|---|---|---|
listen |
ip:port |
required | Where the node takes plain HTTP from your web server. Use a loopback address. Anything else is a warning, because tokens, session ids and your site’s pages then cross unprotected. A unix socket is not built. |
path |
12 to 256 bytes from A-Z a-z 0-9 . _ ~ / -, starting with /, no .. component |
required | The path as the web server hands it to the node. If the web server rewrites the path, use the rewritten one. Choose one your site does not have. |
decoy.origin, decoy.proxy_protocol |
as for tls |
required, false |
Your own web site, on a plain listener of its own. See “What the decoy origin must be” under tls, and below. |
trusted_proxies |
list of 1 to 64 addresses or networks | required | Where your web server connects from: ["127.0.0.1"] on one host. Only a connection from one of these can bring a user, and only from these is client_address_header believed. Everybody else is a stranger and gets the origin’s answer. A network of every address (0.0.0.0/0, ::/0) is refused, and so is a list whose networks cover every address between them. An entry that is neither a loopback nor a private address is a warning: every host inside it can bring a user and is believed about where a request came from, and requests cross from it to the node as plain HTTP. |
client_address_header |
header field name | none | The field in which your web server names the address a request came from, such as X-Forwarded-For. The node takes the rightmost address in it that is not one of trusted_proxies. Without the key, or when a request has no such field, the request counts as the web server’s own, and the limits per source become one limit for everybody. Validation warns when the key is missing. |
limits.sessions |
integer 1 to 1,048,576 | 16384 |
Established sessions the listener holds at once, ws and fronted together. |
limits.fronted_sessions |
integer 1 to 65,536 | 1024 |
How many of them may be fronted sessions. Each keeps its last answer, up to 65,536 bytes, and a digest of its last request. A full pool of 1,024 holds up to 64 MiB of answers. Above 4,096, which is 256 MiB, validation warns and says how much the pool holds. |
limits.proxy_connections |
integer 2 to 65,536 | twice fronted_sessions |
Connections of your web server the listener holds at once. When all are taken, the node stops accepting and leaves new ones in the system’s queue. A ws session takes its connection out of this count. |
limits.decoy_requests_per_source |
integer 1 to 65,536 | 16 |
Requests of one source that may wait for the origin at once. One more closes the connection of the oldest. All strangers together have half of proxy_connections. |
limits.undecided, undecided_per_source, relayed |
as for tls |
as for tls |
The pools for peers that reach the listener from outside trusted_proxies. |
Transports that share a listener have the same decoy, trusted_proxies, client_address_header and limits. A limits.proxy_connections that is not set is twice fronted_sessions, and three times the shaped sse sessions on a listener of a fronted-shaped transport (see below).
The web server in front. An nginx example, with the site and the node on one host. The first server is the site as the world sees it. The second is the same site on a plain loopback listener, without the two rules that hand a path to the node. That one is the decoy origin.
upstream veilway { server 127.0.0.1:8443; keepalive 64;}
server { listen 443 ssl; server_name site.example.com; ssl_certificate /etc/ssl/site.crt; ssl_certificate_key /etc/ssl/site.key; root /var/www/site;
# The path of the ws transport: a WebSocket upgrade, handed on with its query. location = /assets/live-7c1d0a52.js { proxy_pass http://veilway; proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 360s; access_log off; }
# The path of the fronted transport: requests on connections that are kept. location = /assets/sync-90be44f1.js { proxy_pass http://veilway; proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Connection ""; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; client_max_body_size 128k; access_log off; }}
server { # The decoy origin: the same site, plain, on the loopback, with no rule for the two paths. listen 127.0.0.1:8080 default_server; root /var/www/site;}What the example does:
proxy_add_x_forwarded_forputs the address the web server took the request from at the right end ofX-Forwarded-For, behind whatever the visitor sent. The node reads from the right and skips the addresses intrusted_proxies, so a visitor cannot name an address for itself. Behind a CDN, add the CDN’s ranges to nginx’s ownset_real_ip_fromandreal_ip_header, so that the address nginx adds is the visitor’s and not the CDN’s.veilway doctorsays whether a running node has found the field in the requests it read.access_log offon the two paths keeps the query, a token or a session id, out of the web server’s log. A token that was logged and never reached the node can be used once by whoever reads the log, within about a minute.keepaliveandproxy_http_version 1.1let the web server keep its connections to the node.frontedneeds that: it makes one request after another for as long as the tunnel is up. The node keeps a quiet connection of the web server for 300 seconds.proxy_http_version 1.1is required. A request that reaches the node as HTTP/1.0 is never a user’s, and nginx speaks HTTP/1.0 to the node unless told otherwise, which shuts every user out with no other sign. The node counts such requests (proxy_http10in/v1/stats), andveilway doctorsays so when a running node has seen any.proxy_read_timeouton thewspath is above the 300 seconds a session may be quiet for.- Only the two paths go to the node here. A request for either without a valid proof gets the origin’s own not-found answer. You may hand more to the node, or everything: the origin answers whatever is not a user’s, through the node. The origin must never hand a path back to the node, which is why it is a listener without the two rules.
What the decoy origin must be is what tls says, with these differences:
- The visitor arrived over your web server’s TLS, so the origin is the plain listener behind it. It must serve the site as the first server does, without a redirect to
https://. - The node opens a connection to the origin only for a request that is not a user’s, and keeps it for the next such request of the same web server connection. A user’s request never reaches the origin and leaves no connection in its log.
- With
proxy_protocol, the origin is told the address taken fromclient_address_header, with port 0, since the field names no port. A request that names no source, and one whose head the node cannot read to its end, is told with the address of the web server in front. So the origin must grant nothing to an address, its own host’s and its proxy’s included. - The node keeps one origin connection for each web server connection. The origin’s keep-alive time does not decide how long the web server’s connection lives: see “What the origin does between two requests” below.
What happens to a request.
- Who is let in. The node reads the whole request head, at most 16,384 bytes within 10 seconds, takes the source from
client_address_header, and then decides. A head above 16,384 bytes is nobody’s and is handed on as the bytes it is. The node still reads it to its end, up to 65,536 bytes, for one thing only: the source its last proxy named, so that a stranger with a long head does not look like the web server itself to the origin. AGETfor thewspath with a valid proof in its query becomes a WebSocket session, as ontls, and its connection leaves the proxy pool. APOSTfor thefrontedpath with a valid proof opens afrontedsession. Later requests name that session and a number that goes up by one. - What everybody else gets. The origin’s answer, and nothing of the node’s. The node writes no byte of its own for such a request and changes none. The head goes to the origin as read, the body as far as its length or chunks say, and the answer comes back as the origin sent it, to its last byte and no further. When the answer is complete, the next request of the web server is read. An answer that says
Connection: closeends the web server’s connection with it, as does an origin that closes or resets its connection while a request is in its hands. - Bytes behind a complete answer are never handed to the web server: they answer nothing it asked, and its next request on that connection may be a user’s. When the origin writes more than one answer to a request, the web server gets the first and both connections are closed.
- What the origin does between two requests is not passed on. When an origin says something on a quiet connection, as some servers do with a
408, or closes a connection it kept, it did so to the node’s connection only. The node drops that connection, keeps the web server’s, and opens a new one to the origin for the next stranger. If a kept connection turns out closed just as a request is sent on it, the node sends the request once more on a new connection. - A request the node cannot tell the end of. The node reads lengths more strictly than a web server: one
Content-Lengthin digits, no more than 2^63 - 1, orTransfer-Encoding: chunkedalone, CR LF line ends, no folded line, no chunk extension. It reads nothing else. It hands the request on as the bytes it is, with everything the web server sends behind it, until the origin’s first answer is complete, then closes both connections, so it never guesses where the next request starts. nginx refuses most such requests itself, before the node sees them. - A request from outside
trusted_proxiesis a stranger’s whatever it carries, a valid proof and a forwarded-address field included. It is relayed to the origin as ontls. - When the origin is down the web server’s connection is closed with no answer, and the web server gives the visitor its own error, as if the site were down. Users are not affected. The error is not always the one a down site gives. nginx answers 502 either way. HAProxy answers 503 for an origin that refuses connections and 502 for a connection closed with no answer. While the origin is down, that is one more way to tell the node from the site.
- A visitor with more than its share of requests waiting (
limits.decoy_requests_per_source, and half the proxy pool for all strangers together) has the connection of its oldest waiting request closed at once, also in the middle of an answer. The web server makes its own error of that, a 502 on nginx, which a site without the node would not have given. - The upgrade answer of
wsis the one oftls, with an emptyupgrade_headersunless you set it, since the web server addsServerandDateitself.
fronted in particular.
- A request is decided from its head: exactly one
Content-Lengthfrom 18 to 65,536 and noTransfer-Encoding. The node then reads the whole body within 10 seconds before it uses anything, and every chunk in it must open. The origin answers a request that fails any of this, and the session it named stays exactly as it was, so a stranger who learns a session id cannot disturb the session. - One request of a session is in flight at a time. If the next one comes early, the origin answers it. A repeat of the last request with the same bytes gets the same answer again, because a CDN may send a request twice. For that, the node keeps the last answer and a SHA-256 of the last request body, not the body.
- The node answers at once when it has something for the device, and after 250 ms when it has not. The answer is
200withContent-Type: application/octet-streamand aContent-Length, and nothing else from the node. - A session with no accepted request for 60 seconds is forgotten.
frontedhas no keepalive: the requests are the sign of life. - A device may have 2
frontedsessions among its 8. A third takes the place of its oldestfrontedsession once it is established. - An opening request whose body brings no WireGuard packet of its device is held, since the node sends nothing before that. The origin answers it when the session ends, 10 seconds later.
HTTP/2 behind your web server. Your web server may speak HTTP/2 to the client but must speak HTTP/1.1 to the node, which offers no HTTP/2 on these listeners. HTTP/2 or h2c towards the node is not supported. The web server turns a client’s HTTP/2 into HTTP/1.1 as for any site behind it. See HTTP/2, where a client side for ws and fronted over HTTP/2 is designed and not built.
Limits that are fixed are those of tls where they are the same thing: 2 sessions that are not yet established and 8 that are for a device, 16 accepted proofs at once and then one a second, counted across every native transport together. A ws session ends after 300 seconds without a chunk and a fronted session after 60 seconds without an accepted request. Replay and being cut off are as on tls: a proof is accepted once, also across restarts, and whatever cuts a device off ends its sessions within a second.
What is logged. At info: a listener that starts, sessions that a reload ended, an origin that is reached again. At warn: an origin that cannot be reached. Nothing is logged for a single request, and no line names a request’s client address, a path, a token, a session id or a key. Counters are in stream of /v1/stats.
ws-plain
Section titled “ws-plain”Engine. Built on both sides since v0.2.14-alpha. Tested in containers with a small web server, nginx and HAProxy, not on a real network. The transports page is the specification.
ws-plain is the session of the native ws over plain HTTP. Your web server takes the connection on port 80 (or another plain port) and hands one path to the node on the loopback. Use it only where TLS to your server is blocked or slowed and plain HTTP passes. The network sees your server’s name, the path and the token, and can tell that people use this app. A later leak of the transport’s key ties all of a device’s plain connections together. The app asks the user for consent of its own before it dials one, and the panel shows a warning when you add it.
Its keys are those of ws with a decoy: listen, path, decoy, trusted_proxies, optional client_address_header and optional limits. It has no tls, http2, shape, site or transport_key key. It adds one:
| Key | Type | Default | Meaning |
|---|---|---|---|
transport_keys |
list of 1 or 2 keys | required | Secret. The private halves of this transport’s own keys, not the inbound’s. Two allow rotation, the new key first and the old one second. A profile carries the public half of the first. veilway genkey --transport prints one. |
The inbound’s upgrade_cookie is required, and the token travels in a cookie of that name. Pick a name your own site never sets.
Validation refuses:
- a key that is a WireGuard key of the config, a key of any other transport or inbound, or the same key twice, so that a leak of this one opens nothing else;
- a
paththat is thepathof atls,wsorfrontedtransport of any inbound; - a
ws-plaintransport on a listener that a transport which is notws-plainalso uses.
The node reads the whole head of every request before it decides. The token must be in exactly one Cookie field, in exactly one cookie of the inbound’s cookie name. Other cookies in the field are allowed. A request with anything after ? in its target is not from a user. Whoever is not a user gets the decoy’s answer. Your web server must answer the path as it answers any other plain path, a redirect to HTTPS included. The upgrade answer sets no cookie.
The panel makes and rotates the keys (see Rotation). The node only sees the list change.
fronted-shaped
Section titled “fronted-shaped”A fronted transport has one shape: a POST with the data in the body, answered in the body. A fronted-shaped transport lets you pick the method, where in the request the data goes, and whether the node answers each request or writes to one Server-Sent Events stream. Its keys are those of fronted, and one more.
| Key | Type | Meaning |
|---|---|---|
shape |
object, required | up with method, in, and optionally name, limit, inflight and gather, and down with mode and optionally gather, life and fallback. The members, the valid combinations and the ranges are on the page. The panel writes the same object to the profile, and the two must match. The node cannot see the profile, and a difference shows as refusals at the client. |
{"type": "fronted-shaped", "listen": "127.0.0.1:8443", "path": "/assets/feed-5a1e9c30.js", "shape": {"up": {"method": "POST", "in": "body"}, "down": {"mode": "sse"}}, "decoy": {"origin": "http://127.0.0.1:8080"}, "trusted_proxies": ["127.0.0.1"], "client_address_header": "X-Forwarded-For"}- Validation refuses a shape that breaks the rules of the page: a method and
inthat do not go together (aGETwith a body), a refused method or header name, a limit or range out of bounds, anup.inflightabove 1 withpoll, and an unknown member or value. It warns above 256 shaped sessions on a listener, and counts them among the 1,024frontedsessions. - The path of a shape that puts the data in the path (
up.inpath) is matched as a prefix. Your web server hands the node every path under it, and the origin must serve nothing there. Validation refuses apaththat is a prefix of another transport’s path on the same listener. - Limits. The node reads a request line of up to 8,192 bytes when
up.limitis above 4,000. A shaped session holds at most 603,136 bytes.limits.proxy_connectionsis three times the shapedssesessions when not set, because a shaped client keeps up to 5 connections to the host. - Your web server. For
sseit must not buffer the answer. In nginx, useproxy_buffering offfor the path, aproxy_read_timeoutabove the longest stream (down.life, at most 3,600 s) andproxy_http_version 1.1. ForGETthe CDN must not cache the path. The prefix location is forup.inpathonly.
# A fronted-shaped transport with an event stream down. Use a location with a# trailing slash instead of "=" when the data rides in the path.location = /assets/feed-5a1e9c30.js { proxy_pass http://veilway; proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Connection ""; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; proxy_read_timeout 3700s; client_max_body_size 128k; access_log off;}- A node link refuses it, as it refuses
fronted. veilway doctorprints the shape of each such transport.- Your web server and the CDN log data in a URL. For
queryandpaththat is every upstream byte, sealed. If that worries you, usebody.
faketls
Section titled “faketls”The node holds no certificate. It borrows a real site that you choose. A client proves itself inside its ClientHello and gets the tunnel. Every other connection is relayed byte for byte to the site, so a prober sees the site’s own certificate. Clients must reach the node directly: no CDN, no TLS-ending proxy and no trusted_proxies. The site is a third party that has not agreed to any of this. Read “The third party” on the page before you set it.
| Key | Type | Default | Meaning |
|---|---|---|---|
listen |
ip:port |
required | Where the node takes connections. It shares its address and port with no other transport. |
site.host |
host name | required | The site’s name. It is the only server name a user’s hello may carry, and the name used in relayed and probe connections. |
site.names |
list of at most 4 host names | none | Extra server names a user’s hello may carry. |
site.port |
port | 443 |
The site’s port. |
site.address |
IP address | none | An address to connect to instead of resolving the name. |
site.acknowledged |
bool | required | Must be true. Validation refuses the transport without it. Setting it means you accept that the site’s owner has not agreed to this. |
transport_key |
secret | required | A key of this transport alone. Validation refuses a key equal to the transport key of any other transport on the node. |
limits.relayed |
integer | 256 |
Strangers relayed to the site at once. |
limits.site_rate |
integer | 16 |
New connections to the site a second. When it is spent, users are still served and only a stranger that needs a connection is closed. |
Validation also refuses a listener with trusted_proxies, and a site.address that is the node’s own listen address. Pick a site that a user in your country can reach, that is large enough that the node’s connections do not matter to its owner, and, if you can, that is hosted in the same network as the node. None is suggested or shipped. The candidate a profile carries has the node’s address, the site’s name as its server name and the transport’s public key. It has no pin, self_signed, ech, host, path or fronting value.
Proxy inbounds (socks5, http, mixed)
Section titled “Proxy inbounds (socks5, http, mixed)”From v0.2.17. A proxy inbound is an entry of inbounds whose type is socks5, http or mixed: a TCP listener that lets other software use the node as a plain proxy. It has no key, no address pool and no transports. Every connection logs in as one of the config’s users, and its traffic meets the same destination gate, rules, exits, entitlement, quota and counters as that user’s tunnel traffic.
| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | Unique among all the inbounds of the config, tunnels included. Rules name the inbound by it. |
type |
socks5, http or mixed |
required | socks5: SOCKS5 with a user name and password. http: an HTTP proxy. mixed: both on one port. A first byte of 0x05 is SOCKS5 and anything else is read as HTTP. |
listen |
ip:port |
required | The TCP address clients connect to. See “Listen addresses” below. It never shares an address and port with another TCP listener of the node: another proxy inbound, a tls, ws or fronted listener, a tunnel, or an Xray inbound. A UDP port of the same number is no conflict. |
users |
list of user names | [] |
The users that may log in. Each name must be a user of the config. |
groups |
list of group names | [] |
The groups whose members may log in. A user in users or in one of these groups may. With both lists empty, every user that has a proxy_password may. |
udp |
bool | true |
Whether UDP ASSOCIATE is answered. An error on an http inbound, which has none. |
udp_any_port |
bool | false |
Lets a client behind a NAT have a UDP association on an inbound that is not on loopback. See “UDP” below. An error on an http inbound. A warning where it has no effect: on loopback, or with udp off. |
public |
bool | false |
Says you know that a listen address strangers reach carries traffic and passwords in the clear. See “Listen addresses” below. |
enabled |
bool | true |
false keeps the inbound in the config, validated, with its address reserved, and does not listen. Its connections end at the reload. |
limits.connections |
integer 1 to 65536 | 1024 |
Connections the inbound holds at once, logged in or not. |
limits.per_source |
integer 1 to 65536 | 64 |
How many of those one source may hold. A source is an IPv4 address or an IPv6 /64. A loopback peer is exempt, see below. |
limits.handshakes |
integer 1 to 65536 | 256 |
Connections that have not finished logging in. At most 8 of them for one source, and half of them for loopback peers together. |
{"tag": "socks", "type": "socks5", "listen": "127.0.0.1:1080", "users": ["ana"]}Listen addresses
Section titled “Listen addresses”- Loopback (
127.0.0.0/8,::1) is taken as is and warns about nothing. Only this host reaches it.public: trueon loopback is a warning, since it is never needed. - A private address (RFC 1918,
100.64.0.0/10,fc00::/7) is taken withoutpublic, with a warning. On a cloud host a private address is often NATed one to one to a public one, and a local network has other machines. Nothing is encrypted there, so anyone who reaches the port or is on the path can read the traffic and the password. - Any other address, the wildcard (
0.0.0.0,::) and link-local ones included, is an error unlesspublicistrue. Withpublic: trueit is taken with a warning that the traffic and the password cross the network unencrypted, and that the listener is easy to find and block.
veilway check reports these. veilway doctor also checks that each listen address can be bound.
Who may log in
Section titled “Who may log in”A client logs in with the user’s name and proxy_password. There is no anonymous use and no shared password.
| Key | Type | Default | Meaning |
|---|---|---|---|
users[].proxy_password |
16 to 128 printable ASCII characters, no : |
none | Secret. The password this user logs in to a socks5, http or mixed inbound with. Without one the user cannot log in to any of them. No two accounts share a login. A proxy_password in a config with no proxy inbound is a warning. |
- The inbound must admit the user (
users,groups, or neither list for everyone with a password). A user who is disabled, expired or over quota is refused at the request, as everywhere else. - A wrong password, a wrong name and a user the inbound does not admit are answered alike and take the same time.
- Proxy-only users. A user with a
proxy_passwordmay have"devices": []. Such a user is counted, held to a quota and cut off like any other. A user without aproxy_passwordstill needs 1 to 64 devices. - The control API shows
proxy_passwordas"<redacted>". A document sent back with that marker keeps the running password of the user with the samename. A marker in a config file read from disk is an error. - A user’s connections end within a second of the user being cut off, of the password changing, or of the inbound no longer admitting the user.
- The node cannot recover a login the user did not write down. The panel keeps its own sealed copy, see the panel manual.
SOCKS5
Section titled “SOCKS5”- Only the user name and password method (RFC 1929) is offered. A client that offers no such method is answered
0xffand closed. CONNECTandUDP ASSOCIATEare taken.BINDand every other command are refused.- The node resolves a destination name itself, never through the host’s resolver, using the DNS servers of the user’s set and the exit the rules give the query. A name is lower case, at most 253 bytes, of letters, digits, hyphens, underscores and dots.
- Each destination is a flow of the user. The destination gate refuses loopback, link-local, multicast, the cloud metadata addresses, every address pool of the node and every address of this host, the inbound’s own listen address included. Private networks are the rules’ business, as for the tunnel.
CONNECT host:portopens a tunnel to the target. The login isProxy-Authorization: Basic. A request without a login is answered407and the connection is closed.- A plain
http://request with an absolute URI is forwarded, one request to a connection. The node sends it to the origin with the request line in origin form,Hosttaken from the URI, theProxy-Authorizationfield and the other hop fields removed, andConnection: closeadded, so the origin never sees the login. The node reads nothing the client writes after the request’s own end (itsContent-Length, or its chunks), so a second request behind the first never reaches the origin. - Refused:
https://and origin-form requests, userinfo and fragments in a URI, a request with bothContent-LengthandTransfer-Encoding, aTransfer-Encodingthat does not end inchunked, and any version butHTTP/1.0andHTTP/1.1. The head is at most 8,192 bytes and 64 fields.
UDP ASSOCIATE gives a SOCKS5 client a UDP relay on the node. A datagram carries no proof of who sent it, so the node ties the relay to the control connection.
- The relay is bound to the address the control connection reached, with a port of its own. It carries only datagrams from the IP address of the control connection. Fragments are dropped.
- On loopback (Linux) a datagram is carried only from a socket whose owner, read from the kernel, is the user id that owns the control connection. On macOS and Windows the system cannot tell who owns a socket, so a loopback inbound follows the rule for other addresses.
- On any other address the client must say in its request the port it sends from. Only datagrams from exactly that address and port are carried, and a request with port
0is refused (0x02). A client behind a NAT, which rewrites the port, gets no UDP. Validation warns about this on every such inbound withudpon. udp_any_portis the way out for clients behind a NAT: the association binds to the first datagram from the client’s address. The cost is a window before that datagram, in which a sender that shares the client’s address (neighbors behind the same carrier NAT, say) can take the association and reach the destinations of the user’s flows. Validation warns about it. Leave it off unless your clients are behind a NAT.- An association ends with its control connection, after 120 seconds without a datagram, and when the user is cut off. A user holds at most 32 associations and 256 UDP flows, and an inbound 4,096 and 16,384.
- Datagrams dropped because they did not come from the address and port the client named count as
refused_udp_source.
Limits and failed logins
Section titled “Limits and failed logins”Beyond limits, the inbound protects the passwords:
- A client that sends no byte in 3 seconds is closed, and so is a connection that has not finished its login in 10 seconds. A
mixedinbound’s first-byte look and its whole login share those 10 seconds. - A source that fails 10 logins to one user name in 60 seconds is refused that name, unheard, until the window ends. The count is kept per source and name, so one source does not lock out the others. One source may hold counts for 16 names. One that tries more is refused every name until the window ends.
- A refusal for a name that logins failed for waits 100 ms for each failure beyond the first two, up to 2 seconds, whoever asks. A correct login never waits.
- A loopback peer is a front you run, with every user behind one address. It is exempt from the share of a source and from the block, but the wait for a name still holds.
- A request without a login is not a failure: a browser makes one first.
- A user’s TCP connections and UDP flows are counted once across every proxy inbound and the hop of the Xray add-on together, so spreading them over inbounds buys nothing.
- A connection that carries nothing for the exit’s
tcp_idle_secsis ended.
Counters are in proxies of GET /v1/stats, and the bytes are in inbounds as well. A refused login is logged at info, once in 10 seconds at most, with a count and no address, name or password.
Reloading
Section titled “Reloading”A reload that changes a proxy inbound in any way closes its listener and every connection on it. Connections of other inbounds stay. A reload that changes a user’s proxy_password, or removes, disables or no longer admits a user, ends that user’s connections within a second. A reload that removes an inbound also closes its port.
outbounds
Section titled “outbounds”An outbound is a way for the userspace stack to open TCP and UDP flows.
type |
Keys | Meaning |
|---|---|---|
direct |
optional bind (a local IP, not a wildcard), optional interface (a network interface of the host by name, 1 to 15 characters of letters, digits, _, - and ., starting with a letter or digit; since v0.2.8) |
Connects from the node itself. With bind, flows of the other address family are rejected. With interface, every socket of the outbound is tied to that interface (SO_BINDTODEVICE on Linux, IP_BOUND_IF on macOS), so its traffic leaves there whatever the routing table says. |
socks5 |
server (host:port, IPv6 literals in brackets), optional username and password (both or neither, each 1 to 255 bytes), optional via (the tag of another socks5 outbound) |
Connects through a SOCKS5 proxy, or through a chain of them when via is set. UDP uses UDP ASSOCIATE when the proxy offers it. |
xray |
server (host:port) and protocol, or a link; the fields of the protocol |
Connects through a VLESS, VMess, Trojan or Shadowsocks server of another party, run by the Xray add-on. Linux nodes only. See “xray outbounds” below. |
wireguard |
server (host:port), public_key, private_key, addresses, and optional preshared_key, mtu, keepalive_secs, max_flows, bind, interface |
Connects through a WireGuard server of another party, with a tunnel the node runs itself. See “wireguard outbounds” below. |
Every outbound also has a tag.
An interface that is the interface of an l3 exit is an error: the outbound’s traffic would go back into the tunnel. A socks5 outbound takes no interface. Engine: on a system other than Linux and macOS, run refuses a config whose exits use an outbound with interface. If the host lacks the interface when the config comes into force, the node logs a warning and the outbound’s flows fail until it is there. veilway doctor names a missing interface.
Engine: the system resolver resolves a server host name each time the node dials the proxy, never through a tunnel exit. The node uses the UDP relay address a proxy returns only if it is the address the control connection is connected to. A relay address on another host means the proxy counts as having no UDP.
via (socks5 only) sends the connection to this outbound’s server through another socks5 outbound, which may have a via of its own. A chain may have up to 8 proxies, counting this outbound. The node dials the first proxy itself. Each hop then does its own SOCKS5 handshake and authentication over the connection the hop before it made, and the flow’s destination is the last CONNECT. A host name in server goes to the hop before it unresolved, except for the first proxy, which the node resolves.
vianames asocks5outbound. Adirectoutbound cannot be named, and adirectoutbound cannot havevia.- Following
viafrom an outbound must not come back to it. A loop is a config error. - A chain has at most 8
socks5outbounds. A longer one is a config error at theviaof the outbound that makes it too long. - A chain carries TCP only. A UDP ASSOCIATE relay cannot be reached through a TCP chain, so a UDP flow through a chain is refused as a proxy without UDP is. The exit’s
unsupported.udppolicy decides, and the defaultrejectrefuses it.directis a config error on such an exit, since it would send that UDP from this node’s own address, around the chain. DNS over UDP falls back to TCP through the chain, as for any proxy without UDP. - The chain is shown as one outbound. Its latency is the time the whole chain took to answer the greeting of the last hop.
- A chain that cannot be dialled makes its flows fail. Nothing goes around it.
- Plain text. SOCKS5 is not encrypted. Each proxy of a chain sees the user name and password of the proxies after it, the CONNECT targets and the bytes of the flows. Chain only through proxies whose operators may see that, or run TLS inside every flow yourself.
xray outbounds
Section titled “xray outbounds”From v0.2.18. An xray outbound sends flows through a server of another party that speaks VLESS, VMess, Trojan or Shadowsocks, with the Xray add-on doing the talking. Use it behind a netstack exit. The guide shows the steps in the panel. The server, its name and its credential are your secrets. Whoever runs the server sees what the exit carries, as any provider of an exit does.
Give either a link, or protocol and server with the fields of the protocol. A node takes at most 16 outbounds of this type.
| Key | Type | Default | Meaning |
|---|---|---|---|
link |
string of at most 4,096 bytes | none | Input only. One share link of the kinds vless://, vmess://, trojan:// and ss://, on one line. The node reads it with the same reader and table as the desktop app, so it accepts what the app accepts and refuses with the same codes. What it holds becomes the typed fields below and the link itself is dropped: a document read back from the node never has a link. Any other key next to a link is refused. A link the table refuses is an error that names the code and never the link. |
protocol |
vless, vmess, trojan or shadowsocks |
required without link |
|
server |
host:port |
required without link |
The server. IPv6 literals go in brackets. Xray never looks a name up, see “The server’s address” below. |
allow_private |
bool | false |
Lets server be a private, shared (CGNAT) or unique local address. Loopback, link-local, the node’s own addresses and its inbounds’ pools are refused whatever this says. |
id |
UUID | none | Secret. Required for vless and vmess, and refused for the other two. |
password |
1 to 255 bytes, no control characters | none | Secret. Required for trojan and shadowsocks, and refused for the other two. For a shadowsocks 2022 cipher it is the key as standard base64: 16 bytes for 2022-blake3-aes-128-gcm, 32 for the other two. For the older ciphers it must have at least 16 characters, since the key comes from the password alone and a short one is guessed offline. |
cipher |
string | auto for vmess |
vmess: auto, aes-128-gcm or chacha20-poly1305. shadowsocks: required, one of aes-128-gcm, aes-256-gcm, chacha20-ietf-poly1305, 2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm and 2022-blake3-chacha20-poly1305. Refused for vless and trojan. |
flow |
xtls-rprx-vision |
none | vless only, and only over tcp with tls or reality. |
transport |
object with type |
{"type": "tcp"} |
The stream: tcp; ws and httpupgrade with optional path and host; grpc with optional service and authority; xhttp with optional path, host and mode (auto, packet-up, stream-up or stream-one). A path starts with /, has at most 256 bytes and no ?, since a query can carry early data, which the node does not use. host and authority are lowercase host names or addresses. service is 1 to 128 characters from letters, digits and -._/. A path that is left out is /. |
security |
object with type |
{"type": "none"} |
none; tls with optional server_name, alpn (h2 and http/1.1, each at most once) and fingerprint (default chrome); reality with server_name and public_key (43 characters of base64url), and optional short_id (up to 16 lowercase hex digits, an even number of them) and fingerprint. The server_name of tls defaults to the server’s name and is required when server is an address. Certificates are always checked against the system roots. No key turns that off or pins a certificate. |
{"tag": "rented", "type": "xray", "protocol": "vless", "server": "198.51.100.7:443", "id": "3f2b8c1e-5d4a-4e7b-9a6c-1d2e3f4a5b6c", "security": {"type": "tls", "server_name": "example.org"}}What validation refuses.
vlessandtrojanwithouttlsorreality: they are never spoken in the clear.- A
flowthat is not Vision overtcpwithtlsorreality, and aflowon any protocol butvless. realityoverwsorhttpupgrade.- A
?in apath, and any path, host, service or authority that does not read as above. - A
shadowsockspassword that is too short for its cipher, or a 2022 key of the wrong length. - An exit on an
xrayoutbound whoseunsupported.udpisdirector{"via": ...}. An Xray outbound carries UDP itself, and while it is down the flow ends rather than leave the exit around it. Userejectordrop. - A
viaon anxrayoutbound, and anxrayoutbound as theviaof another outbound, of a link or of an upstream. It is reached through a Unix socket and cannot carry a chain. - A literal server at an address the node may not dial, see “The server’s address”.
The warnings are a server written as a name (see below), and routing.fallback next to an exit on an xray outbound, because users routed there fall back to the other exit while the outbound is down. proxy_only of such an exit defaults to true.
How it runs. The add-on’s one Xray process gets a trojan inbound for each xray outbound, listening on a Unix socket and routed to that outbound alone. The node dials that socket and speaks Trojan to it: CONNECT for TCP and one framed stream for each UDP flow. So the exit, the gate, the rules, the quotas and the counters are those every flow meets, and Xray only talks to the provider. Every request carries a password made new at each start. Before it sends a byte, the node checks that the socket belongs to Xray’s user.
- Linux nodes only. The add-on host runs on Linux. On macOS and Windows nodes the outbound is down and says so.
- The add-on must be installed.
scripts/veilway.sh addon xray installalso puts the node’s user in the groupveilway-xray, which owns the folder the sockets are made in (/run/veilway-xray-out, mode 750, andrun --xray-dirchanges it). The script restarts a node that was running, so that it joins the group. Without the add-on, every exit through anxrayoutbound is down and says why, and nothing goes around it. - A restart drops Xray users for a moment. One Xray runs for the inbounds and the outbounds. Adding, changing or removing either restarts it after two seconds without another change. Connections of Xray inbounds and flows through Xray outbounds end and come back with the next connect. No quota, count or user is lost.
- Down, with a reason. An outbound that Xray does not carry is down, and so is every exit through it. That covers a missing add-on, a host that does not answer, Xray starting again, a name that gives no usable address and a socket that is not Xray’s. A flow of a down outbound ends at once and never goes to
direct, to another exit or to the provider’s default.GET /v1/statsgives the reason asdown_reasonof the outbound. It names no address and no credential. - Xray’s own egress is not restricted. The Xray unit has the network of the host, because providers can be anywhere. The document the node writes, with one address the node checked, holds Xray to the provider, but a bug in Xray is not held to it. The
interfaceandbindof the config do not apply to this traffic. On a host with other tunnels it follows the host’s routing table.
The server’s address. A literal address must pass the rules of every dial of the node. It must not be loopback, link-local, unspecified, multicast, a cloud metadata address or an address of an inbound’s pool. It must not be private, shared or unique local unless allow_private is true. The node itself looks up a server that is a name, through dns.servers, never by Xray or the host’s resolver. The first address that passes the rules is written into Xray’s document. A name that gives none leaves the outbound down. The names are looked up again every 15 minutes, and every 30 seconds while a name has no address. The address in use stays while the lookup still gives it, and a failed lookup changes nothing, so a resolver that flaps does not restart Xray. That lookup is plain UDP DNS, so whoever can see the node’s DNS traffic learns the provider’s name. Write the address when the name must stay hidden. Validation warns for each name.
Secrets. id and password are secrets. The control API shows the marker in their place and takes it back for the running outbound with the same tag, server, protocol and kind of security. Change any of those and you must write the secret out again.
veilway doctor checks the add-on and the socket folder when the config has an xray outbound, see “Commands”.
wireguard outbounds
Section titled “wireguard outbounds”From v0.3.6. A wireguard outbound sends flows through a WireGuard server of another party: Mullvad, a server of your own, any wg-quick peer. The node is the WireGuard client and runs the tunnel itself, in userspace, so it needs no wg tool and no interface. Use it behind a netstack exit. The guide shows the steps in the panel. Whoever runs the server sees what the exit carries, as any provider of an exit does.
| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | |
server |
host:port |
required | The server’s UDP endpoint. IPv6 literals go in brackets. A name is looked up by the system’s resolver at start, every 5 minutes and whenever the node starts a handshake again. The last address is kept while the name still resolves. |
public_key |
key | required | The server’s public key. |
private_key |
key | required | Secret. The node’s own key for this server. |
preshared_key |
key | none | Secret. |
addresses |
list of 1 or 2 addresses | required | The node’s addresses inside the tunnel, at most one of each family. A /32 or /128 at the end is dropped. A destination of a family with no address here is refused as unsupported. |
mtu |
integer, 1280 to 1500 | 1420 | The tunnel’s MTU. |
keepalive_secs |
integer, 0 to 3600 | 25 | Seconds between keepalives. 0 sends none. |
max_flows |
integer, 16 to 4096 | 1024 | The flows open through the tunnel at once, TCP and UDP together. A flow past it is refused and counted. |
bind, interface |
as for direct |
none | The local address and the network interface of the UDP socket that talks to the server. |
{"tag": "wg-out", "type": "wireguard", "server": "198.51.100.7:51820", "public_key": "<server public key>", "private_key": "<node private key>", "addresses": ["10.2.0.2", "fd00::2"]}Each flow of the exit is opened from the tunnel by a TCP/IP stack of its own. Every flow leaves from the node’s one tunnel address, so the server cannot tell users apart.
What is refused.
- A
serverthat is loopback, link-local, unspecified, multicast, a cloud metadata address or an address of an inbound’s pool. Private addresses are allowed, since the server may be on your own network. A name that resolves to a refused address leaves the outbound down. - An address inside the tunnel that is loopback, multicast, unspecified, or inside an inbound’s pool.
- A
private_keythat is the server’s, or any inbound, device or link key of the config, or the key of anotherwireguardoutbound. A key has one use. - A
viaon the outbound, and the outbound as theviaof another outbound, of a link or of an upstream. It carries flows of its own and cannot carry a chain. - An exit on it whose
unsupported.udpisdirector{"via": ...}. Userejectordrop.
Warnings.
- Every
wireguardoutbound warns: users routed through it reach whatever the server’s own network reaches, its other peers included. The node’s gate only refuses the node’s own addresses and networks. If users must not reach those ranges, add a routing rule that rejects them, such as{"tag": "no-wg-lan", "ip": ["10.2.0.0/16"], "action": "reject"}. - A
serverwritten as a name: whoever sees the node’s lookups learns the name. Write the address when it must stay hidden. routing.fallbacknext to an exit on it: users fall back to that exit while the outbound is down.
Health. The outbound is down when the server has not answered a handshake for 20 seconds, when a datagram could not be sent, or when one of its two tasks ended. A task that ends is started again after 2 seconds, then 4, doubling to 60. A down outbound tries a new handshake on the same schedule, and every change is logged at info with the reason. An outbound with no handshake for 180 seconds and nothing unanswered is idle and counts as up. While it is down the exit does not work: flows end with a reason and nothing goes to direct, to another exit or to the provider’s default. GET /v1/exits shows the state, see the API reference.
Limits.
- One user holds at most a quarter of
max_flowsflows of an outbound, never fewer than 16. A quarter ofmax_flows, at most 16 places, are kept for the node’s own DNS lookups through the outbound, so a user cannot starve them. - Each flow costs about 272 KiB once it carries data, so the default 1,024 flows come to about 272 MiB and 4,096 to about 1.1 GiB.
- One outbound runs on one core. Measured on three cores of one machine, one connection moved about 54 MB/s in each direction in a release build. More flows share the same core.
- Packets over the MTU, and datagrams too large for it, are dropped and counted.
DNS. The node’s resolver asks through the outbound for users routed to it, over UDP and with a fallback to TCP. No name leaves around the tunnel.
Changes. Adding or removing the outbound, or changing anything the tunnel is made of (the server, the keys, addresses, mtu, keepalive_secs, max_flows, bind, interface, or the pool of an inbound) restarts its tunnel, and flows open at that moment end. Other outbounds, exits and users are not touched.
Secrets. private_key and preshared_key are secrets. The control API shows the marker in their place and takes it back only for the running outbound with the same tag, server and public key. Change any of those and write the key again.
Not hidden. The outer UDP is plain WireGuard, easy to recognize and block. Where WireGuard is blocked, use an xray outbound. Whoever sees the node’s lookups learns a server name, and the server sees every flow.
An exit is a named way out that users are routed to.
| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | |
name |
string, 1 to 64 characters, no control, bidirectional formatting, zero-width, line separator or other invisible characters | the tag | Display name. |
country |
two uppercase letters | none | ISO 3166-1 alpha-2 code shown in clients. Not checked against the ISO list. |
backend |
object | required | See below. |
load |
object | none | Tells apps how busy this exit is. Off when absent. See “load” below. An error on a block exit. |
backend
Section titled “backend”type |
Carries | Keys |
|---|---|---|
netstack |
TCP and UDP | outbound (tag, required), proxy_only, tcp_idle_secs (10 to 86400, default 7200), udp_idle_secs (5 to 3600, default 60), unsupported |
l3 |
every IP protocol | interface (matching [a-z][a-z0-9_-]{0,14}, default veilway0, unique across exits), masquerade (bool, default true). Linux only, needs CAP_NET_ADMIN: on another system run refuses to start. check does not look at the system it runs on. |
block |
nothing | none. Every packet is dropped silently. |
chain |
every IP protocol | links (1 to 4 link tags, required). The traffic leaves from another node through these links, see links below. No key of netstack or l3 is allowed. |
upstream |
every IP protocol | upstream (the tag of an entry of upstreams, required). The traffic leaves through the provider’s network, see upstreams below. No key of netstack, l3 or links is allowed. |
Engine, l3:
- Only one
l3exit can run on a node for now. A second is refused at start. The host needsip(iproute2) andnft(nftables). - With
masqueradetrue, the node turns on forwarding for the whole host if it is off and sets it back when it stops (it stays on if the node is killed). Withmasqueradefalse, the node still loads a small nftables table namedveilway_<interface>that keeps tunnel traffic away from the host itself. - Return traffic is delivered only when it belongs to a live flow that left through that exit. Flows idle out after 7200 seconds for TCP and 60 for UDP.
- An
l3exit can go down while the node runs: its interface was removed under the node, or a reload closed the interface and could set up neither the new one nor the old one again. The node then tries to bring the interface up with the config in force, 2 seconds after it went down, then after twice the wait each time, once a minute at most, until it is up. - While the exit is down, traffic routed to it is answered as unreachable (a reset for TCP, host unreachable for everything else, counted under
unreachablein the drop counters). It never leaves through another exit. The node logs a warning with the failed step when the exit goes down and for every failed try, and an info line when it is up again.GET /v1/exitsshows the state ininterface_up,interface_down_secsandinterface_flips. - A reload that leaves the
l3exit as it is goes through while the exit is down, and makes the node try at once. A reload that changes or removes the exit ends the tries for the old one. Stopping the node ends them too and leaves no interface, route, nftables table or sysctl behind.
The destination gate
Section titled “The destination gate”Every exit refuses these destinations, whatever the config says:
- every address inside an inbound pool (traffic between users never leaves through an exit, see below);
- loopback and link-local (
169.254.0.0/16,fe80::/10); - the cloud metadata addresses
fd00:ec2::254and100.100.100.200(from v0.2.15; only these two addresses, your rules handle the networks around them); - multicast, broadcast, unspecified and
0.0.0.0/8; - IPv6 addresses that embed an IPv4 address (IPv4-compatible, NAT64, 6to4, Teredo) whose embedded address is refused;
- every IPv4-mapped IPv6 destination (
::ffff:a.b.c.d), outright.
The node’s own public addresses are not on that list either. Users reach them unless a rule refuses them, see Reaching the server itself. Private networks are not on that list. Routing rules decide about them, so an operator can refuse them, allow them, or allow them for some users only. See geoip:private under routing.
Traffic between users
Section titled “Traffic between users”A packet whose destination lies inside an inbound pool is never sent to an exit, but routing rules still decide about it. The name tunnel:users in a rule’s ip list stands for every address in every pool except the gateways:
- If the matching rule says
blockorreject, the packet is refused. A config that isolates users, the normal case, has a rule that rejectstunnel:users. - Otherwise the node delivers the packet to the device that holds the address, on the same inbound or another one. An address no connected device holds is answered as unreachable.
tunnel:<inbound tag>, for example tunnel:gaming, stands for the addresses of one inbound only. With the inbound condition, which says where a packet came from, a rule can be about one tunnel in either direction. An inbound cannot have the tag users.
A shared network for games on one tunnel, with every other tunnel isolated, is one rule placed before the isolating rule. Its action "user" means “handle normally”, which for a destination inside a pool is delivery to that device:
{"tag": "gaming-lan", "inbound": ["gaming"], "ip": ["tunnel:gaming"], "action": "user"},{"tag": "isolate-users", "ip": ["tunnel:users"], "action": "reject"}Users on gaming reach each other but not users of other tunnels, and users of other tunnels reach nobody.
Three things hold whatever the rules say. A device can only send from its own addresses. A gateway address answers DNS and ping from the devices of its own inbound and nothing else, plus, on a veilway inbound, the control channel of that inbound. Broadcast and multicast are not carried, so games that find each other by local broadcast will not see other players. Connecting by address works.
proxy_only and unsupported
Section titled “proxy_only and unsupported”proxy_only (bool) says that traffic through this exit leaves only through its SOCKS5 proxy. It defaults to true when the outbound is socks5 or xray. It must be absent or false when the outbound is direct.
unsupported says what a netstack exit does with traffic its outbound cannot carry:
| Key | Applies to | Default | Allowed values |
|---|---|---|---|
icmp_echo |
ping | "local" |
"local", "reject", "drop", "direct", {"via": "<exit tag>"} |
udp |
UDP when the proxy has no UDP ASSOCIATE (DNS keeps its TCP fallback) | "reject" |
"reject", "drop", "direct", {"via": "<exit tag>"} |
other |
every other IP protocol | "reject" |
"reject", "drop", {"via": "<exit tag>"} |
rejectanswers with the matching ICMP error.dropdiscards.localmakes the node answer the ping itself, after the destination gate.directsends from the node’s own address around the proxy, andviahands the traffic to another exit. Either is an error unless the exit sets"proxy_only": false, and validation still warns for each use.- A
viatarget must exist and must not be the exit itself. It must be anl3exit foricmp_echoandother, and must carry UDP forudp. The node never consults the target’s ownunsupportedpolicy, so chains and loops cannot form. - On a
directoutbound, settingudpis a warning: it never applies. - On an
xrayoutbound,udpmust berejectordrop. The outbound carries UDP itself, anddirectandviawould let the flow leave around it while it is down.
Engine:
directforicmp_echouses unprivileged ping sockets and is checked at start.- The
udppolicy is decided once per flow at its first packet, and the flow never moves between the proxy and the other path. A UDP flow the proxy has carried is refused if the proxy loses UDP. Every switch of an upstream between “has UDP” and “has no UDP” is logged at info and counted. - A destination sent by
directorviamust pass the gate of both exits, and a flow sent byvianeeds the user to be entitled to both. Aviatarget that cannot carry the packet rejects it. rejectanswers are rate limited per device (burst 20, then 10 a second).- ICMP codes used: administratively prohibited for rules, the gate, entitlement and a rejected ping. Host unreachable for a tunnel address nobody holds. Port unreachable for
unsupported.udp. Protocol unreachable on IPv4 and prohibited on IPv6 forunsupported.other. icmp_echo: directallows 16 ping identifiers per device, 32 per user and 1024 in total.
From v0.2.18. An exit with a load object tells every device of its users how busy it is, as one of three levels: low, medium or high. Apps show them as Not busy, Busy and Very busy. When load is absent, apps show nothing for the exit.
| Key | Type | Default | Meaning |
|---|---|---|---|
by |
flows or mbps |
required | flows: the flows open on the exit, all users together. mbps: the exit’s traffic, up and down added together, in megabits per second, averaged over the last 30 seconds. |
busy |
integer 1 to 1,000,000 | required | The level is medium from this value. Below it, low. |
very_busy |
integer 2 to 1,000,000 | required | The level is high from this value. Must be greater than busy. |
"load": {"by": "flows", "busy": 50, "very_busy": 200}- The node looks every 10 seconds. A level changes only after the new one held for two looks in a row, so a value that crosses a threshold and comes back does not flap. The first look that has a value sets the level at once. For
mbpsthere is no level before the second look. - A device is told a change at most once in 60 seconds, however often the node looks. It is also told when a reload or a change of whether the exit works would have sent the list anyway. A level that was told and still holds stays as told.
- The levels are coarse on purpose. A device learns at most that the exit got busier, a minute late, never a number, so it cannot watch what other users do. Without
loadthe node sends nothing about other users’ traffic. - A
blockexit has no load, andloadon one is an error. - The control API’s
GET /v1/exitsshows the setting asloadand the level apps are told asload_level.
routing
Section titled “routing”The first packet of a flow is matched against rules in order. The first match decides, and the whole flow keeps that decision.
| Key | Type | Default | Meaning |
|---|---|---|---|
default |
exit tag | required | Used when nothing else chose an exit. |
fallback |
list of exit tags, at most 16 | none | The exits a device without a choice of exit leaves through while its own exit is not working, in this order, each only when the device’s user may use it. It goes back to its own exit once that works again. No block exit, and no tag twice. Without it a device stays on its exit whatever happens to it. An entry node needs that: nothing goes round a node link that is down. From v0.2.11. |
rules |
list | [] |
See below. |
A rule
Section titled “A rule”All conditions present in a rule must hold. Inside one list, any entry may match. A rule needs at least one condition.
| Key | Type | Matches |
|---|---|---|
tag |
tag | required. The rule’s identity for the control API. |
enabled |
bool, default true |
Not a condition. A disabled rule is validated like any other and skipped when flows are matched. |
inbound |
list of inbound tags | the inbound the packet arrived on. A proxy inbound’s tag is a tag like any other: a flow that comes in through one is decided with that inbound. |
transport |
list of transport names | the kind of session the flow started on, see “Transport” below. From v0.2.17. |
user |
list of user names | the sending user |
group |
list of group names | any group of the sending user |
link |
list of link tags | what arrives from one of these links, on a node that is the exit half of each. Not together with user or group, which a link does not have. |
ip |
list of networks, or the names geoip:private, geoip:<cc> (a country), host:self, tunnel:users and tunnel:<inbound tag> |
destination address. host:self is the machine’s own addresses, see Reaching the server itself. |
domain |
list of names | a name this user resolved to the destination address through the node. example.com matches that name only. *.example.com matches every name below it and not example.com itself. |
protocol |
list of tcp, udp, icmp, icmpv6, gre, esp, sctp, or an integer 0 to 255 |
IP protocol. A name and its number are the same entry. |
port |
list of integers 1 to 65535 or strings "lo-hi" with lo <= hi |
destination port. Needs protocol present and limited to tcp, udp and sctp. |
icmp_type, icmp_code |
lists of integers 0 to 255 | Need protocol to be exactly ["icmp"] or exactly ["icmpv6"]. icmp_code needs icmp_type. |
action |
{"exit": "<tag>"}, "user", "block" or "reject" |
required |
Transport
Section titled “Transport”transport holds a flow to the kind of session it started on. The names are udp (plain WireGuard), udp-obfs, tls, ws (both the native one and the ws of a wireguard inbound), ws-plain, fronted, fronted-shaped, faketls, socks5 and http. Like every condition it must hold together with the others, and the first matching rule still decides. A rule that names a transport and a group matches only that group’s flows on that transport.
{"tag": "no-proxy-to-private", "inbound": ["socks"], "ip": ["geoip:private"], "action": "reject"},{"tag": "calls-direct", "transport": ["socks5"], "protocol": ["udp"], "action": {"exit": "direct"}}- For a tunnel flow it is the transport of the device’s session when the flow was created. A device that moves to another transport keeps its old flows where they began, and its new flows follow the rule. A reload that decides a flow again also keeps its transport. A device that races two sessions cannot swap it.
- For a proxy flow it is
socks5orhttp. Amixedinbound reports the one its connection spoke, and UDP ASSOCIATE issocks5. - A link this node dials runs on the transport of the candidate it dialled.
- A DNS query of the node’s resolver follows the
inboundandtransportrules of the flow it came in on. A name a proxy user asks for is resolved under the same inbound and transport. - A flow of the Xray add-on has no transport and matches no rule that names one. It has no inbound either, so a rule with
inboundnever matches it. Validation warns about an enabled rule withinboundwhile Xray inbounds exist. - Validation warns about an entry that names a transport no inbound of the node has, since it never matches.
A domain is lowercase ASCII (punycode for other scripts), at most 253 bytes with labels of 1 to 63, no trailing dot, and not an IP address in any spelling (a last label that is all digits, or 0x followed by hex digits, is refused). * may only be the whole first label and needs at least two labels after it.
geoip:private stands for every private and special-purpose range: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 240.0.0.0/4, fc00::/7 and 2001:db8::/32. It never matches an address inside an inbound pool: those are tunnel:users. A network in a rule, and geoip:private, also match an IPv6 destination that embeds a matching IPv4 address (IPv4-compatible, NAT64, 6to4, Teredo). A tunnel: name must be users or the tag of an inbound. A config that keeps users away from each other and out of the networks behind its exits starts its rules with:
geoip:<cc>, with a two-letter lowercase country code such as geoip:tr, stands for every address the five regional internet registries have allocated or assigned to that country (from v0.3.10). Like geoip:private, it never matches an address inside an inbound pool, and it also matches an IPv6 destination that embeds a matching IPv4 address.
- The list is a file,
/usr/local/lib/veilway/geoip.txt, or the path given with--geoiptorun,checkanddoctor. Each release carries one,veilway-geoip.txt, made from the registries’ files on the day of the release and signed like the other assets.veilway.sh install nodeandupdate nodeinstall it. - The node reads it only when a rule names a country, at start and at every reload, so a newer list takes effect with a reload. It keeps only the countries the rules name.
- A rule that names a country is an error when the file is missing or invalid, or when the country has no addresses in it. A list older than 180 days is a warning.
- Registry data is where an address is registered, not where its server stands. A network registered in one country and used in another follows its registration, and large cloud providers’ addresses mostly follow their home country. For a split by site, add
domainrules.
{"tag": "isolate-users", "ip": ["tunnel:users"], "action": "reject"},{"tag": "no-private", "ip": ["geoip:private"], "action": "reject"}To let one group reach one private network, put a rule for it before that one. To switch a rule off without deleting it, set "enabled": false.
Actions: {"exit": ...} sends the flow to that exit. "user" sends it to the user’s own exit, or to default when the user has none. block drops silently. reject answers with an ICMP error, or a TCP reset for TCP. A flow that matches no rule is treated as "user".
Entitlement
Section titled “Entitlement”A flow may only leave through an exit in its user’s exits, whatever chose it: a rule, the user’s exit, default, or a via. Otherwise the flow is rejected, never passed to another rule.
Validation:
- Error: a rule with a
usercondition sends a named user to an exit outside that user’sexits. - Error: a user with a non-empty restricted
exitshas noexitanddefaultis not in the list. - Warning, with a count of affected users: a rule without a
usercondition can send users to an exit they are not entitled to, directly or throughvia. - Error: a rule limited to protocols other than TCP and UDP sends them to a
netstackexit whose policy for that class isrejectordrop. UDP is never checked, because whether a proxy carries UDP is only known at run time. - Warning: a
domainrule with anexitaction comes before ablockorrejectrule. A user who controls any name matching the pattern can point it at any address, so adomainrule is a routing convenience and never a security control. - Warning: no enabled rule isolates users for everyone, that is, no enabled rule with
blockorrejecthastunnel:usersinipand no other condition. The warning says that users can reach each other’s devices. When aninboundcondition limits the rule, it names the inbounds where that is so. - Warning: no enabled rule refuses private destinations for everyone, that is, no enabled rule with
blockorrejecthasgeoip:privateinipand no other condition. The warning says that private networks behind the exits are reachable. - Warning: an enabled
blockorrejectrule whose only conditions areip,protocolandportmatches a server ofdns.serversor of a DNS set on port 53 before any rule that would send it to an exit, so the node cannot reach its own resolver. The warning gives the path of the server, for exampledns.sets[0].servers[1]. - Error: a rule with a
linkcondition names a link that is not the exit half on this node, a link no config has, or also has auserorgroupcondition. Sending a link to an exit that is not in that link’sexitsis an error too. - Warning: a rule with a
domaincondition and nouserorgroupthat could apply to a link. The exit never learns the names behind a link’s traffic, so the rule never matches it, and a block by name does not cover a link. - Warning, best effort: a single earlier rule completely covers a rule.
checkdoes this within a fixed budget. A reload does not.
Engine: protocol is the upper-layer protocol after walking IPv6 extension headers. Fragments are reassembled before routing, within a per-user bound, or dropped. A packet whose headers cannot be parsed is dropped and counted.
For flows between users, only an echo reply counts as the answer to a delivered echo request. The rules decide every other ICMP message for its own direction.
The flow table holds at most 65536 flows in total, 1024 per user and 512 per device. Refused flows are remembered apart from these limits (4096, the oldest gives way).
Reaching the server itself
Section titled “Reaching the server itself”Since v0.3.6, users can reach services on the node’s own public addresses unless a rule refuses them. Loopback and link-local stay refused by the gate. The panel listens on 127.0.0.1, so users cannot reach it.
The name host:self in a rule’s ip list stands for every address the machine holds on its interfaces. The node reads them again every 60 seconds and at each reload, so it follows DHCP and addresses added later. An IPv4 address of the machine written inside an IPv6 one matches too. Addresses of the inbound pools are not part of it. The rule holds for every door: tunnels, proxy inbounds, the Xray add-on and node links.
Nothing adds a rule for you. If other services listen on this machine (a database, an admin port, another panel), refuse them:
{"tag": "no-self", "ip": ["host:self"], "action": "reject"}A rule above it can let one user reach one port. Rules are checked from the top, so the first match decides:
{"tag": "alice-ssh", "user": ["alice"], "ip": ["host:self"], "protocol": ["tcp"], "port": [22], "action": "user"},{"tag": "no-self", "ip": ["host:self"], "action": "reject"}- A service that listens on every address of the machine is reachable through any of them until a rule refuses them.
- An address that no interface holds, such as a public address behind a 1:1 NAT, is not in
host:self. An anycast address and a broadcast address are not either. They stay reachable. - With an
l3exit, the node’s own firewall table still keeps tunnel traffic from the host itself, whatever the rules say. veilway doctorlists the addresseshost:selfcovers and says whether an enabled rule names it.
| Key | Type | Default | Meaning |
|---|---|---|---|
name |
matching [a-z0-9][a-z0-9._-]{0,63} |
required | Unique. This is the user’s identity: usage counters and rules refer to it. Renaming a user is removing one and adding another. |
enabled |
bool | true |
|
groups |
list of declared group names | [] |
|
devices |
list of 1 to 64 | required | See below. Empty only for a user with a proxy_password, who then uses proxy inbounds alone. |
proxy_password |
16 to 128 printable ASCII characters, no : |
none | Secret. The password for the socks5, http and mixed inbounds, with name as the user name. See Proxy inbounds. From v0.2.17. |
exits |
"all" or a list of exit tags |
required | Exits the user may leave through. An empty list means none: the user can connect but every flow is rejected, and validation warns. |
exit |
exit tag | none | The user’s own exit. Must be in exits. |
quota_bytes |
integer 1 to 9007199254740991 | none | Bytes of inner IP traffic, both directions, per period, including traffic that was rejected or answered locally. See Usage and quota. |
quota_period |
daily, weekly, monthly |
none | When the count resets, at 00:00 UTC (weekly on Monday, monthly on the 1st). Needs quota_bytes. Without it the quota never resets. |
expires_at |
RFC 3339 timestamp with an explicit offset | none | From this moment the user is treated as disabled. |
speed_up, speed_down |
integer 16384 to 12500000000, bytes per second | none | The most the user sends and receives through this node. Without a key there is no limit. See Speed limits. From v0.3.6. |
devices
Section titled “devices”| Key | Type | Default | Meaning |
|---|---|---|---|
name |
matching [a-z0-9][a-z0-9._-]{0,63} |
required | Unique within the user. |
public_key |
key | required | Unique across all devices of all users, and never equal to the public key of an inbound. |
preshared_key |
key | none | Secret. |
transport_secrets |
list of 1 or 2 secrets | none | Secret. Required for a device with an address in a veilway inbound, where the device proves one of them to the control channel. A secret is 32 random bytes written like a key, and veilway gensecret prints one. No two devices share one, and none is a WireGuard key of the config. Two allow rotation: add the new one, give it to the device, remove the old one later. On a device with no address in a veilway inbound the key is a warning. |
addresses |
list of 1 to 16 host addresses, written without a prefix | required | The device’s tunnel addresses. |
Each address must lie in the pool of exactly one inbound and may not be that pool’s gateway, network or IPv4 broadcast address. A device has at most one IPv4 and one IPv6 address per inbound. No address is used by two devices.
Engine: when a user is disabled, expires, runs out of quota or is removed, or when a device’s key or addresses change or the device is removed, the core drops the session keys, resets every flow and clears the DNS memory that belong to it within one second, and logs it at info. A timer and the byte count drive expiry and quota, not only a check at handshake. A backward step of the node’s clock does not re-enable a user. Usage is kept outside this document, see below.
Usage and quota
Section titled “Usage and quota”Engine. Usage never lives in the config document. It is kept in one state file (see --state under Commands) and survives reloads and restarts. Without a state file path, the node keeps usage in memory only and loses it on restart.
- Over quota. A user at or over quota is cut off by the same path as a disabled user, within one second, and the info log line names quota as the reason. The user cannot shake hands again until the period rolls over, a reload raises or removes the quota, or the operator resets the user’s usage.
- Clock steps back. A backward step of the clock by less than one period never resets a count and never re-enables a user, at that moment or when the clock moves forward again. The period start stays where it was.
- A period start far in the future. A period start more than one whole period after the node’s clock (more than 1 day for
daily, 7 days forweekly, 31 days formonthly) is not trusted. It can come from an edited state file, a state file written while the clock was wrong, or a clock that was ahead by more than a period and was then set right. Left alone, such a count would not reset until the clock reached that time, which can be years. So the node sets the period start to the start of the period its clock is in, keepsused_bytes, and logs one warning that names the user. It checks this when it loads the state file and four times a second while it runs. The price: a clock that jumps back by more than a period and then forward again gives that period’s quota a second time. Keep the node’s clock synchronized. - Without
quota_period. Any period start after the node’s clock is set to0withused_byteskept and one warning. Nothing resets by itself for such a user, so this costs nothing. - Reset by the operator.
veilway usage reset --user <name>, orPOST /v1/users/{name}/usage/reseton the control API, setsused_bytesto 0 and the period start to the start of the current period. A user who was over quota can connect again at once, without a reload. Lifetime totals are kept unless you give--totals("totals": true). See Commands and the control API reference. - Counters. Every user, inbound and exit has totals of up and down bytes and packets, which the control API serves. Whoever polls computes rates. Counters follow names and tags across reloads. Exit counters count inner IP packets and bytes where a packet is handed to or taken from the exit. A flow that leaves by way of another exit (
via) is counted on the exit that carries it, not on the exit the rule chose. - State file. Versioned JSON keyed by user name. Per user it holds the bytes used in the current quota period, the start of that period, and lifetime totals (up and down, bytes and packets). Next to the users it holds
devices: for each device that chose an exit on the control channel, its public key and that exit’s tag. The version stays 1, because an older build passes that key over and still reads every user’s count. The file keeps the choices of at most 65,536 devices, and no more than fit in the 16 MiB a state file may have next to its users (with 65,536 users that is about 1,500). After a restart, a device whose choice is not kept has chosen no exit, and the node warns when that happens. - Writes. The file is written with mode 600 through a temporary file in the same directory, then fsync, then rename, at most once every 10 seconds and only when something changed. It is also written on shutdown, right after a user is cut off for quota, and right after a usage reset. Writing never blocks traffic. A failed write is retried, and logged once and then every 10 minutes while writes keep failing. If the node is killed, at most about 10 seconds of counted usage can be lost.
- Start. A missing file means zero usage. A file that cannot be parsed, has an unknown version, or is larger than 16 MiB is kept under the same name with
.badappended and logged at error, and the node starts from zero, since an unreadable state file must not keep a node down. A state file that group or others can write is treated the same way. Values are clamped, never trusted: a count above 9007199254740991 saturates at it. One entry that is not valid is skipped, and only that user starts from zero, or only that device has chosen no exit. The node reads no more than 65,536 users and 65,536 devices from a file. An exit in the file is held against the config like one a device asks for: when it is not in the user’sexitsit is dropped at start and logged at info. If the path is not a regular file (a directory, a symbolic link, a device), the node leaves it untouched, logs an error and keeps usage in memory until it stops. - Removed users. The entry of a user the config no longer names is dropped at the commit that removes it, and so is the entry of a device. A renamed user starts from zero.
Speed limits
Section titled “Speed limits”speed_up is the most a user sends through this node and speed_down the most the user receives, in bytes per second (16384 to 12500000000). A user with neither key has no limit. From v0.3.6.
- Per user. All of the user’s devices and proxy logins share one limit, so more devices get no more speed.
- Per node. The same user on two nodes has the limit on each.
- TCP is slowed down. TCP that the node holds as a stream (a
netstackexit, the proxy inbounds, the Xray add-on) waits for its turn. Nothing is lost. - Other traffic over the rate is dropped. That is UDP, pings, and anything through an
l3,chainorupstreamexit. A drop is counted asrate_limitedindropsand in the user’slimitedcounters. - DNS and the control channel are never dropped. They count against the limit, so a user at the limit can still look up names and keep the control channel up.
- A short burst passes. It is one second at the rate, at least 64 KiB, and cannot be set.
- A dropped upload still counts toward the quota. A dropped download does not.
- Node links have no limit of their own. Limit the user on the entry node.
- Apps do not show it.
GET /v1/usersand the panel do.
notices
Section titled “notices”A list of at most 4 notices that the control channel of every veilway inbound gives to every device. The node writes none of its own.
| Key | Type | Default | Meaning |
|---|---|---|---|
severity |
information or warning |
information |
|
text |
1 to 280 bytes | required | Plain text. Control characters, line breaks, bidirectional formatting and zero-width or other invisible characters are refused, so a client shows it as written. A client shows no link in it. |
"notices": [{"severity": "warning", "text": "Maintenance on Friday from 02:00 UTC"}]Notices on a node with no veilway inbound are a warning.
These notices reach only connected devices. A panel can also put notices into every signed subscription document. Apps keep those from their last refresh and show them even when they cannot connect. That is a panel setting, not part of this config, see the panel manual.
public_addresses
Section titled “public_addresses”A list of at most 8 host names or IP addresses, with no port and none twice. A profile of this node gets these addresses, each with the port of the listener of one of its transports. The control API shows them in GET /v1/status and GET /v1/inbounds. A host name is lowercase ASCII, written as for a rule’s domain without *. An IPv4-mapped IPv6 address and an address with a zone are errors, as everywhere.
"public_addresses": ["node.example.com", "203.0.113.7"]replay
Section titled “replay”Engine. The replay memory makes sure a proof is accepted once (for udp-obfs an opening packet, for tls the token of an upgrade request). One memory serves every obfuscated transport of the node. A node without such a transport keeps none, and the key is then a warning.
| Key | Type | Default | Meaning |
|---|---|---|---|
cache |
integer 1024 to 1,048,576 | 262144 |
How many accepted proofs the node remembers at once. When it is full, every proof is refused until entries leave, which they do 60 seconds after their time. The node reads it at start. A reload that changes it logs a warning and changes nothing until the next start. |
per_source |
object with burst and rate, each 1 to 65,536 |
{"burst": 256, "rate": 32} |
Accepted proofs one source may bring: burst at once, then rate a second. A source is an IPv4 address or an IPv6 /64. Raise it when many users share one address. A reload changes it at once. |
"replay": {"cache": 262144, "per_source": {"burst": 256, "rate": 32}}The replay file
Section titled “The replay file”The memory is kept in a file of its own: veilway-replay next to the state file, or the path given with run --replay. Next to it the node keeps a lock file (veilway-replay.lock) and, during a write, a temporary file (veilway-replay.tmp).
- Start. A node does not start a config with an obfuscated transport, and does not reload into one, without a replay path whose directory it can write. A second node on the same file does not start. Two nodes never share a replay file, and an inbound’s keys belong to one node.
- Size. While the node runs, the file is 68 bytes and is written about every 10 seconds, with mode 600. A clean stop writes the cache as well, 41 bytes for each entry, at most about 10 MiB at the default
cache. - After a clean stop nobody is refused because of the restart.
- After a kill or a crash the node refuses proofs for up to 20 seconds, or up to 80 if a device with a fast clock had connected just before. Logged at info.
- With no file, which is how a node starts for the first time, every proof is refused for 60 seconds. Logged at warn.
- A file that cannot be used (cut short, damaged, larger than
cacheallows, writable by group or others) is moved aside with.badbehind its name and logged at error. The node then starts as with no file. A config that lowerscachebelow the entries of the last clean file makes that file unusable in this way. - A file that cannot be written while the node runs: within about 20 seconds the node accepts no new proof. Open sessions go on. Logged at error once a minute, and counted in
replay.write_failuresof/v1/stats.
Three things undo replay protection:
- Restoring the replay file from a backup or a machine snapshot. An older file reopens replay for everything accepted since it was taken. If in doubt, delete it: that costs 60 seconds of refusals.
- A reset (below) after the node’s clock ran ahead.
- A lost file on a node whose clock is behind where it has been. Do not start the node before its clock is synchronized.
When the floor is ahead of the clock
Section titled “When the floor is ahead of the clock”A node whose clock ran ahead and was then put right can be left refusing every device: its floor, the time up to which proofs are refused, lies in the future. The outage lasts as long as the clock was wrong by, and waiting is the only thing that ends it by itself.
It is not the same as what a start costs. After a start or a reset, the floor is up to 80 seconds ahead of the clock, as the list above says. That passes by itself, is logged once at start with the time left, and is shown as replay.refusing_secs of /v1/stats. Do not reset for it: a reset costs another 60 seconds.
The fault is a floor more than 80 seconds ahead of the clock, or one that rose while the node ran:
- The node logs it at error once a minute with the distance, and counts the refused proofs in
replay.refused_floor_ahead.replay.floor_aheadof/v1/statsis true. veilway doctorshows the floor, the limit and the clock. It fails when the floor is more than 80 seconds ahead, and warns when the limit is. A floor up to 80 seconds ahead is reported as what a start costs, with the time left.veilway replay resetends it, see Commands.
A list of at most 64 node links. A link joins this node to one other node. Users connect to one of the two (the entry) and their traffic leaves from the other (the exit). See the node links page for roles, directions, pairing and what each node enforces.
Do not write a link by hand. veilway link offer and veilway link answer pair two nodes and print the entry for each node’s config, which you put into links, see Commands. A link is either one this node is dialled for (it has inbound and public_key) or one it dials (it has peer and private_key), never both.
| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | |
id |
22 characters of base64url | required | The id the pairing made. Unique. |
direction |
forward or reverse |
required | Who dials. In a forward link the entry dials the exit, in a reverse link the exit dials the entry. |
enabled |
bool | true |
A link that is not enabled is validated like any other and neither dialled nor answered. |
addresses |
object | required | dialler: 1 or 2 addresses, at most one IPv4 and one IPv6. listener: one address for each family of dialler, in the same order, in a reverse link and in no other. They lie in the pool of the inbound the link connects to. |
mtu |
integer 1280 to 1500 | 1390 when the link lists udp-obfs, else 1420 |
MTU inside the link’s tunnel. Above the default is a warning, since nothing is fragmented for a link. |
transport_secret |
secret | required | The secret the two nodes agreed on when they were paired. It never travels. No device and no other link has the same, and it is not a WireGuard key or a transport key of the config. |
inbound |
inbound tag | none | On the node that is dialled: the veilway inbound the link connects to. It has no fronted or fronted-shaped transport and carries no users’ devices. Needs public_key. |
public_key |
key | none | On the node that is dialled: the WireGuard public key the other node made for this link. Needs inbound. |
private_key |
key | none | Secret. On the node that dials: the WireGuard key it made for this link. Needs peer. |
peer |
object | none | On the node that dials: inbound_public_key (the key of the other node’s inbound), transport_public_key (needed when a tls or ws transport is listed) and transports (1 to 8). Needs private_key. See below. |
exit |
exit tag | none | On the exit half: the exit the link leaves through when no rule names one. Without it that is routing.default. Must be in exits. |
exits |
list of exit tags | required on the exit half | On the exit half: the exits the link may leave through. A chain exit is among them only with allow_chain. An upstream exit never is. |
allow_chain |
bool | false |
On the exit half: whether the link may continue through a chain exit of this node, into another link. The node the traffic leaves from then sees this node’s link address, and the entry is not told. A chain has at most 3 links, see Chaining. An error on the entry half. From v0.3.6. |
allow_private |
bool | false |
On the exit half: whether rules decide about private networks for this link. Without it they are refused whatever the rules say. |
max_flows |
integer 1 to 65,536 | 16384 |
On the exit half: the flows the link may have at once. It is a warning when the max_flows of the links this node is the exit of come to more than half of the 65,536 flows the node holds. |
quota_bytes, quota_period, expires_at |
as for a user | none | On the exit half. They mean what they mean for a user. |
Which half a node is follows from direction and from whether the link has inbound or peer. The entry half has the users and is used through a chain exit. The exit half takes traffic from the other node. exit, exits, allow_private, allow_chain, max_flows, quota_bytes, quota_period and expires_at are errors on the entry half, and exits is required on the exit half. An entry half that no chain exit names is a warning.
peer.transports lists the other node’s transports in its order of preference, in the shape a profile of a subscription has:
| Key | Type | Meaning |
|---|---|---|
type |
udp, udp-obfs, tls or ws |
fronted and fronted-shaped are read and refused: a link carries the traffic of many users, and polling that through a CDN is not a transport of a link. |
endpoints |
list of 1 to 16 ip:port |
For udp and udp-obfs, which take neither path nor candidates. |
path |
as for tls |
For tls and ws, together with candidates. |
candidates |
list of 1 to 16 objects | For tls and ws. Each has address (ip:port), sni (a lowercase host name, or "" for none), host (the Host of the request), verify_name (the name the certificate is held against: required when sni is empty and not allowed otherwise) and pins (1 to 4 SHA-256 hashes of the certificate’s SubjectPublicKeyInfo, in base64). self_signed (only on tls: true trusts the certificate through its pins alone, see below). A tls candidate needs pins. Unless self_signed is set, the chain is validated and a pin is a second condition. |
A tls candidate with "self_signed": true is for a node with no domain. Its certificate is trusted through its pins alone: no chain, name or date is checked, and the handshake must be signed by the pinned key. It needs 1 to 4 pins and no verify_name, and sni may be "". It is an error on ws. A new key on that node breaks the link until the operators pair again. See Pins and self-signed links.
An address the node dials may not be loopback, link-local, unspecified, multicast, broadcast, in 0.0.0.0/8, inside an inbound pool of this node, or an IPv6 address that carries an IPv4 address. A private address is a warning.
Other checks: a link’s key is not an inbound’s or a device’s, and no two links share a key. The addresses of a link must be host addresses (not the gateway, network or broadcast address) of its inbound’s pool on the node it connects to. On the node that dials, they must lie outside every pool of this node, inside 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 or fc00::/7, where the pool they come from lies. The chain exit that names a link must name an entry half, and no two exits name the same link.
Engine. A link that is down carries nothing, and nothing routed to it goes round it: the flows are answered as unreachable. The dialler tries one candidate at a time and never latches a failure. GET /v1/links of the control API shows each link’s state, and veilway doctor checks the links.
tunnels
Section titled “tunnels”A list of at most 64 tunnels. A tunnel forwards one TCP or UDP port of this node to one address on the far side of a node link, through a chain exit, with no client. See “Tunnels” on the node links page for what each node does.
| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | |
enabled |
bool | true |
A tunnel that is not enabled is validated and does not listen. |
protocol |
tcp or udp |
required | |
listen |
ip:port |
required | The address this node listens on. No other tunnel, inbound transport or listener of this node uses the same address, port and protocol. |
exit |
exit tag | required | A chain exit. |
target |
ip:port |
required | The address the far node is asked to reach. Not a link address, not a listen address of this node, not inside an inbound pool of this node, and nothing the destination gate refuses. A private address works only when the far node set allow_private for the link. |
sources |
list of 1 to 64 networks | anyone | The networks that may connect. Required for a UDP tunnel that listens on an address that is not loopback. Without it, such a TCP tunnel is a warning. |
max_connections |
integer 1 to 4096 | 256 |
TCP connections or UDP sessions at once. One source (an IPv4 address or an IPv6 /64) holds at most 16. All tunnels together come to at most 16,384. |
Engine. A tunnel’s connections leave from the entry’s link address with ports from the entry’s translation, as users’ flows do. The far node’s rules for the link decide them. While the chain exit is down, connections are refused. /v1/stats counts each tunnel under tunnels.
upstreams
Section titled “upstreams”A list of at most 8 other providers’ Veilway subscriptions. The node holds one device on each and uses it as the way out of an exit: the exit’s backend is {"type": "upstream", "upstream": "<tag>"}, and at most one exit names each upstream. The upstream protocol page says what the provider sees and can do. The operator guide walks through enrollment.
| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | Any tag but preview, which the control API uses for previews. |
enabled |
bool | true |
A disabled upstream is checked, and neither fetched nor connected. |
link |
string | required | The provider’s native veilway://sub/ link. It holds the account’s token, so it is a secret. |
signing_key |
string | required | The provider’s signing key the operator confirmed: 8 groups of 4 hex digits with spaces, or 43 characters of base64url. The link must carry this key. |
private_key |
string | required | The node’s device key for this provider, a 32-byte key in standard base64, as WireGuard keys are written. A secret. Not all zero, and no two upstreams share one. |
device_secret |
string | required | The device secret for this provider: 16 bytes as 22 characters of base64url. A secret. |
profile |
string | automatic | The provider’s profile to use. Without it the node tries every profile in the document’s order, starting with the last one that carried. |
failover |
bool | false |
With a profile: whether the node goes on to the other profiles when that one cannot be used. Without failover a named profile is the only one used. Without a profile every profile is tried whatever this says. From v0.3.6. |
transport |
string | automatic | The type of transport to connect over: udp, udp-obfs, tls, ws, ws-plain, fronted or fronted-shaped. Without it the node picks from every transport of the profile, as the apps do. A type outside allow is refused. From v0.3.6. |
exit |
string | the provider’s default | The provider’s exit to leave through. A change of exit alone is taken on the running tunnel: nothing leaves through the old exit meanwhile. Any other change makes the tunnel again. |
via |
outbound tag | none | A direct or socks5 outbound for the fetch and for TCP transports. A proxy on a loopback, unspecified or link-local address is refused. |
allow |
list of up to 4 classes | empty | Transport classes besides cover that the node may use: no_cover, exposed and plain. |
allow_private |
bool | false |
Whether the provider may have the node dial private addresses. |
Validation refuses a link that is not native, one whose key is not signing_key, a preview tag, two upstreams with one account or one device key, two exits for one upstream, and an unknown via. veilway check warns about an upstream that no exit names, one with no exit set, and one that an exit uses while routing.fallback exists, since users then fall back to it while the provider is down. The node keeps upstreams.state next to the usage state file. Removing an upstream through the control API is kept until the provider confirms, see the protocol page.
The Xray add-on: inbounds that the official xray binary serves, with users that are users of this node. It is off when the key is absent, and nothing runs until the add-on is installed, see the Xray manual. The traffic of Xray’s users goes only to the node, so the gate, rules, exits, quotas and counters apply to them as to a tunnel’s users. (The unit does not keep Xray itself from the network, since xray outbounds need it to reach their providers.) An Xray user’s traffic leaves through netstack exits only: l3 and chain exits are not built for it.
| Key | Type | Default | Meaning |
|---|---|---|---|
inbounds |
list of at most 16 | required | See below. |
"xray": {"inbounds": [{ "tag": "vless-443", "protocol": "vless", "listen": "0.0.0.0:8443", "tls": {"cert": "site.crt", "key": "site.key"}, "decoy": "http://127.0.0.1:8080", "clients": [{"user": "alice", "secret": "5783a3e7-e373-51cd-8642-c83782b807c5"}]}]}| Key | Type | Default | Meaning |
|---|---|---|---|
tag |
tag | required | Unique among the Xray inbounds. |
protocol |
vless, trojan or shadowsocks |
required | VMess and Reality are not built. |
listen |
ip:port |
required | The public listener of Xray. Unique among the Xray inbounds. A shadowsocks inbound uses its port for UDP as well, so no udp or udp-obfs transport of the node may share it. |
tls |
cert and key, both file paths |
required for vless and trojan, not allowed for shadowsocks |
The certificate chain and its key, as PEM files of at most 1 MiB. The node reads them and hands them to Xray. self_signed is not available here. |
decoy |
http:// URL, as decoy.origin of tls |
required for vless and trojan, not allowed for shadowsocks |
Your own web server. After the TLS handshake, a connection that does not carry a user’s credential goes to it. |
server_key |
32 bytes as standard base64 (44 characters) | required for shadowsocks, not allowed for the others |
Secret. The server’s key of the cipher 2022-blake3-aes-256-gcm. |
clients |
list of at most 65,536 | required | The users that may use this inbound. Each has user (the name of a user, once per inbound) and secret. |
The secret of a client is secret and unique within its inbound. For vless it is a UUID. For trojan it is a password of 22 to 128 characters from A-Z a-z 0-9 _ - . + / =. For shadowsocks it is a key of 32 bytes as standard base64 (44 characters).
Engine. The node leaves a user who is disabled, expired or over quota out of what Xray is given, and holds quota and expiry itself. A change of the section, or of the users it names, starts Xray again with a new document after 2 seconds without another change, and connections on it drop for that moment. Without the add-on’s executable, or with no Xray host to ask, the node keeps running and Xray is not started.
Reloading
Section titled “Reloading”Engine. The core reloads on SIGHUP and on an apply through the control API. It does not watch the file.
- The core parses and validates the new document. An error leaves the running config in force and is logged once with the field path.
- Prepare: every new listener is bound, every certificate loaded, every device and firewall change staged, while the old config keeps running. Any failure aborts the reload and the old config stays whole.
- Commit: the new config replaces the old one in one step. The core logs which inbounds, outbounds, exits, rules and users were added, changed or removed, by tag or name.
After a commit:
- A flow belongs to a user and device, not to an address. A device is identified by its public key across reloads.
- Every live flow is checked again. It is reset when its exit is gone, its exit’s backend or outbound changed, its destination no longer passes the gate, its user is no longer entitled to the exit, or the rules now decide differently for it.
- Changing an inbound’s
private_key,addressormturestarts that tunnel and drops its sessions. Changing one transport restarts that transport only. Changing a tag is removing one object and adding another. - Certificates are read again on every reload.
- A proxy inbound that changes closes its listener and its connections. See Proxy inbounds.
- A device’s chosen exit is kept across a reload while it is still in the user’s
exits. One that is not is dropped and logged at info, and the device leaves through the user’s exit. It does not come back when the exit is given back. - A control connection proved one transport secret. When a reload removes that secret from the device, the connection is closed within a second and the secret opens no new one. A
udp-obfssession opened with the removed secret ends at the reload, and an opening packet under that secret is a stranger’s from then on. A session opened with the device’s other secret stays up, and so does a tunnel that came overudp, where no secret opened it. Adding a second secret disturbs nothing. - A
udp-obfstransport that stays as it was keeps its sessions across a reload, also whenlimits.sessionsor the inbound’spaddingchange. New padding bounds apply to sessions opened afterwards. A transport whoselistenchanges, or whose tunnel restarts, loses its sessions, and its devices open new ones. - A
tlstransport whoselistenandpathstay as they were keeps its sessions across a reload, also when itslimits, itsdecoy, its certificate files, or the inbound’supgrade_cookie,upgrade_headersorpaddingchange. The certificate and key files are read again at every reload, also at aSIGHUPon an unchanged config file, and connections made afterwards get the new certificate. A file that cannot be used refuses the reload and the certificate that runs stays. A newpathends the sessions that came in by the old one, which is the origin’s from then on. Removing the transport closes its listener with every connection on it, except connections being relayed to the origin, which stay with it. - A session opened with a
tlsproof ends at the reload that removes the transport secret it was opened with, like audp-obfssession. Adding a key totransport_keysends no session. Removing one ends, within a second, every session opened under it, and a token sealed to it opens nothing from then on. The keys of a session’s chunks come from the key its proof was sealed to, so removing a key is the answer to one that leaked. Keep the old key in the list until every profile carries the new one, then remove and destroy it. Devices still connected under it connect again under the new one. - A
wsorfrontedtransport behind your web server whoselistenandpathstay as they were keeps its sessions across a reload. A session ends with its transport secret or its transport key, like atlsone. A newpathends the sessions that came in by the old one, which is the origin’s from then on. A proxy taken out oftrusted_proxiesis a stranger from its next connection on. Removing the transport ends its sessions. Removing the last one on a listener closes the listener with every connection on it. - The name of a
decoy.originis resolved at start and at every reload, and the header fields of the upgrade answer are asked for again. replay.per_sourcechanges at once.replay.cachechanges at the next start.- An inbound whose
typechanges fromveilwaytowireguardloses its control channel: its connections are closed and the port is refused. The other way round, the port starts to answer. Sessions stay. - Changed
noticesare sent to every open control connection. - A link that keeps its
idand its key keeps its state across a reload. A changedxraysection, or a changedxrayoutbound, starts Xray again, seexrayand “xrayoutbounds”. - DNS sets change without touching a tunnel or a session. You can add a set, remove it, give it other servers, or give it to other inbounds, groups and users. The next query of every user goes to the list the new config gives them.
- A user whose queries go to other servers than before loses the names the node remembered for them, so that no
domainrule keeps matching on the word of a resolver that is no longer theirs. This is logged at info. What counts is the servers of the list the user’s queries go to, looked at for each inbound their devices are on. Moving a user, a group or an inbound into, out of or between sets clears the memory, and so does changing theserversof the set they are on. The same holds fordns.servers: changing it clears the memory of every user who resolves through it, which earlier builds did not do. The same servers in another order, and another set with the same servers, are no change.domainrules match for that user again as their names are resolved again. Open flows are checked like after any reload. - A query that is waiting for a server when the reload happens is answered by that server. Its answer is not remembered when the servers its user resolves through changed in the meantime.
Commands
Section titled “Commands”veilway run --config config.jsonveilway run --config config.json --state veilway-state.jsonveilway run --config config.json --replay /var/lib/veilway/veilway-replayveilway run --config config.json --api /run/veilway/veilway.sockveilway run --config config.json --no-apiveilway run --config config.json --addons /usr/local/lib/veilway/addons --xray-socket /run/veilway-xray/host.sockveilway check --config config.jsonveilway doctor --config config.jsonveilway doctor --config config.json --json --timeout 3veilway usage reset --user aliceveilway usage reset --user alice --totals --api /run/veilway/veilway.sockveilway replay resetveilway replay reset --yesveilway schemaveilway genkeyveilway genkey --transportveilway gensecretveilway link offer --config config.json --tag to-exit --inbound links --name entry-1 --address 10.9.0.2 --transports transports.jsonveilway link answer --config config.json --tag to-exitveilway link remove --config config.json --tag panel --empty --applyveilway mirror --config mirror.jsonveilway mirror --config mirror.json --checkrunrefuses a config file that group or others can read, unless you start it with--allow-readable-config.run --state <file>sets the path of the usage state file. Default:veilway-state.jsonnext to the config file.run --replay <file>sets the path of the replay file. Default:veilway-replaynext to the state file. Only a config with an obfuscated transport uses it, seereplay.run --api <path>sets the unix socket of the control API. Default:veilway.socknext to the config file.run --no-apistarts the node without the API. You cannot combine the two. Whoever can open the socket administers the node, see the control API reference. Both flags exist on Unix only.run --addons <folder>is the folder the Xray add-on is installed in (/usr/local/lib/veilway/addonsby default). It must not be writable by the user the node runs as.run --xray-socket <path>is the socket of the Xray host that starts Xray (/run/veilway-xray/host.sockby default).run --geoip <file>, also oncheckanddoctor, is the country address list thatgeoip:<cc>rule entries read. Default:/usr/local/lib/veilway/geoip.txt.run --xray-dir <folder>is the folder shared with Xray’s user where the sockets ofxrayoutbounds are made (/run/veilway-xray-outby default).- Only a config with an
xraysection or anxrayoutbound uses these three, seexrayand “xrayoutbounds”.veilway xray-hostis that host, which the install script runs as its own service on Linux. checkprints every error and warning and exits with a non-zero status when there is an error. It gives the same answer on any operating system.schemaprints the JSON Schema of this format.genkeyprints a new WireGuard private key and its public key.genkey --transportprints a new node transport key fortransport_keysof one inbound of one node, and its public half, which is what the profiles of that inbound carry. It is an X25519 key of its own and never a WireGuard key.gensecretprints a new transport secret for one device of aveilwayinbound.
doctor tries what a config needs of this host and of the network, once, before you start the node or after it misbehaves. It needs no running node. It never changes the node, the host or any file, and it never prints a secret. A line names tags, user-chosen names, field paths and listen addresses, and no other value of the config.
--config <file>is the config to check (config.jsonby default).--state <file>,--replay <file>,--api <path>and--no-apimean what they mean forrun, with the same defaults. So do--addons <folder>and--xray-dir <folder>, which matter when the config has an Xray inbound or anxrayoutbound.--jsonprints one JSON document with the same lines in place of text.--timeout <seconds>is how long each step on the network may take, 5 by default and 10 at most.
Checks run a few at a time, so the command ends in well under a minute on a network that answers nothing.
link remove takes the link --tag out of this node’s config, which ends it at once. On a node a panel manages, this is how the node leaves the panel. --empty also removes every user and the clients of the Xray inbounds. Without it the users stay and keep working, out of the panel’s reach. It asks for yes first, or not with --yes. --apply (or --apply=SOCKET) changes the node that runs through its control socket. Without it the config file is changed, and the node reads it at its next reload or restart. A link that an exit still names is refused, and nothing is changed. Exit status: 0 removed, 2 not confirmed, 1 anything else
link offer and link answer pair this node with another one, one command on each node. Once both operators have compared the fingerprint, they print the entry for each node’s links. Neither writes the config: you put the entry in yourself. Exit status: 0 paired, 2 not confirmed, 1 anything else.
| Flag | Meaning |
|---|---|
--config <file> |
The config of this node. Default: config.json. |
--tag <tag> |
The tag the link gets in this node’s config. Required. |
--inbound <tag> |
offer only. The veilway inbound the link connects to, which carries no users’ devices. Required. |
--reverse |
offer only. A reverse link: the other node dials and is the exit, and this node has the users. Without it the link is a forward one and this node is the exit. |
--name <label> |
offer only. This operator’s label for the link, which the other operator is shown. Required. |
--address <ip> |
offer only. The address of the node that dials, from the inbound’s pool: one, or an IPv4 and then an IPv6 one. Required. |
--listener-address <ip> |
offer only. In a reverse link, this node’s own link address from the same pool, one for each --address. |
--transports <file> |
offer only. A JSON file with a list of the transports the other node may dial, in this node’s order of preference and in the shape of peer.transports. Required. |
--mtu <n> |
offer only. The link’s mtu, 1280 to 1500. |
--lifetime <seconds> |
offer only. How long the offer is good for, 1 to 3600. Default: 600. |
usage reset starts one user’s quota count over on a running node. It talks to the node through the control socket, so it exists on Unix only and needs a node started without --no-api. It does not read or change the config file or the state file itself.
| Flag | Meaning |
|---|---|
--user <name> |
The user to reset. Required. |
--totals |
Also zero the user’s lifetime totals (up and down, bytes and packets). Without it they are kept. |
--api <path> |
The control socket of the node. Default: veilway.sock next to the config file. |
--config <file> |
Only used to find the default socket path. Default: config.json. |
On success it prints one line with the new used_bytes and period_start. It says so if the user still cannot connect because they are disabled or expired. Errors go to standard error.
| Exit status | Meaning |
|---|---|
| 0 | The usage was reset. |
| 1 | Anything else: the node answered with another error, or did not answer within 30 seconds. |
| 2 | Wrong command line, for example no --user. |
| 3 | The running config has no user of that name. Nothing was changed. |
| 4 | No node answers on the socket: nothing is at the path, or a node that was killed left its socket behind. |
| 5 | This user may not open the socket. Run the command as the user the node runs as, or as root. |
Running it twice is safe: the second run sets the count to 0 again and changes nothing more.
replay reset empties the replay memory of a running node and sets its floor and its limit to the node’s clock plus 60 seconds. Use it for a node whose floor is ahead of its clock, see replay. It talks to the node through the control socket, like usage reset. It never touches the replay file itself, because the node’s next write would undo a file edited under a running node. With the node stopped, deleting the replay file has the same effect. Usage and quotas are not touched.
It costs two things, and the command prints them every time before it does anything:
- Every client is refused for 60 seconds.
- A proof that was accepted with a timestamp above the new floor is forgotten. Whoever recorded it can use it once more when the clock reaches that timestamp. That can only be a proof from the time the node’s clock ran ahead.
| Flag | Meaning |
|---|---|
--yes |
Do it. Without this flag the command prints what a reset costs and changes nothing. |
--api <path> |
The control socket of the node. Default: veilway.sock next to the config file. |
--config <file> |
Only used to find the default socket path. Default: config.json. |
On success it prints one line with the new floor and limit and the node’s clock.
| Exit status | Meaning |
|---|---|
| 0 | The replay memory was reset and its file is written. |
| 1 | Anything else. Either the node could not write the replay file: it then accepts no proof until it has written the floor and the limit it had back, and the reset has to be run again. Or the node did not answer within 30 seconds. |
| 6 | The node has not finished writing the reset after 10 seconds. The reset is in force when the write ends. Do not run the command again: that would cost another 60 seconds. replay.resets of /v1/stats grows by one when it has landed. |
| 2 | --yes was not given, or the command line is wrong. Nothing was changed. |
| 3 | The node keeps no replay memory, because it has run no obfuscated transport. |
| 4 | No node answers on the socket. |
| 5 | This user may not open the socket. |
Each doctor check prints one line: its status, the kind of check, what it looked at, and one sentence on what was found. On warn and fail, the sentence also says what to do about it. The checks run in this order:
- The config file: its mode, and the validation report with every error and warning. A config that does not validate ends the run here, and everything after it is reported as
skipped. - The control socket: whether a node answers on it. If one does, a listen address that is taken and an l3 interface that exists are expected, not failures.
- Every listen address of every inbound: whether it can be bound right now, over UDP or over TCP as the transport needs. Transports that share an address are tried once.
- Every certificate and key of a
wsortlstransport: both load, the key matches the certificate, the certificate is not expired and not yet valid, and the names it covers. Under 14 days left is a warning. - The decoy origin of every
tlstransport, with two requests made the way the node makes its own. The first asks for the origin’s page. An origin that is not reached, or does not answer as a web server, is a failure, because whoever is not a user would then be closed with no answer. An answer that redirects to anhttps://address is a warning: behind the node’s TLS that redirect never ends. A field ofupgrade_headersthat the origin does not send is a warning. The line says whichHostthe request carried. The second asks for the path of the transport with a query that is no proof. The origin has to answer it itself, as it answers a path it does not have. An answer below 400 is a warning, since the origin must serve nothing there. A 5xx answer, or none in time, is a failure: an origin that hands the path on to the node would send every stranger’s request round in a loop. The path is sent to the origin and printed nowhere. That request, with the path and a query of 144 letters, is in the origin’s access log afterwards.doctordoes not measure how long the origin keeps a connection that sends nothing. - The proxy path of every
wsandfrontedlistener: whether anybody but this host can reach it, whether the web server in front speaks HTTP/1.1 to the node, and, from a running node, whether the requests it has read named their source inclient_address_header. For afronted-shapedtransport the line also prints the shape the transport uses, so that you can compare it with the profile. - Every link that is enabled, without dialing anything: a clock that is set, whether an exit with a
chainbackend names each entry half, whether itsmtuis below that of an inbound with users, and, from a running node, each link’s state. - Every DNS server of the config, those of
dns.serversand then those of every DNS set: one query over UDP and the same query over TCP from the node itself, with the answer time. The line of a server of a set names the set, as inset family, servers[0], udp. A server that stands in two lists is asked once for each. Silence over UDP is a failure when some exit leaves directly, and a warning otherwise. Silence over TCP is a warning, because answers too long for one datagram and every lookup through a SOCKS5 proxy need TCP. - Every outbound. An
xrayone is not tried, since its socket exists only while the node runs: its line says so, and thexrayline described below says whether the add-on is installed. Adirectone gets a TCP connect and a UDP exchange to the first DNS server it can reach from the node itself. Asocks5one gets four separate lines: the TCP connect to the proxy, the handshake with the authentication of the config, a CONNECT through the proxy to the first DNS server with a real DNS query, and a UDP ASSOCIATE with a real DNS query through the relay, so a proxy without UDP shows as one clear warning on the last line. The relay must be on the host the control connection reached, as the node requires. These DNS servers are those ofdns.servers. The servers of DNS sets are not tried through the outbounds. When the config has sets, askippedline (sets, through the outbounds) after the DNS lines says so: where a set’s queries leave through a proxy, doctor does not tell whether that proxy reaches the set’s servers. - Every l3 exit (Linux only):
CAP_NET_ADMIN,/dev/net/tun,ipandnfton thePATH, the forwarding sysctls as they are now and what the node would change withmasquerade, and whether the interface name is free. When a running node has the exit and its interface is not there, a warning says the exit is down and being retried. - ICMP ping sockets, when a policy uses
icmp_echo: direct. - The state file: a regular file or absent, a directory the user can write, the mode, and whether it parses.
- The replay file, for a config with an obfuscated transport: a regular file or absent, a directory the user can write, whether it is a replay file, and its floor, its limit and the clock. A floor more than 80 seconds ahead of the clock is a failure, because the node then refuses every device for longer than any start costs. A floor up to 80 seconds ahead is what a start or a reset leaves, and the line says how long it lasts. A limit more than 80 seconds ahead of the clock is a warning: a clock ran ahead, and a node started on that file refuses every device until the clock passes the limit. A file that is not a replay file is a failure. Doctor reads the file and never moves or writes it.
- The control socket path: what is there, and whether its directory can be written.
When the config has an Xray inbound or an xray outbound, doctor also checks the add-on right after the config file, in a line of the kind xray. It checks whether the executable is installed, is the pinned release and belongs to root. For an xray outbound it also checks the folder for the sockets (--xray-dir). It is a warning while the folder is not there yet, since the Xray unit makes it when it runs. It fails when the folder is not a folder, when its mode lets anyone but the owner and the group in or the group write to it (the mode is 750), when it belongs to root, or when the user doctor runs as is not in its group. Run doctor as the node’s user, since root enters any folder. scripts/veilway.sh addon xray install sets the group and the folder up.
The four statuses: ok means the check passed. warn means the node starts and works, but something is likely not what you want. fail means the node would not start or would not work for the thing checked. skipped means the check was not tried because a step before it failed, or because of --no-api. A final line counts the statuses.
The exit status is 0 when nothing failed, 1 when a check failed (a config that reads but does not validate counts), and 2 when the config could not be read at all. Warnings do not change it.
veilway mirror
Section titled “veilway mirror”veilway mirror --config mirror.json runs a relay mirror of a subscription endpoint, see Relay mirrors. --check only validates the file and prints every error and warning. It exits with a non-zero status on an error. The file holds the secret it shares with the panel, so the command refuses a file that group or others can read. The file is JSON, unknown members are errors, and the command reads it once at start. The command exists on Unix only.
| Member | Default | What it does |
|---|---|---|
name |
none | The name the panel lists the mirror under in trusted_mirrors: 1 to 32 characters from a-z, 0-9, - and _. |
listen |
none | The address the mirror listens on, plain HTTP. Use a loopback address and put your web server in front: another address is a warning. |
path |
none | The public path it serves, such as /sub: the path of the link’s endpoint on the mirror’s own domain. 2 to 128 characters from letters, digits and - _ . ~ /, with a leading slash and no trailing slash. Only a POST to exactly this path is forwarded. |
upstream |
none | The panel’s subscription endpoint, an https URL with a path. The path names the endpoint: the panel tells its endpoints apart by path and never by the Host header, so it is the panel’s sub_path, a slash and the endpoint’s name. The chain of its certificate is always validated against the Mozilla roots. A query, a fragment, a user name and a host under .invalid (the template’s placeholder) are refused. |
upstream_pins |
[] |
Up to 4 SHA-256 hashes, in base64, of the SubjectPublicKeyInfo of the upstream’s certificate. When given, the certificate must match one of them as well as validate. Empty is a warning. |
trusted_proxies |
[] |
Up to 16 addresses and networks of your web server, none of them every address. Only they may connect, and only they are believed about the visitor’s address. Empty is for a test with no web server: the mirror then answers requests it will not take with a 404 of its own. |
client_address_header |
X-Forwarded-For |
The header the visitor’s address is read from, as for the ws transport’s client_address_header: 1 to 64 characters from A-Z a-z 0-9 -. |
secret |
none | 32 to 64 random bytes in unpadded base64url, the same as in the panel’s trusted_mirrors. |
The mirror forwards a POST with Content-Type: application/octet-stream and a body of exactly 1,073 bytes, in HTTP/1.0 or HTTP/1.1. That body is a request sealed to the panel’s gateway, which the mirror cannot open. The mirror hands back status 599 for the web server to answer in these cases: anything else, and every request while the upstream fails or answers with anything but a 200 with Content-Type: application/octet-stream and a body of 9,413, 36,979, 140,867, 538,201 or 1,089,437 bytes (a sealed answer), the panel’s own 599 included. A sealed answer is passed on unchanged with that status and content type, and with Cache-Control: no-store. The panel is sent X-Veilway-Mirror: v1;name;address;timestamp;nonce;mac. There, mac is HMAC-SHA-256 under the secret over veilway mirror v1, then the name, address, timestamp (seconds), nonce (16 random bytes) and the SHA-256 of the body, each after a line feed. The nonce, the hash and the mac are in unpadded base64url. The panel accepts it within 60 seconds of its clock and once per nonce.