Skip to content
BoWen Chai's Notes
Go back

OpenWrt Regional Routing with RelayWayX and Tailscale

Edit page

OpenWrt Regional Routing with RelayWayX and Tailscale

This design record covers one deployable stack for this network:

It was last reviewed on 2026-08-21. Re-run the acceptance tests after every OpenWrt, sing-box, Momo, RelayWayX, or firewall upgrade.

Decision

Use OpenWrt-momo in core-only mode with stable sing-box 1.13.x. The raw sing-box profile owns DNS, the TUN, route policy, and nftables interception. Momo owns only profile validation and the OpenWrt procd service lifecycle. Core-only mode deliberately skips Momo’s mixins, tag checks, access control, DNS hijacking, routing, and firewall generation.

The TUN uses auto_route, auto_redirect, strict_route, and the system stack. auto_redirect is the modern nftables path and is documented as faster than conventional TPROXY. Its kernel pre-match path is also what preserves the original balanced-VLAN source on direct CN flows.

This exact profile requires sing-box 1.13 or newer and kmod-nft-queue. As of this review, upstream stable is 1.13.19, the OpenWrt 25.12 package is 1.13.18, and 1.14 is prerelease. Pin the validated 1.13.x build; do not use 1.14-only fields. Install and verify the queue modules before enabling traffic:

lsmod | grep -E 'nfnetlink_queue|nft_queue'
logread | grep -i 'pre-match disabled'

The second command must return nothing. Without these modules, sing-box disables pre-match and a bypass can degrade into a router-originated userspace socket, weakening both performance and VLAN attribution.

See the OpenWrt-momo project, sing-box TUN documentation, upstream releases, and the OpenWrt 25.12 package. Run sing-box check before every activation.

This is the only policy stack in the router namespace. Do not add another transparent proxy, policy-routing manager, or DNS hijacker. The independent firewall4 kill switch remains loaded before, during, and after the core runs.

Exposure Policy

FakeIP is mandatory, but FakeIP alone does not hide a real public address. It is a synthetic destination returned to a client. The site sees a provider source address only when RelayWayX actually carries the request to a remote exit.

The privacy VLAN is the default for managed devices:

flowchart LR
  client[Privacy VLAN client] --> owned{Exact owned local service?}
  owned -->|Yes| local[Kernel bypass to local service]
  owned -->|No| rwx{RelayWayX path healthy and supported?}
  rwx -->|Yes| proxy[Remote RelayWayX exit]
  rwx -->|No| reject[Reject]

No CN-direct exception applies to the privacy VLAN. Every public IPv4 TCP flow uses RelayWayX, and unsupported public traffic is rejected.

The balanced VLAN retains direct CN routing for workloads that need it. Those connections deliberately expose the residential WAN address to their CN destinations. Direct authorization requires both a balanced source and a CN destination IP; a domain label alone is never sufficient.

The proxy provider still sees the home source and transport metadata. Accounts, cookies, GPS, and browser fingerprints can identify a user regardless of the network exit.

Terminology and Topology

A Beijing Tailscale peer relay is not an Internet exit. It relays encrypted tailnet traffic when a direct peer connection is unavailable. Tailscale prefers direct, then peer relay, then DERP. This design neither advertises nor consumes an exit node. See Peer Relays and connection types.

A local RelayWayX SOCKS5 listener is also not proof of remote egress. Deployment is allowed only after a destination and a DNS capture prove that RelayWayX uses a provider exit and remote hostname resolution.

flowchart LR
  clients[Privacy and balanced VLANs] -->|DNS and data| router[OpenWrt R5S<br/>dnsmasq views<br/>Momo core-only and sing-box]

  subgraph x86[x86 host]
    rwx[RelayWayX namespace<br/>unique transit IP<br/>SOCKS5 and egress guard]
    ts[Tailscale namespace<br/>unique transit IP<br/>kernel Tailscale]
  end

  router -->|SOCKS5 on infrastructure VLAN| rwx
  rwx -->|provider tunnel only| exit[RelayWayX remote exit]
  exit --> internet[Public destination]

  tailnet[Tailnet peer using advertised subnet] --> path{Selected tailnet path}
  path -->|Direct| ts
  path -->|Peer relay| peer[Beijing peer relay]
  peer --> ts
  path -->|Fallback| derp[DERP relay]
  derp --> ts
  ts -.->|specific LAN prefixes only| router

RelayWayX and Tailscale use separate namespaces or VMs and separate unique, non-SNATed transit addresses. OpenWrt cannot see a remote process identity; it can enforce only ingress interface, source address, destination, and protocol. Preserve both addresses across the infrastructure VLAN and apply source/interface anti-spoofing. UID or cgroup rules, when useful, belong in the x86 namespace’s own OUTPUT path and do not cross Ethernet.

Bind SOCKS5 to the RelayWayX transit address and allow port 1080 only from the router transit address. Authentication is defense in depth. Routing may bypass interception for that exact endpoint, but firewall4 must still deny managed clients access to it and deny unrelated infrastructure services.

The RelayWayX namespace has its own persistent egress guard. Its physical interface may reach only pinned provider tunnel/control endpoints and narrowly defined bootstrap services. Destination traffic may leave only through the provider tunnel. If no enforceable tunnel interface or bounded provider endpoint set exists, the design is not deployable because a live SOCKS listener could otherwise fall back to the home WAN.

Mandatory FakeIP DNS Views

FakeIP preserves a reliable domain-to-flow association even when domains share addresses or sniffing is unavailable. It also lets the SOCKS request carry the domain to RelayWayX for remote resolution.

Use the typed FakeIP server and persist mappings:

{
  "type": "fakeip",
  "tag": "fakeip",
  "inet4_range": "198.18.0.0/15"
}

Set store_fakeip: true in the cache file and dns.independent_cache: true. The latter prevents a warm real-CN response in the balanced view from being reused in privacy, or a privacy FakeIP response from contaminating balanced. See the official FakeIP server and cache-file documentation.

One dnsmasq forwarding to one loopback listener cannot implement multiple DNS views: after forwarding, sing-box sees dnsmasq’s loopback source rather than the original client VLAN. Run a separate dnsmasq interface/instance per view and forward each to a distinct tagged sing-box DNS listener. The example ports are local-only:

flowchart TD
  B[Balanced clients] --> BD[dnsmasq balanced]
  BD --> BS[sing-box balanced-dns<br/>127.0.0.1:1053]
  BS --> BC{Exact local or positive CN name?}
  BC -->|Yes| BR[Real A and balanced CN AAAA]
  BC -->|Other A| BF[Mandatory IPv4 FakeIP]
  BC -->|Other AAAA| BN[NOERROR with empty answer]

  P[Privacy clients] --> PD[dnsmasq privacy]
  PD --> PS[sing-box privacy-dns<br/>127.0.0.1:1054]
  PS --> PC{Exact local name?}
  PC -->|Yes| PR[Real local answer]
  PC -->|Public A including CN| PF[Mandatory IPv4 FakeIP]
  PC -->|Public AAAA| PN[NOERROR with empty answer]

  I[Infrastructure services] --> ID[dnsmasq infrastructure]
  ID --> IR[Separate real-answer resolver<br/>outside managed policy]

Implement balanced-dns and privacy-dns as loopback-only sing-box direct inbounds that accept both TCP and UDP. Match DNS rules by inbound tag, not by the now-hidden client source. An inbound-tagged hijack-dns route rule must run before every bypass rule; it needs no sniffing and therefore does not stop TUN pre-match. The generic local bypass must never consume these DNS listener flows.

The balanced view may use an exact mainland resolver endpoint for positive CN names. The privacy view has no direct public resolver: public A queries return FakeIP without upstream lookup, and RelayWayX performs eventual domain resolution. Local zones, DHCP names, reverse zones, the router, printers, captive portals, and the two x86 transit names are narrow real-answer exceptions.

Implement empty AAAA as an explicit predefined NOERROR response ordered before the A-to-FakeIP default. This avoids version-dependent NODATA behavior; see the DNS rule actions.

The sing-box 1.13 FakeIP transport handles only A and AAAA. In both managed views, return an explicit empty NOERROR for public HTTPS and SVCB queries so their address hints cannot escape the FakeIP model. Return REFUSED for every other public query type unless it is explicitly allowlisted and resolved through RelayWayX. Exact local and reverse zones continue to use their authoritative real-answer view.

For both managed VLANs:

Client-controlled DoH over 443 cannot be classified perfectly by the router. It can bypass FakeIP mapping, so manage Secure DNS on endpoints. The data-plane default still sends any resulting real foreign IPv4 TCP destination through RelayWayX rather than authorizing direct WAN access.

Tailscale’s 100.100.100.100 Quad100 resolver is device-local. Because Tailscale is not on OpenWrt, never use Quad100 as the router upstream or advertise it to LAN clients. See the Quad100 documentation.

Every client receiving a synthetic answer must return through the same sing-box core and persisted mapping. Infrastructure services intentionally use real DNS. An ordinary device hard-bypassed into that view is unmanaged and may expose the home WAN address; it is not a privacy exception.

IPv6 and Unsupported Transports

The deployed RelayWayX SOCKS path is IPv4 TCP-only. The two managed VLANs therefore have explicit, different IPv6 behavior:

Preserve necessary ICMPv6, neighbor discovery, router advertisements, link-local traffic, and path-MTU discovery. DNS filtering is not enforcement: raw or client-resolved destinations must meet the same firewall rules.

Privacy-public and balanced-non-CN UDP, IPv4 ICMP, and other traffic unsupported by the upstream SOCKS service are rejected, never sent direct. Balanced CN UDP/ICMP remains part of its deliberate direct policy. Rejecting non-CN UDP/443 makes browsers fall back from QUIC to TCP. This document does not enable SOCKS5 UDP based only on a client-side flag; it would require end-to-end UDP ASSOCIATE evidence and a new review of the kill switch.

The effective route order is:

flowchart TD
  flow[Inbound flow] --> dns{Tagged DNS listener?}
  dns -->|Yes| hijack[Hijack to matching DNS view]
  dns -->|No| service{Approved x86 source or exact owned service?}
  service -->|Yes| bypass1[Kernel bypass]
  service -->|No| cn{Balanced source and CN destination IP?}
  cn -->|Yes| bypass2[Kernel bypass to WAN or WAN6]
  cn -->|No| v6{Public IPv6?}
  v6 -->|Yes| reject6[Reject]
  v6 -->|No| icmp{IPv4 ICMP?}
  icmp -->|Yes| rejecti[Reject]
  icmp -->|No| inspect[Sniff and hijack DNS]
  inspect --> udp{UDP?}
  udp -->|Yes| rejectu[Reject]
  udp -->|No, IPv4 TCP| proxy[RelayWayX SOCKS5]

Data-Plane Rules

Use literal reserved transit IPv4 addresses. The representative route order is conceptual; replace every placeholder and validate the complete profile:

{
  "inbounds": [
    {
      "type": "direct",
      "tag": "balanced-dns",
      "listen": "127.0.0.1",
      "listen_port": 1053,
    },
    {
      "type": "direct",
      "tag": "privacy-dns",
      "listen": "127.0.0.1",
      "listen_port": 1054,
    },
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "singtun0",
      "address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"],
      "auto_route": true,
      "auto_redirect": true,
      "strict_route": true,
      "stack": "system",
    },
  ],
  "outbounds": [
    {
      "type": "socks",
      "tag": "rwx",
      "server": "192.168.50.2",
      "server_port": 1080,
      "version": "5",
      "network": "tcp",
      "bind_interface": "<infrastructure-interface>",
    },
    {
      "type": "direct",
      "tag": "direct",
      "bind_interface": "<actual-wan-interface>",
    },
  ],
  "route": {
    "rules": [
      {
        "inbound": ["balanced-dns", "privacy-dns"],
        "action": "hijack-dns",
      },
      {
        "source_ip_cidr": ["<rwx-transit-ip>/32", "<tailscale-transit-ip>/32"],
        "action": "bypass",
        "outbound": "direct",
      },
      {
        "type": "logical",
        "mode": "and",
        "rules": [
          { "ip_cidr": ["<rwx-transit-ip>/32"] },
          { "network": "tcp" },
          { "port": 1080 },
        ],
        "action": "bypass",
        "outbound": "direct",
      },
      {
        "ip_cidr": ["<exact-owned-local-prefixes>"],
        "action": "bypass",
        "outbound": "direct",
      },
      {
        "type": "logical",
        "mode": "and",
        "rules": [
          { "source_ip_cidr": ["<balanced-vlan-prefix>"] },
          { "rule_set": ["geoip-cn"] },
        ],
        "action": "bypass",
        "outbound": "direct",
      },
      { "ip_version": 6, "action": "reject" },
      { "network": "icmp", "action": "reject" },
      { "action": "sniff" },
      { "protocol": "dns", "action": "hijack-dns" },
      { "network": "udp", "action": "reject" },
    ],
    "final": "rwx",
  },
}

The inbound-tagged DNS rule is first and cannot match TUN traffic. All bypass rules remain before sniff. In sing-box 1.13, sniff ends auto-redirect pre-match; putting it first silently loses the kernel fast path. The infrastructure source exception is limited to the two non-SNATed service addresses. The SOCKS destination exception is further limited to its /32, TCP, and port 1080. Firewall4, independently, permits that listener only from the router.

The complete raw profile must also define both managed DNS views, the separate infrastructure real-answer view, rule-set sources, cache file, and listen permissions. Do not place the entire infrastructure VLAN in <exact-owned-local-prefixes>; list only intentionally client-reachable owned services and add exact per-service bypass rules. Never bypass all RFC1918, ULA, or 100.64.0.0/10; an ISP or hostile next hop may use those ranges.

Leave exclude_mptcp disabled so unsupported MPTCP is rejected instead of bypassing. The rwx outbound has no direct fallback. A RelayWayX outage must fail closed.

Independent Firewall4 Kill Switch

Place privacy and balanced clients in separate firewall4 zones. The persistent security policy is not created, populated, or removed by Momo or sing-box:

Balanced CN traffic must remain the original forwarded packet through kernel bypass, so its VLAN identity reaches the fw4 FORWARD chain. If pre-match is unavailable, router OUTPUT does not rescue a userspace direct socket; the connection fails closed. If userspace direct routing ever becomes necessary, it requires a separately marked namespace and a new security review.

The CN firewall sets are fw4-owned objects populated atomically by an independent, pinned-data updater. A missing, expired, or failed update leaves the sets empty and balanced traffic closed. They may use the same reviewed data source as sing-box, but they are not sing-box’s nftables sets.

The Tailscale source exception is necessarily broad because direct peer endpoints change. Constrain it inside the Tailscale namespace: OUTPUT permits only the tailscaled UID/cgroup plus exact DNS and control bootstrap, FORWARD permits only advertised LAN prefixes, and the namespace defaults to reject. OpenWrt’s fw4 destination-zone rules provide the second boundary.

Inspect the rendered inet fw4 chains, not only the UCI declarations. Privacy reject and balanced non-CN reject must precede any general LAN-to-WAN forwarding accept and must be scoped to WAN/WAN6 egress. Sing-tun’s OpenWrt integration installs singtun0 accepts in both fw4 input and forward; the kill switch must not drop TUN-bound traffic. Kernel-bypassed flows remain ordinary forwarding. Load the kill switch before sing-box starts and keep it through reloads and upgrades.

Keep software and hardware flow offload disabled on managed interfaces. Re-enable it only after those interfaces are excluded from flowtables by construction and crash/reload regression tests pass.

Loop-Prevention Invariants

  1. The RelayWayX and Tailscale transit /32 sources bypass transparent capture; no other x86 workload inherits that exception.
  2. Only the router can reach the RelayWayX /32, TCP port 1080, and that flow is never redirected into sing-box again.
  3. Router loopback, WAN ingress, the exact transit endpoints, and proxy-core output are not recaptured.
  4. RelayWayX is configured by literal transit IP, eliminating DNS bootstrap recursion.
  5. Sing-box alone owns interception marks; the per-VLAN dnsmasq instances alone own LAN port 53.
  6. IPv6 has equivalent enforcement or is rejected; there is no parallel unfiltered WAN6.
  7. The fw4 and x86 namespace kill switches remain loaded when a process exits or replaces its own nftables table.
sequenceDiagram
  participant Client as Privacy client
  participant Router as OpenWrt sing-box
  participant RWX as RelayWayX namespace
  participant Tunnel as Provider tunnel
  participant Site as Destination

  Client->>Router: Connect to persisted FakeIP
  Router->>RWX: SOCKS5 request carrying domain
  RWX->>Tunnel: Provider underlay only
  Tunnel->>Site: Connection from remote exit
  Site-->>Client: Reply through the same path

Tailscale Placement

Run kernel-mode Tailscale in its own x86 namespace or VM with /dev/net/tun, the required privileges, and its unique transit address. This is distinct from the RelayWayX namespace, so their routes, marks, DNS, and default-route behavior cannot collide. See Tailscale’s Docker parameters and kernel versus userspace routers.

For LAN subnet advertisement, enable IP forwarding inside the Tailscale namespace, route the LAN prefix through OpenWrt, allow the matching infrastructure-to-LAN flow in fw4, and keep Tailscale’s default subnet-router SNAT initially. Advertise only the specific LAN prefixes; never advertise 0.0.0.0/0 or ::/0, and do not consume an exit node.

OpenWrt’s Tailscale source bypass is broad enough to preserve direct UDP, the Beijing peer relay, and DERP/control fallback; the namespace OUTPUT/FORWARD guard above restricts what may use it. Run no unrelated workload in that namespace. Tailscale restarts must not replace the LAN default route, the sing-box tables, or the RelayWayX namespace policy.

Endpoint Privacy Boundary

For traffic that actually traverses OpenWrt, the current TCP-only policy makes foreign STUN, QUIC, and WebRTC UDP fail instead of leaking directly. It cannot stop a phone switching to cellular, browser code learning a local address, or identity disclosure through accounts, cookies, GPS, and fingerprints.

Manage Secure DNS and WebRTC policy on privacy endpoints. Disabling non-proxied WebRTC UDP behind a transparent gateway commonly disables that UDP; it does not magically relay it through SOCKS and may break calls. Use TURN-only application policy when a peer must not learn an address. Devices that leave the protected Wi-Fi need their own always-on, fail-closed tunnel policy.

Acceptance Tests

Test the full matrix after installation, two cold boots, and every relevant upgrade:

Capture physical WAN and WAN6 with balanced and unmanaged clients quiesced; quiesce or filter the unique Tailscale source as well. For privacy clients, captures may show only approved provider underlay and necessary ISP control traffic, never the client’s destination, public DNS query, or direct STUN server. Inspect nftables, policy rules, and routes separately inside OpenWrt, RelayWayX, and Tailscale namespaces. DNS success alone does not prove the data-plane policy.

Final Recommendation

Deploy one stack: Momo core-only with stable sing-box 1.13.x, TUN auto_route + auto_redirect + strict_route, mandatory persisted IPv4 FakeIP, distinct per-VLAN DNS views, and an independent firewall4 kill switch. Keep all kernel bypass rules before sniffing and require kmod-nft-queue.

Use privacy as the managed-device default: exact owned services are local, every public IPv4 TCP flow exits through RelayWayX, and unsupported traffic fails closed. Retain balanced only for explicit CN-direct workloads that accept residential-IP exposure.

Keep RelayWayX and Tailscale in separate x86 namespaces with separate non-SNATed transit addresses and separate egress policy. The Beijing Tailscale peer relay is never treated as an Internet exit. There is no second proxy core, no direct foreign fallback, and no competing DNS owner.


Edit page
Share this post:

Next Post
Make Zig Avaliable in Jules Env