Skip to content

luci-app-passwall2: add ordered priority failover - #1199

Draft
AliZamaniJ wants to merge 4 commits into
Openwrt-Passwall:mainfrom
AliZamaniJ:feature/priority-failover-upstream
Draft

luci-app-passwall2: add ordered priority failover#1199
AliZamaniJ wants to merge 4 commits into
Openwrt-Passwall:mainfrom
AliZamaniJ:feature/priority-failover-upstream

Conversation

@AliZamaniJ

@AliZamaniJ AliZamaniJ commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Why I added this

I use PassWall2 on a low-memory Linksys MX4200 with a shunt setup: Iranian traffic is direct and the default route goes through Xray. I wanted the default route to stay on one node and move to a known backup only when that node was actually unavailable.

The existing balancing mode is useful for distributing or selecting traffic, but it is not ideal for this case. Depending on the strategy, different connections can leave through different public IPs. Some login, payment and anti-abuse systems are sensitive to that. SOCKS Auto Switch also solves a different problem: it manages a separate SOCKS service instead of providing an ordered, reusable Xray routing node for shunt and global traffic.

When my active node failed, both foreign traffic and remote DNS failed with it. Recovery also required more process and service work than I wanted on a small router. I therefore added an opt-in _failover virtual node that keeps the active outbound sticky and changes it only after a confirmed failure or a confirmed primary recovery.

Memory issue I observed

While reproducing prolonged node and remote-DNS failures on a low-memory router, I saw the private DNSMasq instances create many TCP workers. I observed about 26 DNSMasq children, while the private PassWall2 configurations did not inherit the router's intended connection cap. This reduced the available memory headroom during exactly the kind of outage where the router was already doing additional recovery work.

I added dnsmasq_tcp_max_connections for the private Direct and Global instances. 0 keeps it automatic: 8 connections on devices with up to 512 MiB RAM and 20 on larger devices. Existing configurations do not need a migration.

What I changed

  • Added an Xray _failover virtual node with one primary and an explicitly ordered backup list.
  • Limited the list to 10 candidates in total and rejected duplicates and nested virtual nodes.
  • Kept node references by UCI section ID, so renamed nodes keep working and deleted subscription nodes are shown as missing and skipped at runtime.
  • Probed only the active node during normal operation. Backups are checked serially, in user order, only after failure is confirmed.
  • Used a loopback socks5h probe so the check covers the selected outbound and its DNS path together.
  • Added two configurable probe URLs and accepted final HTTP 2xx responses.
  • Added configurable failure count, minimum failure duration, recovery count, recovery interval and minimum backup dwell time.
  • Used monotonic time for intervals so NTP or clock changes do not cause early transitions.
  • Added an optional Direct fallback. It is disabled by default; the default all-down behavior remains blackhole with bounded retry backoff.
  • Used Xray's routing API on loopback and balancer overrides for switching. A switch does not restart Xray, DNSMasq or nftables.
  • Kept runtime state under /tmp and restored the selected override after an Xray restart.
  • Routed remote DNS through the same active failover outbound.
  • Added lifecycle support for Global, Shunt and SOCKS-generated Xray configurations, including cleanup of temporary supervisors.
  • Added WAN route/carrier diagnostics, proxy-aware timing fields and URL redaction for probe logs.
  • Reused the existing PassWall2 process monitor instead of adding another watchdog.

Difference from balancing

Balancing can intentionally spread connections between multiple outbounds. This failover node normally keeps every connection on one outbound, so the public IP stays stable. It changes the IP only after the active node has met the failure threshold, or after the primary has passed the configured recovery checks and dwell time.

Validation

I used a local router smoke harness to cover candidate ordering, the two-cycle failure threshold, all-down behavior, Direct fallback, missing nodes, IPv4/IPv6 and ECMP route detection, URL redaction, monotonic timers, Xray restart recovery and SOCKS lifecycle cleanup. The harness is specific to my test environment, so I did not include the tests/ directory in this PR.

I also ran:

  • Lua syntax parsing for every changed Lua file;
  • sh -n for every changed shell script;
  • git diff --check;
  • forced primary and backup failures on the router;
  • DNS checks for direct Iranian domains and remote foreign domains;
  • repeated Xray/PassWall2 recovery and balancer override checks.

I ran the feature on a Linksys MX4200 v1 with OpenWrt 25.12.5, Xray, three active radios and normal client traffic for more than 30 hours on the same router boot. The test included many real failover and recovery transitions. I observed no new OOM, killed process or router reboot, swap stayed unused, the private DNSMasq instances did not accumulate, and Xray was not killed by memory pressure.

This feature is opt-in and does not change existing balancing, shunt or SOCKS configurations.

@AliZamaniJ
AliZamaniJ marked this pull request as ready for review July 29, 2026 03:26
@lwb1978
lwb1978 marked this pull request as draft August 1, 2026 04:02
@AliZamaniJ

Copy link
Copy Markdown
Contributor Author

Hi @lwb1978,

I noticed this PR was moved back to draft, but I couldn't find any feedback. Is there anything you’d like me to change or test before it’s ready for review?

Also, I can move the dnsmasq change to a separate PR if you prefer.

Thanks.

@handsomeji

Copy link
Copy Markdown
Collaborator

Hello, this feature has a lot of code, currently set it as a draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants