You followed the checklist: deny outgoing, allow the tunnel, enable ufw. Then the client waits at the handshake and never connects, or it connects and no page loads. Both failures come from the same gap. The rule that lets the tunnel exist is missing, because the tunnel is not an interface your packets leave from.

Why an allow on tun0 matches nothing at first

tun0 is created by the client after a session exists. The datagrams that build that session are ordinary UDP packets addressed to the provider on port 1194, and they leave through wlan0 or eth0 with the physical source address. allow out on tun0 matches only traffic whose outgoing interface is tun0, so during the handshake it matches nothing at all.

Put default deny outgoing on top of that and the outer packets are dropped before the interface ever appears. The client retries, times out, and reports a TLS or handshake failure, which sends people hunting through certificates and MTU while the firewall quietly eats the connection. The rule meant to contain your traffic is stopping that traffic from being contained.

The reflex fix makes the leak permanent. sudo ufw allow out on wlan0, or flipping the default back to allow, brings the tunnel up and leaves the machine with no kill switch: the next time the tunnel drops, everything resumes over the physical adapter. I lost an afternoon to exactly that pair of edits before I worked out which packet I was actually filtering.

What the two sets of packets need

Your traffic crosses the firewall twice, in two shapes. The inner packet runs from the tunnel address out through tun0. The outer packet runs from the physical address to the peer, and it is the only thing visible on the wire. One rule per shape:

sudo ufw allow out on tun0
sudo ufw allow out to 203.0.113.10 port 1194 proto udp
sudo ufw allow out to 203.0.113.10 port 443 proto tcp comment 'tcp fallback'

Replace the address and port with the ones from your own profile. WireGuard peers usually answer on 51820/udp; OpenVPN profiles that ride TCP 443 need that port instead. The details differ, the shape does not: both rules are narrow, and together they define the only destinations the machine can reach while the tunnel is down.

Order is the second myth. With raw iptables you read the chain top to bottom and reason about which rule wins. ufw is not a thin wrapper around the list you typed; it compiles your rules into its own chains and picks the insertion points. ufw status prints them in ufw's order, not yours. When a rule looks ignored, compare ufw show added with the compiled set from iptables -S | grep ufw before rewriting anything.

The exception has to stay one address wide

A peer rule is only correct while it names the address the client dials. Providers rotate endpoints, and a profile that resolves a hostname will follow a DNS change to a new address while the rule keeps pointing at the old one. When a tunnel that worked for months stops coming up, compare the address in the rule with the one the client is reaching for before widening anything.

Widening to a whole subnet, or to every destination on port 1194, turns the policy back into a firewall with a hole. The exception is the kill switch; once it is broad, there is nothing left to switch. Keep it to a single address and leave a comment naming the profile and provider it belongs to, because in six months the address will look arbitrary.

Name resolution deserves its own line. The lookup happens before the tunnel exists, over the physical interface, so a strict deny-outgoing policy can leave the client unable to find its own server. Allow port 53 to your resolver's address, or resolve once and pin the address in the profile. Allowing DNS on every interface is a decision, not a default.

Build the policy before the tunnel is needed

ufw enable applies the whole policy at once and reloads its chains, and changing a default policy on a running firewall reloads them too. The ufw manual warns that this can drop existing connections such as SSH. So the commands go in this order, with the SSH rule before the enable and a console session open:

sudo ufw default deny incoming
sudo ufw default deny outgoing
sudo ufw allow out on tun0
sudo ufw allow out to 203.0.113.10 port 1194 proto udp
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose

ufw reset is left out of that block on purpose. It deletes every rule and disables the firewall, and on a remote host the following enable is what drops your session. If you do reset, add the SSH rule and confirm a second way back in through a console before going further.

The sequencing is not cosmetic. Rules added while ufw is running do not flush anything, but the moment you flip a default policy the chains reload, and a session that the old policy permitted goes with them. Do the defaults, the allows and the enable in one pass, while you still have a way in that does not depend on the machine.

Test the blocked state, and keep the undo

An untested kill switch is a guess. Disconnect the client on purpose and try to load a page: it must fail. Reconnect and it must recover without touching a rule. sudo ufw status verbose confirms the defaults and the logging state, and ufw status numbered shows which test rule to delete when it outlives its purpose.

Keep the reverse script beside the forward one. A machine with deny-outgoing and a dead tunnel cannot download its own repair, so the undo has to sit on disk and be readable from a console:

sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

Run it when the tunnel is already down and the rules have to come off, not as a routine step. Before you close the terminal, four things should hold: the peer rule names exactly one address, DNS has a named exception, the enable came after the allows, and pulling the tunnel kills ordinary traffic without killing the client for good.

Barruulee wal qabatan