Two support tickets that look unrelated turn out to share one cause. In the first, an administrator wants the server to accept connections only from the tunnel; in the second, VPN users should reach a single internal host and nothing more. Both policies belong in FORWARD, and both are usually attempted in INPUT. The result looks like a firewall and behaves like a placeholder.

Which chain answers your question

Packets that a VPN client sends to a LAN host, or onward to the internet, are forwarded by the kernel and evaluated in FORWARD. INPUT and OUTPUT only see traffic that terminates at or originates from the server itself. A reject for anything not arriving on tun0 therefore protects the server own listeners, and it will also end your SSH session unless established flows are admitted first. It changes nothing about where clients are allowed to go.

Name the question first: who may reach the server, or where may clients go. Most networks need both answers, in different chains.

State first, then the permit, then the refusal

An allow-list only works if the specific permission sits above the general refusal, and if return traffic is admitted above both. Insert rules at explicit positions, then dump the chain with iptables -S FORWARD so the order is visible rather than assumed.

POOL=10.99.0.0/24
iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -I FORWARD 2 -s "$POOL" -d 172.31.20.15 -p tcp -m multiport --dports 443,3389 -j ACCEPT
iptables -I FORWARD 3 -s "$POOL" -j REJECT

Two details decide whether this survives contact with real traffic. Without the state rule, replies to the connections you just allowed are themselves evaluated against the refusal and dropped. An unthrottled LOG rule can exhaust a small partition the first time a scan hits it, so add -m limit --limit 10/min before you keep logging. REJECT returns an error immediately, which is friendlier while you tune the list; DROP discards silently, so every mistake presents as a timeout. Pick one on purpose and write the choice in the change record.

Forwarding and NAT must already work

Filtering cannot prove a path exists. Confirm first that the kernel forwards at all, and make that setting persistent rather than a shell one-off. The filter is the last layer, not the first.

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s "$POOL" -o eth0 -j MASQUERADE

Where the masquerade belongs depends on where clients exit. If they leave toward a remote network through the tunnel, the NAT rule goes on that interface rather than the WAN interface. Then watch the counters: if the POSTROUTING counter never moves while the FORWARD accept count rises, the return path is broken and the filter is fine. That reading saves an afternoon of rewriting rules that were never the problem.

The mail block belongs one chain over

Blocking port 25 in INPUT only stops mail addressed to the VPN server. A compromised client sending mail through the tunnel is forwarded, so the block has to sit in FORWARD next to 465 and 587. If one application legitimately relays mail, put its destination and port above that rule.

iptables -I FORWARD 2 -s "$POOL" -p tcp -m multiport --dports 25,465,587 -j REJECT

Let the reject counter run for a week before you tighten anything else. The number that stops climbing is the legitimate flow you just broke.

Read it back, then prove it from a client

Read the ruleset as the kernel reports it, not as your script wrote it. Start with iptables -S FORWARD, then the NAT table with iptables -t nat -L POSTROUTING -n -v, then the conntrack entry for the flow you are probing. Keep a rollback command or a scheduled flush for as long as the work lasts. Every invocation needs -w, or two administrators will collide on the xtables lock. None of that is optional during a change window.

Finish by testing from a client inside the pool and from one outside it. A rule that only works from the machine you typed it on is not a policy.

ХӀокхунах тера йаззамаш