Iptables: Drop wg0 Packets Except UDP 51821 (ACCEPT vs Negate)
Master iptables rules for WireGuard wg0: drop all incoming packets except UDP port 51821. Why ACCEPT+DROP beats single negated rule. Avoid pitfalls like negation logic, rule ordering, connection tracking, INPUT vs FORWARD chains.
iptables: Drop all packets on interface wg0 except UDP destination port 51821 — use ACCEPT+DROP or a single negated rule?
I want to drop all packets arriving on interface wg0 that are not destined for UDP port 51821. One option is:
iptables -A INPUT -i wg0 -p udp --dport 51821 -j ACCEPT iptables -A INPUT -i wg0 -j DROP
The Ubuntu iptables wiki suggests that ACCEPT stops further processing, so I’m concerned that explicitly ACCEPTing might interfere with other rules. Alternatively, iptables supports negation, so I considered:
iptables -A INPUT -i wg0 ! -p udp ! --dport 51821 -j DROP
Would the single negated rule work as intended (dropping everything on wg0 except UDP dst port 51821)? Is it preferable to the two-rule ACCEPT-then-DROP approach? What pitfalls (rule ordering, connection tracking/ESTABLISHED matches, protocol matching, unintended interactions with other chains/rules, performance) should I watch out for?
Use ACCEPT+DROP: explicitly ACCEPT UDP destination port 51821 on interface wg0, allow established/related state, then DROP the rest. The single negated rule (! -p udp ! --dport 51821) does not express “not (UDP and dport 51821)” because ! negates only the immediate match token — so UDP on other ports will not be caught. Place the ESTABLISHED,RELATED rule first, pick the correct chain (INPUT vs FORWARD), and prefer simple, auditable rules (or switch to nftables if you need one-line boolean logic).
Contents
- Recommended iptables rules for WireGuard (wg0)
- Why the single negated iptables rule fails
- Rule ordering, connection tracking and chain selection
- nftables alternative (single-rule expressiveness)
- Performance, maintainability, testing and persistence
- Sources
- Conclusion
Recommended iptables rules for WireGuard (wg0)
Short version: accept what you want, allow connection-tracking, then drop the rest. That is clear, fast enough, and easy to audit.
Recommended minimal rules (packets destined to the local host on wg0):
# allow established/related first
iptables -I INPUT 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# allow loopback (usual best practice)
iptables -I INPUT 2 -i lo -j ACCEPT
# allow only UDP dst port 51821 arriving on wg0
iptables -I INPUT 3 -i wg0 -p udp --dport 51821 -m comment --comment "Allow WG on 51821" -j ACCEPT
# drop everything else coming in on wg0
iptables -I INPUT 4 -i wg0 -j DROP
Notes:
- Use -I (insert) so these rules land where you intend; order matters. If you append with -A you may accidentally place DROP before other broader rules.
- If your system forwards traffic that arrives on wg0 (i.e., packets are routed through this host), apply analogous rules to the FORWARD chain instead of (or as well as) INPUT.
- If your host already has a default DROP policy on INPUT, you may not need the final explicit DROP, but an explicit DROP is often clearer and easier to audit.
- Annotate rules with
-m comment --comment "..."so future you (or teammates) know intent.
If you want to avoid accidental lockout when applying firewall changes remotely, test interactively (see testing section).
Why the single negated iptables rule fails
You asked whether this would work:
iptables -A INPUT -i wg0 ! -p udp ! --dport 51821 -j DROP
That single-rule approach is incorrect for your intent. In iptables the ! (negation) operator applies only to the next option/token. So ! -p udp means “protocol is not UDP”. If that token fails (i.e., for UDP packets), the whole rule doesn’t match — and thus UDP packets on other destination ports will not be dropped by that rule. In short, the expression becomes an AND of two negations (not-UDP AND not-dst-port-51821), which is not the logical NOT of (UDP and dst port 51821). The procustodibus writeup explains this pitfall very clearly: https://www.procustodibus.com/blog/2021/04/wireguard-access-control-with-iptables/. The Superuser discussion about iptables negation also covers the “negation affects the immediate part only” behavior: https://superuser.com/questions/42525745/iptables-negation-which-parts-does-it-affect.
You could write two DROP rules (one to drop non-UDP, one to drop UDP but not on 51821), but that becomes equivalent to the two-rule ACCEPT+DROP approach and is harder to read. For concise boolean logic (not (udp and dport 51821)) use nftables instead (see below).
Rule ordering, connection tracking and chain selection
Why ordering matters
- iptables evaluates rules in order; when a match occurs and the target is ACCEPT or DROP, processing in that chain stops for the packet. So put the rules that must match first (ESTABLISHED,RELATED, loopback), then your allow rule, then the catch-all DROP for wg0.
- If an earlier rule (elsewhere) already ACCEPTs the packet, your later DROP won’t run. Check the entire policy and rule set (not just the two lines you plan).
Connection tracking (UDP is stateful-ish here)
- UDP is connectionless, but the kernel’s conntrack still tracks pseudo-connections for replies. Allowing
ESTABLISHED,RELATEDbefore DROP prevents return traffic from being blocked and avoids breaking existing sessions:-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT. - If you drop INVALID packets early (
-m conntrack --ctstate INVALID -j DROP), you reduce noise and edge cases.
INPUT vs FORWARD
- Packets to local processes hit the INPUT chain. Packets being routed through the host hit FORWARD. Decide which chain(s) to protect depending on whether wg0 is used for peer-to-host connections or routed subnets.
- Example: if WireGuard peers connect to services on the host (ssh, apps), use INPUT. If wg0 carries routed client traffic that should be forwarded to LAN, use FORWARD.
Other interactions to watch
- Firewalld/ufw/netfilter-persistent wrappers — if you’re running a firewall manager, changing raw iptables rules might be overwritten. Use the distro’s recommended tooling or add persistent rules.
- NAT: If you use MASQUERADE or DNAT, remember NAT hooks run in different tables (PREROUTING/POSTROUTING). Your filter rules won’t magically solve NAT issues.
See an iptables primer for how common patterns use explicit ACCEPT then implicit/explicit DROP: https://www.digitalocean.com/community/tutorials/iptables-essentials-common-firewall-rules-and-commands and a practical WireGuard firewall walkthrough at nixCraft: https://www.cyberciti.biz/faq/how-to-set-up-wireguard-firewall-rules-in-linux/.
nftables alternative (single-rule expressiveness)
If your goal is a single-rule expression like “drop everything on wg0 except UDP dport 51821”, nftables supports boolean grouping and not on compound expressions. Example (requires creating table/chain first if not present):
# table/chain setup (example)
nft add table inet filter
nft 'add chain inet filter input { type filter hook input priority 0; }'
# single-rule logic: drop packets on wg0 that are NOT (udp && dport 51821)
nft add rule inet filter input iifname "wg0" not (ip protocol udp and udp dport 51821) drop
That line implements the logical not (udp && dport 51821) correctly in one place. If you’re comfortable migrating to nftables (the modern netfilter API), the rule is compact and unambiguous.
Performance, maintainability, testing and persistence
Performance
- Two simple iptables rules vs one theoretically-complex rule: the CPU difference is negligible for typical traffic. The real win is readability and correctness.
- Keep rules simple and commented; it’s easier to audit and less error-prone under load.
Maintainability
- ACCEPT-then-DROP is explicit and easy for future admins to understand.
- Use comments and consistent ordering (ESTABLISHED, loopback, allowed services, catch-all drops).
Testing tips
- View rules and counters:
iptables -nvL --line-numbers - Check a specific rule exists:
iptables -C INPUT -i wg0 -p udp --dport 51821 -j ACCEPT(returns 0 on match). - Watch traffic while testing:
tcpdump -i wg0 -n udp port 51821 or 'not udp'(adjust filter for your tests). - Add a temporary LOG rule (rate-limited) above DROP to debug dropped packets:
iptables -I INPUT 4 -i wg0 -m limit --limit 5/min -j LOG --log-prefix "wg0-drop: " --log-level 4
Then remove the LOG rule when done.
Persistence
- Save rules with
iptables-save > /etc/iptables/rules.v4and restore withiptables-restore, or useiptables-persistent/netfilter-persistenton Debian/Ubuntu. Confirm your distro’s recommended method.
Caveat: If you’re editing rules remotely (SSH), avoid applying a DROP that could cut your SSH access — test from a second session or use configuration tools that rollback automatically.
Sources
- WireGuard Access Control With Iptables
- iptables rule for filtering WireGuard packets - Super User
- How To Set Up WireGuard Firewall Rules in Linux - nixCraft
- iptables, order of rules - do I understand it right? - Ask Ubuntu
- Iptables Essentials: Common Firewall Rules and Commands - DigitalOcean
- port - iptables negation: which parts does it affect? - Stack Overflow / Superuser
Conclusion
Use the ACCEPT+DROP pattern for iptables on wg0: explicitly ACCEPT UDP destination port 51821 (after ESTABLISHED,RELATED and loopback rules) and then DROP everything else arriving on wg0. The single negated iptables rule you suggested does not express the intended logic because ! negates only the next token; for a one-line boolean “not (udp and dport 51821)” use nftables instead. Order, connection-tracking, and choosing INPUT vs FORWARD are the common pitfalls — test, comment, and persist your rules so you don’t accidentally block legitimate traffic.