A set of shell scripts that extend the functionality of routers running Asuswrt-Merlin.
Each script lives in /jffs/scripts and integrates with Merlin's built-in hook system
(services-start, firewall-start, wgclient-start, etc.).
Firmware requirement: these scripts depend on Merlin's custom script framework and will not run on stock ASUS firmware.
- Scripts are provided as-is; review the code before deployment.
- Deployment is straightforward: always upload the
scripts/utilsfolder (it contains the shared libraries) and the files mentioned in the section of the script you want to deploy (e.g.,scripts/services-start,scripts/firewall/ipset_builder.sh). Edit the configs locally before uploading them. - The code and configuration files are extensively commented - it's recommended to review them
to understand how each component works and how to adapt it to your environment. In most cases,
you'll find all the details you need directly inside the script headers or in
config.sh. - Adjust IPs, subnets, filesystem labels, and USB vendor IDs to match your own setup. If you remove a script you don't use, also delete any references to it in related hook or event-handler files.
send_email.sh(used by WAN failover, startup notifications, and ipset builder retry alerts) requires that email is preconfigured inamtmbefore running these scripts.- Most network-related scripts are dual-stack and support both IPv4 and IPv6 where applicable.
- Flash Asuswrt-Merlin to your router.
- Enable SSH (Administration → System → Enable SSH = "LAN only"). Strongly recommended to use key-based auth (ed25519 or RSA) and a non-default port.
- Enable custom scripts/configs (Administration → System → Enable JFFS custom scripts and configs = "Yes").
- Clone the repository to your local machine:
git clone https://github.com/kuchkovsky/asuswrt-merlin-scripts.gitBefore applying these steps, make sure you've reviewed the scripts described in the next sections and adjusted their configuration to fit your own environment, removing the scripts you don't need. Once configured, follow these commands to safely upload and activate them on your router:
# On your machine
cd asuswrt-merlin-scripts
# Backup your router's current /jffs directory
# (recommended before overwriting anything)
scp -O -r admin@<router-ip>:/jffs/ jffs_backup/
# ⚠️ WARNING:
# The next command will overwrite files in /jffs on your router
# with those from your local jffs directory.
# It is strongly recommended to merge configs first:
# - Compare jffs_backup/configs/* with your local jffs/configs/*
# - Compare jffs_backup/scripts/* with your local jffs/scripts/*
# Merge any differences into your local jffs before proceeding.
scp -O -r jffs/* admin@<router-ip>:/jffs/
# On your router (SSH session)
chmod -R a+rx /jffs/scripts
reboot- Automatic USB SSD TRIM
- Inbound WAN Firewall (ipset-based blocking & DoS Protection)
- Tunnel Director (ipset-based policy routing)
- WireGuard Client Port Forwarder
- Dual WAN email notifications & optional LAN host blocking
- IPv6 SLAAC and RA support for SDN networks
- nextdns-cli integration for SDNs & automatic updates
- Static IPv6 routes to LAN hosts
- Router startup notification
- Shared utilities
Prerequisites:
- The USB storage must be formatted with an ext2, ext3, or ext4 filesystem.
Other filesystems are not supported for TRIM by this script.- TRIM must be supported and correctly exposed by both the SSD and its USB bridge/enclosure.
This script will try to work around common limitations (e.g., broken UNMAP, buggy discard handling), but success is not guaranteed. If you run into issues, feel free to contact me via SNBForums or open a GitHub issue for further investigation.
By default, ASUS firmware does not provide a built-in and reliable way to issue TRIM for USB-attached SSDs.
Even though Linux and ext-based filesystems support TRIM, the stock behavior often means that:
fstrimis never run automatically,- many USB-SATA/NVMe bridges expose SSDs with
provisioning_mode=fullorpartial, which blocks UNMAP, - some adapters fail on larger discard operations with kernel errors such as
"Remote I/O error".
In practice, this can result in USB SSDs never being trimmed, leading over time to reduced write
performance, increased write amplification, and unnecessary wear.
The script in this section provides automated, safe TRIM handling for USB SSDs on Asuswrt-Merlin.
-
ssd_trim.sh- automated TRIM handler for USB SSDs.
This script turns TRIM into a regular maintenance task handled entirely on the router side. It:- scans all USB-backed mountpoints under
/tmp/mnt(or a single label if specified), - skips excluded labels, unsupported filesystems, and disks that were permanently disabled due to previous errors,
- enforces
provisioning_mode="unmap"on eligible devices so the kernel can send TRIM/UNMAP commands, - runs
fstrimand detects whether any space was actually reclaimed, - handles problematic USB adapters by:
- retrying with
write_same_max_bytesas a safe upper bound, - caching per-disk working
discard_max_bytesvalues in nvram, - permanently disabling TRIM only for devices that cannot be made to work reliably.
- retrying with
All per-disk decisions are stored under stable nvram keys derived from the device's USB identity, so behavior remains consistent across reboots without manual tuning.
- scans all USB-backed mountpoints under
-
config.sh- configuration forssd_trim.sh.
This file centralizes user-facing settings:- defines filesystem labels to exclude from automatic TRIM via
EXCLUDED_SSD_LABELS, - allows adjustments without editing the main script,
- is sourced by
ssd_trim.shon every run, so changes take effect immediately.
To completely skip a given disk, assign it a label and add that label to
EXCLUDED_SSD_LABELS. - defines filesystem labels to exclude from automatic TRIM via
-
services-start- scheduled trimming via cron.
Theservices-starthook installs a periodic cron job (weekly by default) that runsssd_trim.sh, ensuring that connected USB SSDs are trimmed regularly without manual intervention.
Note:
If the script permanently disabled a disk (for example due to repeated TRIM errors) and you want to re-enable it for testing or debugging, simply remove the corresponding nvram flag:nvram unset ssd_trim_<disk-id>_dmb nvram commitYou can find the exact
<disk-id>in the script log - it appears whenever the device is skipped. For example:ssd_trim: Skipping drive=/dev/sdb (disabled via nvram: ssd_trim_24a9_205a_24092311730058_dmb=0) ...After unsetting the key, the drive will be treated as a fresh candidate on the next run.
Prerequisites:
- Designed for use only when IPv4 port forwarding is enabled or IPv6 services are allowed in the firewall for external access - it has no effect otherwise.
- The WAN interface must receive a public IP directly (no double NAT).
- For IPv6-based blocking to function, your ISP must support IPv6, and it must be enabled in the router's settings.
- Country blocking with GeoLite2 requires a
MAXMIND_LICENSE_KEYand external USB storage; otherwise, IPdeny will be used.- The CIDR aggregation feature requires external USB storage for the mapCIDR binary; otherwise, IP ranges won't be aggregated.
-
wan_firewall.sh- high-performance inbound firewall for Asuswrt-Merlin that applies only to the TCP/UDP ports you forward or allow for external access in the GUI and supports both IPv4 and IPv6. It provides country blocking, malicious IP filtering, spoofed traffic protection, and optional per-IP/per-port DoS rate limiting - all applied before packets hit DNAT, conntrack, or routing, protecting your public services with minimal overhead.By default, it uses FireHOL Level 1 as a baseline against spoofed traffic and common attack sources for minimal false positives. Includes predefined but commented-out rules for stronger protection (FireHOL Level 2/3, AbuseIPDB, IPsum, etc.) that you can enable as needed.
Features ipset aggregation using mapCIDR for maximum performance (handled by the
ipset_builder.shmodule - see details below), and fully customizable rules for the forwarded ports. An optional killswitch prevents your services from being exposed before the initial ipset build/restore completes on system boot.On download or ipset build failure, retries via cron and sends email alerts; on success, cancels retries and sends a resolution notification.
- Bogon traffic drop
- Discards spoofed traffic from reserved/unroutable IPv4 and IPv6 ranges before conntrack, saving CPU and avoiding table pollution.
- Country blocking with high-accuracy GeoLite2
- Uses MaxMind's free GeoLite2 database (weekly updates, multi-source verification) for some of the most accurate country-to-IP mappings available.
- Falls back to IPdeny feeds if you don't set a
MAXMIND_LICENSE_KEY. - Blocks entire countries from reaching your forwarded ports - useful for cutting off high-risk regions entirely.
- Fully supports IPv6 ranges, as well as the IPv4 ones.
- Custom ipsets for malicious source blocking
- Pulls in threat intelligence feeds you choose directly from URLs - e.g. AbuseIPDB, IPsum, DShield, Greensnow. Can also use custom hardcoded IP lists defined directly in the configuration file.
- Supports downloads from any source, including the API key protected ones by allowing you to specify
custom
curlarguments like headers, so you can integrate paid or authenticated feeds without modifying the script. - For IPv4, ships with FireHOL Level 1 enabled as a baseline: curated bogon, abuse, and attack-source list designed for minimal false positives.
- For IPv6, it uses two reputable, continuously maintained sources - Team Cymru fullbogons, which lists all unallocated or reserved IPv6 ranges to block invalid traffic, and Spamhaus DROPv6, which targets well-known abusive networks such as spam, malware, and botnet hosts - together providing a clean, safe IPv6 baseline with minimal risk of overblocking.
- Includes predefined (commented out by default) high-impact feeds for stronger protection:
- FireHOL Level 2 - broader threat coverage, focusing on active attacks from the last ~48 hours (blocklist_de, dshield_1d, greensnow).
- FireHOL Level 3 - even more aggressive, covering up to ~30 days of attack, spyware, and malware activity (bruteforceblocker, ciarmy, dshield_30d, myip, vxvault).
- AbuseIPDB - daily feed of the 10,000 most-reported abusive IPs with 100% confidence scores.
- IPsum - aggregate of ~30 trusted threat feeds; Level 2/3 variants require IPs to appear in at least 2 or 3 sources to reduce false positives.
- bds_atif - public threat intelligence feeds targeting malicious activity.
- cybercrime - curated list of IPs linked to cybercriminal infrastructure.
- etn_aggressive - aggressive IP blacklist maintained by Etnetera and commonly used by Suricata IDS/IPS, listing abusive and scanning hosts.
- Example rules:
# Each set starts with a name ending with a colon blk: https://iplists.firehol.org/files/firehol_level1.netset # Multiple sources can be combined into one set mal: https://example.com/malware_feed.txt https://another-feed.com/list.txt -H "Authorization: Bearer API_KEY" 1.2.3.4/32 # inline CIDR entry # You can define a passlist for highly-trusted sources pss: 4.5.6.7 # your VPS IPv4 11.12.13.14 # your friend's home IPv4 pss6: 2a12:abcd::10 # your VPS IPv6 2a12:abcd::30 # your friend's home IPv6
- Flexible ipset-based WAN Firewall rules
- Defines IPv4/IPv6 filtering behavior using a clear rule format:
mode:ports:protos:set[,set...]:set_excl[,set_excl...][:minutes][:ip_count]
where:mode→pass,allow,block, orlogports→ one or more ports or ranges (80,443,1000-2000)protos→tcp,udp, oranyset→ one or more country or custom ipsetsset_excl→ optional sets to exempt from a matchminutes/ip_count→ optional logging controls (forlogrules only)
- Behavior of modes:
pass→ unconditionally allow trusted sources and bypass all further filtering (including DoS rate limits).allow→ permit traffic only from the listed sets; drop all others for the specified ports/protocols.block→ drop traffic from the listed sets unless explicitly excluded.log→ record matching traffic for analysis without dropping it. Adjustable logging frequency viaminutes(time window) andip_count(max logs per IP). Packets are logged to syslog with theips_(orips6_for IPv6) prefix (ips_80,443_t_blk,ips6_22_t_blk_exc1).
- Features:
- Excludes let you carve out trusted or special-case sets within a broader rule.
- Rule order matters: place broad/global rules (e.g., passlists, global blocks) before more specific matches.
- Example rules:
pass:any:any:pss # unconditionally allow matches from pss block:any:any:blk,cn,kp # drop IPv4 traffic from blocklist, CN or KP block:any:any:blk,cn,kp:exc1,exc2 # same as above, but exempt exc1/2 block:any:any:blk6,cn6,kp6 # drop IPv6 traffic from blocklist, CN or KP allow:80,443:any:rly,us # allow rly/US, drop others on 80/443 allow:1000-2000:tcp:ca # allow CA on TCP 1000-2000 log:123:udp:any # log all NTP with 5 min window, 1 IP log:123:udp:us::2:4 # log US NTP with 2 min window, 4 IPs
- Defines IPv4/IPv6 filtering behavior using a clear rule format:
- Optional per-IP / per-port DoS Protection
- Adds selective throttling against high-rate connection attempts using the
xt_hashlimitmodule. - Defines rate-limiters using a clear rule format:
mode:port:proto:above[:burst][:minutes][:log_count]
where:mode→ limiter mode (per_iporper_port)per_ip→ limits new connection rates per source IP (--hashlimit-mode srcip), ideal against individual abusive hosts.per_port→ limits aggregate new connection rates to a destination port (--hashlimit-mode dstport), useful as a global safety net.
port→ destination portproto→tcporudpabove→ packets-per-second threshold that triggers throttlingburst→ optional number of packets allowed before throttling begins (default: 3)minutes→ optional tracking window (default: 5 minutes)log_count→ optional,per_iponly; max logs per offending IP per window (default: 1)
- Logging:
- Offending packets are logged to syslog with the
dos_(ordos6_for IPv6) prefix (dos_ip_443_t,dos6_port_123_u). - Logging is automatically rate-limited to prevent log floods.
- Offending packets are logged to syslog with the
- Best practice:
- Place
per_iprules before any overlappingper_portrules so attackers are throttled individually before aggregate limits apply.
- Place
- Example rules:
per_ip:123:udp:100 # per_ip on UDP/123, burst defaults to 3 per_ip:123:udp:100:500 # per_ip on UDP/123 with explicit burst=500 per_ip:80:tcp:200:400:10:5 # per_ip with explicit window and per-rule log cap # (5 logs across 10 minutes) per_port:123:udp:3333 # per_port, burst defaults to 3 (log_count ignored)
- Adds selective throttling against high-rate connection attempts using the
- Dynamic targeting & early filtering
- Auto-detects forwarded or publicly allowed ports from the GUI and filters only those - both IPv4 and IPv6.
- All filtering happens in the
rawtable for earliest possible drop.
- Smart rule tracking & idempotence
- Computes hashes of current vs. desired rule sets.
- Applies changes only when needed, avoiding redundant reloads and keeping the firewall stable.
- Blocks entire threat regions at the packet edge - before services see the traffic.
- Reduces attack surface by excluding known malicious IPs and unneeded countries.
- Saves CPU cycles by avoiding unnecessary conntrack entries and DNAT processing.
- Minimizes false positives - rules apply only to your WAN-exposed services.
- Runs on
firewall-startto auto-refresh rules after GUI changes. - If
ipset_builder.shkillswitch is active, both IPv4 and IPv6 WAN rules are inserted afterward, ensuring no unfiltered traffic leaks before ipsets are restored.
- Bogon traffic drop
-
ipset_builder.sh- ipset builder module used bywan_firewall.sh.- Builds per-country IPv4 and IPv6 ipsets from GeoLite2 (preferred) or IPdeny, depending on availability and router configuration.
- Fetches and maintains custom ipsets from inline CIDRs or remote lists (HTTP/HTTPS).
- Bundles FireHOL Level 1 for safe baseline protection; can optionally add Level 2/3, AbuseIPDB, IPsum, and other feeds for stronger security.
- Deduplicates and aggregates with mapCIDR if available (auto-installs if external storage is present).
- Generates combo sets to match multiple sources in a single rule.
- Restores from cached dumps for fast boot; SHA-256 hashes skip rebuilds if custom ipset definitions are unchanged.
- Optional killswitch (
-k):- Temporarily blocks WAN ingress on specified
ports:protospairs during the first ipset initialization phase (e.g., right after boot), before any filtering rules are active. It's automatically removed after the successful build of all ipsets. - Prevents your forwarded ports from being exposed to unfiltered traffic while ipsets are still downloading and building.
- When IPv6 is enabled, the killswitch also applies to IPv6 firewall chains.
portscan be a single number (e.g. 80), a comma-separated list (e.g. 80,443,123), or a dash-style range (e.g. 1000-2000)protoscan betcp,udp, orany(matches both).- Example rules:
80:tcp # HTTP 123:udp # NTP 443:any # HTTPS with HTTP/3 support (both tcp and udp) 20-21:tcp # FTP data/control 53,853:udp # DNS and DoT
- Temporarily blocks WAN ingress on specified
- On build failure, retries via cron and sends email alerts; on success, removes retries and notifies resolution.
- Runs
wan_firewall.shif requested via switches so iptables rules referencing these ipsets go live immediately.
-
config.sh- shared configuration file for bothwan_firewall.shandipset_builder.sh.
Review and adjust it to match your environment before running the scripts. -
fw_shared.sh- internal shared library for the firewall-related scripts. -
services-start- Asuswrt-Merlin hook invoked on boot completion. It triggers the initial ipset build and schedules periodic refresh via cron (daily for country sets and twice daily for custom feeds), then starts WAN Firewall. -
firewall-start- Asuswrt-Merlin hook that reapplies firewall rules after every firewall reload. -
profile.add- Asuswrt-Merlin custom shell profile config. Defines a helper alias so you can editconfig.shand reapply firewall changes instantly by simply typingipw, without rebooting.
- All configuration lives in
config.sh. Review before use - it also contains extended docs for every option. - Change
IPS_BDR_DIRinconfig.shto external storage if possible - required for GeoLite2 country ipset generation and CIDR aggregation, as these features rely on additional binaries that are only bootstrapped when using external storage. Otherwise, the script will fall back to IPdeny feeds and disable CIDR aggregation. - Set
MAXMIND_LICENSE_KEYinconfig.shfor country blocking with GeoLite2; otherwise the script falls back to IPdeny. Theconfig.shfile contains instructions on how to sign up for a free license key. - Define your rule sets in
config.sh- e.g.CUSTOM_IPSETS,IPSET_RULES,DOS_RULES, andKILLSWITCH_RULES. These determine the firewall's behavior. - No Entware dependency - all required external binaries are downloaded and managed automatically by the script.
- This script is designed to operate on inbound traffic only, specifically targeting ports that are publicly
exposed to the WAN. It intentionally does not filter or inspect outbound traffic. The rationale is threefold:
- Limiting filtering to inbound services minimizes the risk of false positives that could disrupt legitimate local or client-initiated connections.
- It improves performance by reducing the number of packets that must be inspected, which is especially important on consumer-grade router hardware.
- There is no compelling reason to apply ipset-based blocking to outbound traffic, as user activity protection on the internet is better handled by higher-level tools like AdGuard or NextDNS, which offer more accurate domain-level filtering, improved privacy protection, and easier management.
- Recommended: if possible, forward only ports ≤ 32767 in the ASUS GUI, avoiding higher port numbers.
Ports ≥ 32768 are commonly used as ephemeral source ports by major operating systems
(Linux, macOS, iOS, Windows, Android). Staying at or below 32767 reduces edge cases where
reply traffic from outbound connections might reuse the same numbers as your forwarded services,
keeping matching in
raw PREROUTINGunambiguous.
Prerequisites:
- At least one VPN client (WireGuard or OpenVPN) must be configured and active in Asuswrt-Merlin.
- NAT must be enabled for the selected VPN clients in the ASUS GUI.
- For OpenVPN, "Redirect Internet traffic through tunnel" must be set to "VPN Director / Guest Network" for the selected client(s).
- Country ipsets with GeoLite2 require a
MAXMIND_LICENSE_KEYand external USB storage; otherwise, IPdeny will be used.
-
tunnel_director.sh- outbound, ipset-based policy routing for Asuswrt-Merlin. It lets you route selected LAN subnets through specific tunnels (WireGuard/OpenVPN) or the WAN based on where traffic is going (countries or custom ipsets).
Perfect for anti-censorship use cases: you can force all traffic to or from entire countries or custom ipsets through a censorship-resistant VPN, or invert the logic with meta ipsetany- send everything through the VPN except selected countries.- Tight integration with
ipset_builder.sh- Reuses the same IPSet Builder script as the WAN Firewall, avoiding code duplication.
- Automatically downloads, normalizes, and builds the required ipsets based on your defined rules.
- Flexible ipset-based rules
- Defines routing behavior using a clear rule format:
table:src[%iface][:src_excl[,src_excl...]]:set[,set...][:set_excl[,set_excl...]]
where:table→ routing table:wgcN(WireGuard client),ovpncN(OpenVPN client), ormain(WAN).src→ LAN source subnet (RFC1918, e.g.,192.168.50.0/24,192.168.50.2/32).iface→ optional; bind the rule to a specific LAN ingress interface (e.g.,br0,br52); defaults tobr0(the main LAN bridge).src_excl→ optional; comma-separated list of LAN subnets to exclude from matching.set→ one or more destination ipset keys (countries or custom lists).- Special keyword
anymatches all destinations (no ipset filtering). - For country codes, the script automatically prefers GeoLite2 extended set if available, falling back to standard country set otherwise.
- Special keyword
set_excl→ optional; comma-separated ipsets to exclude from the match.
- Features:
- Excludes - carve out exceptions at both the source (LAN devices/subnets) and destination (countries/ipsets).
- Meta ipset
any- match all traffic without ipset filtering, enabling simple policies like "send everything through VPN except X" or "only send Y through VPN" - without relying on VPN Director. - Rule order matters - rules are applied sequentially. Place broad/global catch-alls before more specific rules to avoid shadowing.
- Example rules:
# 1. Inclusion policy (send all traffic from the main LAN via VPN, but only # when destinations are in certain ipsets, excluding one LAN device): wgc1:192.168.50.0/24:192.168.50.10/32:us,ubl # -> Route all devices in 192.168.50.0/24 via wgc1, but only # for destinations in the US and in the custom ipset "ubl". # Device 192.168.50.10 is excluded and always routed via WAN. # 2. Exclusion policy (send a specific VLAN device via VPN, except for some countries): wgc1:192.168.52.10/32%br52::any:us,ca # -> Route all traffic from device 192.168.52.10 (on VLAN br52) via wgc1 # for all destinations, but exclude the US and Canada. # Packets to US/CA bypass the tunnel and follow normal WAN routing.
- Defines routing behavior using a clear rule format:
- Country-aware destination matching (extended vs. standard sets)
- When GeoLite2 is available, the script automatically prefers extended country sets, which include both geolocated networks and registered-country ranges. This broader coverage is ideal for tunneling around national firewalls and CDN edge infrastructure.
- If GeoLite2 is not available, the script falls back to standard country sets from IPdeny, which are registration-based only (grouping IP blocks by the country where they are registered with the regional internet registries). Unlike GeoLite2, these sets do not reflect actual geolocation, so they may over- or under-match in cases such as CDNs or hosting providers with globally distributed infrastructure.
- Smart rule tracking & idempotence
- Uses content hashes to detect when the configured rules differ from the currently applied state.
- Automatically validates rule counts (chains,
PREROUTINGjumps, and ip rules) to detect drift. - Rebuilds rules only when necessary, ensuring clean updates without redundant reloads.
- Keeps Tunnel Director stable and predictable by preventing duplicate or half-applied policies.
-
Unblock entire countries/regions at once
- If a country's services are throttled/blocked on your direct WAN, route all traffic destined to that country via
a VPN table (e.g.,
wgc1). This transparently covers websites, apps, CDNs, microservices, and API hosts without per-domain tinkering. - Extended sets (GeoLite2) also include registered-in-country ranges used by global clouds/CDNs, ensuring that you capture not only geolocated IPs but also infrastructure owned by that country. This reduces the risk of missing endpoints that matter for the target region's internet ecosystem.
- If a country's services are throttled/blocked on your direct WAN, route all traffic destined to that country via
a VPN table (e.g.,
-
Granular exceptions & flexible policies
- Keep most traffic on the fast direct WAN, but selectively tunnel specific countries or content categories (e.g., news, social media, messaging) through VPN.
- Or invert the policy: tunnel everything through VPN by default (using
any), then carve out exceptions for trusted countries back to WAN (main). This balances privacy and performance. - Per-source rules let you apply different censorship-bypass strategies per subnet or even per-device.
-
Resilient to domain/IP churn
- Country-based ipsets are automatically downloaded and refreshed by the builder; you don't chase hostnames or play whack-a-mole with changing IPs.
- New endpoints in the target region are picked up automatically on the next update cycle, so policies remain effective even if services move to new CDNs or hosting providers.
- Tight integration with
-
config.sh- shared configuration file for bothtunnel_director.shandipset_builder.sh.
Review and adjust it to match your environment before running the scripts. -
fw_shared.sh- internal shared library for the firewall-related scripts. -
services-start- Asuswrt-Merlin hook invoked on boot completion. It triggers the initial ipset build and schedules periodic refresh via cron (daily for country sets and twice daily for custom feeds), then starts Tunnel Director. -
firewall-start- Asuswrt-Merlin hook that reapplies Tunnel Director rules automatically whenever the firewall restarts. -
profile.add- Asuswrt-Merlin custom shell profile config. Defines a helper alias so you can editconfig.shand reapply tunnel changes instantly by simply typingipt, without rebooting.
Prerequisites:
- Your AllowedIPs in the WireGuard config should include the tunnel subnet (e.g.
10.0.0.0/24) and any optional public subnets, but not your LAN range (192.168.0.0/16). Use this tool to calculate only the AllowedIPs you really need.- Go to VPN → VPN Client → WireGuard → Select client instance → [your instance] → Inbound Firewall, set it to Allow, and click Apply; otherwise, services running on the router will not be accessible to other WireGuard peers.
-
wgc_port_forwarder.sh- the ASUS GUI forwards ports only to the WAN interface. This script adds equivalent port forwarding for the WireGuard client tunnel (wgcX).- Creates two NAT chains:
<WGC_IF>_VSERVER- hooked fromPREROUTINGfor traffic to the router'sWG_CLIENT_IP.<WGC_IF>_VSERVER_RULES- holds interface-agnostic DNAT rules.
- Inserts per-interface jumps (WireGuard + LAN) from
_VSERVERinto_VSERVER_RULESto reduce duplicate rules and keep lookups cheap. The LAN jump makes forwarded ports reachable locally on the WG client IP (e.g.10.0.0.2) from devices on the home LAN - useful for services that reference the WG IP. - Reads a simple mapping list
ext_ports:protos:int_ip[:int_port]and installs DNAT rules.ext_portssupports comma lists and ranges (e.g.80,443,81-85).protosis required:tcp|udp|tcp,udp|any(alias fortcp,udp).- If multiple ports / ranges are used,
int_portmust be omitted (ports are preserved). - Logs each action and warns about unsupported combinations.
- Tracks rule sets by computing hashes, applying changes only when needed. This ensures idempotence and avoids unnecessary firewall reloads.
- Creates two NAT chains:
-
nat-start- Asuswrt-Merlin hook that reapplies the rules after every NAT reload. -
wgc_route.sh- toggles static routes for your WireGuard client onwgcN. ConfigureWG_CLIENT_NUMBERandWG_CLIENT_SUBNETS(one or more CIDRs, e.g.10.0.0.0/24) inside the script so LAN hosts can reply to any tunnel peer. The script is idempotent - routes are only added or removed when needed. -
wgclient-start/wgclient-stop- Asuswrt-Merlin hooks that callwgc_route.shwithaddordel, ensuring routes exist only while the client is up. -
profile.add- Asuswrt-Merlin custom shell profile config. Defines a helper alias so you can edit the configuration and reapply port forwarding changes instantly by simply typingwpf, without rebooting.
Prerequisites / Notes:
- Dual WAN must be enabled in the UI and configured in failover mode with automatic failback.
- Tested only with official Asuswrt Dual WAN in the failover / failback mode, using an Ethernet port as the primary WAN and a USB LTE dongle as the secondary WAN. Third-party Dual WAN implementations were not evaluated.
-
wan_failover.sh- monitors ASUS Dual WAN events and alerts you when the primary ISP connection goes down or is restored.- Waits until the router has been up for at least 3 minutes before sending an email, preventing false alerts immediately after a power outage when the primary link may still be initializing.
- Optionally blocks selected LAN devices from using the backup link (for example, stopping a torrent box from consuming expensive LTE data), and unblocks them when the main link returns.
-
wan-event- Asuswrt-Merlin hook that triggers the script whenever a WAN interface goes up or down. -
firewall-start- Asuswrt-Merlin hook that reapplies the LAN-hosts block if the firewall is restarted while the secondary WAN is active.
Prerequisites:
- SDN is supported only on VLAN-capable router models running Asuswrt-Merlin 3006.* or later.
- Your ISP must delegate a /48, /56, or /60 IPv6 prefix to your router.
If the WAN PD is /64, there are no free /64 subnets available for SDN bridges.- Enable IPv6 globally in the router's Web UI:
Advanced Settings → IPv6
You can select any IPv6 mode except Passthrough.- Disable IPv6 for each SDN in the GUI.
The scripts automatically handle IPv6 addressing and Router Advertisements (RA).
ASUS firmware currently lacks proper support for stateless IPv6 autoconfiguration (SLAAC) on
SDN (Self-Defined Network) bridges. The built-in dnsmasq service
only enables stateful DHCPv6 for SDNs, not stateless autoconfiguration. As a result, when IPv6 is enabled
for SDN interfaces in the GUI, the firmware reconfigures all internal subnets - including the main LAN - from
/64 to /72. This breaks SLAAC entirely, since most client devices require a /64 prefix to self-generate
global IPv6 addresses. Consequently, IPv6 connectivity fails completely for clients that don't support DHCPv6.
When your ISP provides a delegated prefix (PD) (for example, /56), the main LAN correctly receives
a /64 only as long as SDN IPv6 remains disabled. Once enabled, however, the firmware slices the entire PD
into smaller /72 segments, effectively breaking SLAAC on all networks - including the main LAN.
The scripts below work together to restore full IPv6 functionality on all SDN bridges.
They automatically allocate proper /64 subnets from the delegated prefix, enable Router Advertisements (RA)
for SLAAC, and configure a secure firewall policy that allows outbound traffic while maintaining
isolation between SDNs and the router.
Note: the scripts configure IPv6 on all SDN bridges by default. If you want to exclude specific bridges,
upload all required files listed below, then run the sls command on the router (a helper alias)
to list SDN names and their bridges:
kuchkovsky@rt:/tmp/home/root# sls name=Guest br=br54 sdn_idx=3 name=IoT br=br56 sdn_idx=5
Next, edit config.sh and set EXCLUDED_IFACES to skip the selected bridges.
-
sdn_v6_br.sh- assigns stable, per-bridge/64prefixes.- Reads the WAN delegated prefix (PD) and computes unique
/64subnets for each SDN bridge. - Assigns each bridge its own router address (e.g.
<prefix>::1/64). - Keeps subnet numbering stable between reboots or config changes.
- Supports PD sizes
/48,/56, and/60(skips/64since there's nothing to carve). - Example:
WAN PD = 2001:db8:abcd:1000::/56 br0 (idx=0) → 2001:db8:abcd:1000::/64 br54 (idx=1) → 2001:db8:abcd:1001::/64 br56 (idx=2) → 2001:db8:abcd:1002::/64
- Reads the WAN delegated prefix (PD) and computes unique
-
sdn_v6_dnsmasq.sh- enables Router Advertisements and SLAAC.- Automatically appends the required
dnsmasqdirectives for each SDN instance. - Ensures that clients receive IPv6 configuration via SLAAC, not stateful DHCPv6.
- Advertises DNS (RDNSS) via RA, so clients can resolve hostnames automatically.
- Runs automatically through the
dnsmasq-sdn.postconfhook - no manual execution required.
- Automatically appends the required
-
sdn_v6_firewall.sh- enables and secures IPv6 connectivity for SDN bridges.- Establishes full IPv6 forwarding support, allowing SDN clients to reach the internet via the router.
- Permits essential local services - DNS (UDP/53) and DHCPv6 (UDP/547) - for address configuration.
- Blocks direct access to the router's global IPv6 addresses and isolates SDN bridges from each other.
- Prevents unsolicited inbound ICMPv6 (ping) traffic from the Internet to internal networks.
-
sdn_v6_shared.sh- internal helper library shared by all SDN IPv6 scripts. -
config.sh- shared configuration.- Lists excluded bridges (if you want some SDNs to stay IPv4-only).
- The main LAN (
br0) is always excluded automatically. - All scripts respect this configuration file.
-
dhcpc-event- Asuswrt-Merlin system hook invoked on DHCP events. Triggerssdn_v6_br.shwhenever the WAN interface receives a new IPv6 prefix delegation (PD). -
dnsmasq-sdn.postconf- Asuswrt-Merlin hook executed after SDNdnsmasqconfiguration is generated. Callssdn_v6_dnsmasq.shto enable Router Advertisements (RA) and SLAAC on SDN instances. -
firewall-start- Asuswrt-Merlin hook triggered after each firewall reload. Runssdn_v6_firewall.shto reapply IPv6 forwarding and isolation rules for all SDN bridges. -
profile.add- Asuswrt-Merlin custom shell profile config. Defines a helper aliasslsthat lists all SDN networks and their corresponding bridges, allowing you to quickly identify the bridge name you may want to exclude in your configuration.
Prerequisites:
- SDN is supported only on VLAN-capable router models running Asuswrt-Merlin 3006.* or later. Routers without SDN ignore the
dnsmasq-sdn.postconffile.- Install and configure
nextdns-clibefore adding these scripts.
-
dnsmasq-sdn.postconf- extendsnextdns-cliintegration to all SDN (Self-Defined Network) instances, not just the main LAN.
The script directly invokesdnsmasq.postconf, which is generated automatically bynextdns-cli, ensuring that SDN networks also benefit from NextDNS filtering and DoH configuration. -
services-start- Asuswrt-Merlin hook invoked on boot completion that adds a weekly cron entry to update thenextdns-clibinary.
Prerequisites:
- Your ISP must delegate a /48, /56, or /60 IPv6 prefix to your router.
A /64 prefix is insufficient for routing additional subnets to LAN hosts.- Enable IPv6 globally in the router's Web UI:
Advanced Settings → IPv6
You can select any IPv6 mode except Passthrough.
ASUS firmware currently provides no GUI support for IPv6 static routes, making it impossible
to route additional IPv6 subnets (e.g., dedicated /64s) to specific LAN hosts or downstream routers.
This script fills that gap by allowing you to define and automatically maintain static IPv6 routes
using link-local next-hops - the correct and reliable method for on-link IPv6 routing.
It is particularly useful for setups where a device such as a Docker host, hypervisor, or secondary router manages its own downstream subnet. You can carve one or more subnets from your delegated prefix (PD) and route them to that device, allowing it to manage those addresses natively.
-
v6_static_routes.sh- defines and maintains static IPv6 routes to LAN hosts.- Reads rule definitions from the
STATIC_ROUTE_RULESvariable, where each line follows
the formatiface|link_local|static_route.
Example:br0|fe80::1234:56ff:fe78:9abc|2a10:abcd:1234:aa10::/64 - Validates each rule:
- Ensures the interface exists.
- Verifies that the next-hop address is link-local (
fe80::/10).
- Applies routes idempotently:
- Skips routes that already exist with the same parameters.
- Replaces or removes conflicting entries automatically.
- Supports any valid IPv6 prefix size (
/48-/128), though/64is typical for host subnets. - Uses link-local next-hops to avoid dependence on global SLAAC or DHCPv6 addresses.
- Logs all actions, warnings, and errors for full transparency.
- Reads rule definitions from the
-
dhcpc-event- Asuswrt-Merlin system hook invoked on DHCP events. Triggersv6_static_routes.shwhenever the WAN interface receives a new IPv6 prefix delegation (PD).
Tip:
Avoid using the first few subnets from your delegated prefix if you also usesdn_v6_br.shfor SDN networks, since that script assigns lower-indexed subnets automatically and may cause overlap.
services-start- Asuswrt-Merlin hook invoked on boot completion that triggers startup notification email 60 seconds after the router comes online. Serves as an indirect power outage alert: if you receive the message without having rebooted the router yourself, it likely means power was lost and then restored.
Helper scripts shared by other modules.
Purpose - provide common building blocks (a lightweight email notifier, structured logging, per-script locking,
temp files/dirs with auto-cleanup, SHA-256 hashing, comment stripping/normalization, port/protocol validation
& normalization, idempotent iptables helpers, selective WAN-blocking utilities) so feature scripts stay short,
focused, and easy to maintain.
-
send_email.sh- small wrapper used by other scripts (e.g., WAN failover and startup notifications) to send status emails. -
common.shlibraryFunction What it does Typical use-case uuid4Generates a kernel-provided random UUIDv4 string from /proc/sys/kernel/random/uuid.Create unique temp names, job IDs, or correlation IDs in logs. compute_hash [<file>|-]SHA-256 helper: hashes a file (when path is given) or stdin (no arg/ -); prints the 64-char lowercase digest only.Change detection (e.g., ruleset/config hashing) and cache keys; works cleanly in pipelines. get_script_pathReturns the absolute path to the running script, resolving symlinks (falls back to $0if needed).Locate the script itself (e.g., . "$(get_script_path)").get_script_dirReturns the directory containing the current script (absolute, no trailing slash). Locate sibling files (e.g., . "$(get_script_dir)/config.sh").get_script_name [-n]Returns the script's filename; -nstrips the extension.Derive a log tag, lock name, or temp file prefix. log [-l <level>] <msg...>Lightweight syslog wrapper (facility user). Logs to both syslog and stderr; supports priorities:debug|info|notice|warn|err|crit|alert|emerg. Adds readable prefixes for non-default levels.Uniform logging across scripts; easy grepping in /tmp/syslog.log.acquire_lock [<name>]Takes a non-blocking lock under /var/lock/<name>.lock(defaults to script name). Exits early if another instance is running; holds the lock until process exit.Prevent concurrent runs of cron- or hook-driven scripts. tmp_fileCreates a UUID-named temp file in /tmp, tracked for auto-cleanup on exit via trap. Prints the path.Scratch files that should be deleted automatically. tmp_dirCreates a UUID-named temp directory in /tmp, tracked for auto-cleanup on exit via trap. Prints the path.Staging directories for downloads, extracts, or generated assets. is_lan_ip [-6] <ip>Returns 0if the address is private (IPv4 RFC1918 or IPv6 ULA/link-local),1otherwise.Quick guard before applying LAN-only logic or validations. resolve_ip [-6] [-q] [-g] [-a] <host|ip>Resolves a host or literal IP to one or more addresses. Supports IPv4/IPv6 selection, global/public filtering, quiet mode, and returning all matches. Generic resolver for hostnames; used when family/publicity doesn't matter. resolve_lan_ip [-6] [-q] [-a] <host|ip>Like resolve_ip, but requires results to be private/LAN (IPv4 RFC1918, IPv6 ULA/link-local).Safely resolve LAN-only devices (e.g., internal firewall targets). get_ipv6_enabledPrints 1if IPv6 is enabled in NVRAM (ipv6_service!=disabled), else0.Conditional logic for dual-stack scripts (skip IPv6 logic when disabled). get_active_wan_ifReturns the interface name of the currently active WAN ( wanN_primaryflag; falls back towan0_ifname).Pick the correct egress interface for WAN-specific rules or diagnostics. strip_comments [<text>]Trims lines, drops blanks and #comments (including inline), normalizes input. Reads from argument if provided, otherwise stdin; prints cleaned lines.Preprocess multi-line configs (e.g., CUSTOM_IPSETS,IPSET_RULES,DOS_RULES) before parse.is_pos_int <value>Returns success ( 0) if<value>is a positive integer (≥ 1); returns1otherwise.Validate numeric config such as minutes,above,burst,log_count. -
firewall.shlibraryFunction What it does Typical use-case validate_port <N>Validates a single TCP/UDP port: must be an integer between 1and65535. Returns0if valid,1otherwise.Sanity-check user-provided port numbers before using them in iptablesor parsing routines.validate_ports <spec>Validates a port specification: accepts "any", single ports (N), comma lists (N,N2), dash ranges (N-M), or mixed forms (e.g.,80,443,1000-2000). Returns0if valid.Validate destination port fields in rule definitions before constructing CLI args. normalize_protos <spec>Normalizes protocol spec to tcp,udp, ortcp,udp. Acceptsany,tcp,udp,tcp,udp,udp,tcp; prints canonical form, returns0if valid.Convert user configs into canonical proto lists used by multiport rules. fw_chain_exists [-6] <table> <chain>Returns 0if<chain>exists in<table>(iptablesorip6tables), else1.Guard before creating or deleting user-defined chains. create_fw_chain [-6] [-q] [-f] <table> <chain>Ensures a chain exists; with -f, flushes it if present.-6selectsip6tables;-qsuppresses info logs.Initialize or reset per-script chains (e.g., WAN_FILTERING,WGC1_RULES).delete_fw_chain [-6] [-q] <table> <chain>Flushes and deletes a user-defined chain if present. No-op if absent. Clean teardown or script disable/uninstall routines. find_fw_rules [-6] "<table> <chain>" "<pattern>"Lists all rules in <table>/<chain>matching a regex. Prints nothing if none.Inspect active rules to drive higher-level sync/purge logic. purge_fw_rules [-6] [-q] [--count] "<table> <chain>" "<pattern>"Deletes all rules matching <pattern>in<table>/<chain>. With--count, prints number of deletions.Bulk cleanup of outdated jumps or stale variants before reinsertion. ensure_fw_rule [-6] [-q] [--count] <table> <chain> [-I [pos] | -D] <rule...>Idempotent rule manager: append, insert, or delete rules safely. Skips duplicates; --countprints1if a change occurred.Core helper for consistent single-rule management throughout all firewall scripts. sync_fw_rule [-6] [-q] [--count] <table> <chain> "<pattern>" "<desired args>" [insert_pos]Reconciles a rule set to exactly one desired rule: if matches differ, purges all and inserts one canonical rule. Supports IPv6 and positional inserts. Maintain a single authoritative jump (e.g., PREROUTINGjump toWAN_FIREWALL).block_wan_for_host <hostname|ip> [wan_id]Resolves the host to its LAN IPv4 and (if IPv6 enabled) all global IPv6 addresses. Inserts DROP/REJECT rules in filter/FORWARDfor both directions. Defaults towan_id=0.Temporarily isolate a LAN device from a specific WAN (e.g., LTE backup, parental control). allow_wan_for_host <hostname|ip> [wan_id]Resolves LAN IPv4 and global IPv6, removes the corresponding DROP/REJECT rules to restore connectivity. Defaults to wan_id=0.Restore WAN access previously blocked by block_wan_for_host.chg <command ...>Runs a command and returns success ( 0) if its stdout is a non-zero integer - useful with--counthelpers.Gate logic like if chg purge_fw_rules ...; then do_something; fifor clean change detection.
This project is distributed under the GNU General Public License v3.0.