Accounts & delivery

Routing & blocklist

The Routing module has three tabs — blocked ports, custom DNS and outbounds — each scopable to Global default or a single node. The Blocklist module handles domains.

A scope pill at the top of Routing switches between the fleet-wide default and a specific node. An amber dot on a node pill means that node already carries an override. Global saves write the panel setting and fan out to every node; per-node saves store the override locally and then explicitly sync to that node’s agent.

Overrides are additive in spirit, not always in code

Blocked ports and DNS overrides replace the global value for that node. Blocklist domains are the exception: node lists stack on top of the global list.

What it does

Rejects outbound traffic to specific TCP/UDP ports for tunnelled users — the standard way to keep torrenting, spam relaying and IRC abuse off your IP ranges.

How it is enforced

  • Lists are parsed with range support (6881-6889, 1–65535) and stored in firewall.blocked_ports_tcp / firewall.blocked_ports_udp.
  • A dedicated iptables chain GRVPN_PORTBLOCK is created and flushed on each apply, hooked into FORWARD and OUTPUT.
  • Rules REJECT with tcp-reset for TCP and icmp-port-unreachable for UDP, added in chunks of 15 ports because of the iptables multiport limit.

How to use

  1. 1
    Routing → Blocked ports, choose the scope.
  2. 2
    Either click a quick preset — BitTorrent, SMTP (spam), IRC, Napster/P2P — which merges its port list into the current sets, or type ports and ranges manually.
  3. 3
    Save. A global save also pushes the policy to every node.
http
GET /api/routing/ports
PUT /api/routing/ports      {tcp[], udp[]}  or  {tcp_raw, udp_raw}

What it does

Sets the resolvers the VPS itself uses, which is also what tunnelled clients get when they do not carry their own resolver.

PresetServers
cloudflare1.1.1.1 / 1.0.0.1
google8.8.8.8 / 8.8.4.4
quad99.9.9.9 / 149.112.112.112
adguard94.140.14.14 / 94.140.15.15

The UI also offers an OpenDNS chip; server-side presets are the four above.

  • Applying DNS writes /etc/systemd/resolved.conf.d/grvpn.conf and restarts systemd-resolved, falling back to writing /etc/resolv.conf directly when the stub resolver is not in use.
  • Block DoH reuses the blocklist writer to null-route well-known DoH endpoints (cloudflare-dns.com, dns.google, dns.quad9.net, doh.opendns.com, mozilla.cloudflare-dns.com, chrome.cloudflare-dns.com, dns.nextdns.io) via /etc/hosts.
http
GET /api/routing/dns
PUT /api/routing/dns      {primary, secondary, block_doh, preset}

An outbound is a named egress path an account can be pinned to instead of the node’s own IP.

KindWhat it is
directThe implicit default: the server’s native IP.
warpCloudflare WARP over WireGuard. Exactly one profile per node.
xray_customA pasted vless://, vmess://, trojan:// (or ss://) URI parsed into an outbound config.

How to set one up

  1. 1
    Routing → Outbounds.
  2. 2
    Select a node in the scope picker first if you are installing WARP — WARP is node-bound. Leave it on Global default for a custom Xray outbound every node can reference.
  3. 3
    Click Cloudflare WARP → Install (runs the wgcf-based registration on the selected node), or Custom Xray and paste the URI with a name.
  4. 4
    Under Routing policy set the default outbound for the scope, and optionally override per protocol (vmess / vless / trojan / ssh).
  5. 5
    Pin individual accounts from Per-user assignments or from the account detail page’s Settings tab. A pinned account overrides the policy default.
  6. 6
    To rotate a WARP egress IP, use New IP on the existing WARP row — it re-registers the same WireGuard identity for a fresh Cloudflare address.
outbound endpoints
GET    /api/outbounds                     secrets scrubbed from config
POST   /api/outbounds                     {name, kind, config?, uri?}
PATCH  /api/outbounds/{id}                name / config
DELETE /api/outbounds/{id}
POST   /api/outbounds/apply               push config to the fleet
GET    /api/outbounds/policy
PATCH  /api/outbounds/policy              {default, byProtocol:{vmess,vless,trojan,ssh}}
POST   /api/outbounds/{id}/install-warp   node comes from config.nodeId
PATCH  /api/accounts/{id}/outbound        pin one account (null = policy default)

What the node does with them

  • The node builds outbounds[] with direct (freedom) and blocked (blackhole) first, then one entry per policy outbound tagged ob-<id>.
  • WARP kinds become a real Xray wireguard outbound, parsed from the stored wg-quick profile or explicit key/endpoint fields. Cloudflare’s reserved 3-byte field is derived from the wgcf client id when not supplied.
  • If a node has no usable WARP profile yet, the tag silently falls back to direct with a warning rather than breaking the user’s internet.
  • Speed-capped accounts get cloned, mark-tagged outbounds plus routing rules, inserted after API and blocking rules so blocking always wins.
  • ZIVPN has no per-user routing: the plane’s chosen outbound becomes the whole node’s ZIVPN egress.

Deletion cleanup is best-effort

Deleting an outbound removes the row, then in parallel resets pinned accounts to Direct, strips it from the policy, clears per-node overrides and re-syncs. Plans that reference it are not auto-cleaned — check them manually.
  • One WARP identity per node; adding a second to the same node is blocked in the UI. Use New IP instead.
  • The SSH per-protocol outbound policy is stored and visible but not enforced at the OS level — it does not change SSH egress IP.

What it does

Null-routes domains at the hosts/DNS level on the panel host and on nodes, affecting both Xray’s own resolution and any tunnelled client using the VPS resolver. It applies to SSH and Xray traffic alike.

Mechanism

  • Domains live in blocklist.domains (global) and blocklist.node.<node_id> (per-node extra list, additive).
  • A managed block in /etc/hosts between # BEGIN GRVPN BLOCKLIST and # END GRVPN BLOCKLIST is rewritten with 0.0.0.0 domain, 0.0.0.0 www.domain and :: domain.
  • Input is sanitised: lowercased, leading dots stripped, must contain a dot, only a-z 0-9 . - allowed, de-duplicated.

How to use

  1. 1
    Open Blocklist and pick the scope: all nodes (global) or one node chip.
  2. 2
    Paste one or many domains (space or comma separated; scheme and paths are stripped automatically) and press Block.
  3. 3
    Or click a quick preset — Adult 18+, Torrents, Ads/trackers, Social — to bulk-add a curated list to the current scope.
  4. 4
    Remove single domains with the trash icon, or filter the list with the search box.

With a node scope active the UI shows “+N inherited from global”, because node domains stack on top of the global list.

http
GET    /api/blocklist                     {domains[]}
PUT    /api/blocklist                     full replace
POST   /api/blocklist                     append {domain?} or {domains[]}
DELETE /api/blocklist/{domain}
GET    /api/blocklist/node/{node_id}      {nodeId, domains, global}
PUT    /api/blocklist/node/{node_id}      replace that node's extra list

Hosts-level blocking is bypassable

A client with its own hardcoded DoH resolver ignores this entirely. Combine the blocklist with Block DoH in Routing → DNS, and add the target’s ports/ranges to blocked ports for a belt-and-braces block.