Docs

Choosing what goes through the tunnel

Optional RouterOS recipes for a ForestVPN router — send one group of apps, send everything, and stop traffic leaking when the tunnel is down.

Updated October 8, 2026

Everything on this page is optional. A ForestVPN router tunnels its own traffic the moment it connects, and none of this is needed for that. This is the other job: deciding which of your LAN's traffic leaves through the exit, and what happens when the exit can't be reached.

The division of labour never changes. RouterOS decides what goes to the tunnel, the container tunnels whatever it's handed, and your account decides where it comes out. So all of this is ordinary RouterOS configuration — mangle marks, a routing table, a firewall rule — and none of it touches the container.

Read this first

Three facts decide whether any of it works. Skipping them is a night you don't get back.

Paste these on the box that routes your LAN. RouterOS's prerouting chain only sees traffic the box itself routes. On an access point sitting downstream of your real gateway, every rule below is inert — measured on a cAP ax whose own default route is the router upstream of it: the container tunnelled perfectly and no LAN client's traffic ever went near it. The container can live on that access point; the rules go on the gateway.

The exit admits one source address per device — the overlay lease it issued. A packet arriving with any other inner source is dropped, not routed, however correct your rules are. Something has to rewrite your LAN onto that lease, and which thing depends on the container:

ContainerWhat you have to do about it
suquant/fvpn — the account-managed appNothing. From version 0.3.9 the container masquerades whatever you forward into it onto its own lease, and follows that lease when it moves. The install command turns this on.
suquant/fvpnq — the raw tunnel, from a .conf you wroteAdd a src-nat to the Address in your .conf, which is also what you put in the exit's AllowedIPs. You chose it, so it is stable and a rule can name it.

Running a container older than 0.3.9? Re-run the install command — the rules below will mark and route perfectly and the exit will drop every packet, which reads as the LAN losing the internet outright the moment you add the routing rule.

Fasttrack skips mangle. MikroTik's factory firewall fasttracks established connections, and a fasttracked connection never reaches the mangle chain again — so your marks apply to the first packet of a flow and to nothing after it. Every recipe below carries the one rule that keeps marked connections out of fasttrack, and it has to sit above the fasttrack-connection rule.

And the IPv6 half, which these rules can't do for you

Everything here is IPv4. If your router has working IPv6, every destination you select below is also reachable over v6, around the tunnel, and nothing warns you. If instead the router advertises itself as an IPv6 router without having a v6 default route, LAN clients send v6 at it and get nothing back — not even an error, because RouterOS 7 has no unreachable route for v6, only a silent blackhole. Devices without Happy Eyeballs — TVs, watches, anything doing a bare connect on an AAAA record — then stall for a full connect timeout on every v6 destination, which reads as "the internet is broken sometimes" and blames the Wi-Fi. This one line reports which of the two you're in:

# Does this router tell LAN clients it is their IPv6 router?
/ipv6/nd/print where ra-lifetime!=none
 
# Does it have somewhere to send their v6 traffic?
/ipv6/route/print where dst-address="::/0" and routing-table="main"

Answers to the first and none to the second is the second case. Give IPv6 a real default route, or stop advertising one with /ipv6/nd/set [find] ra-lifetime=none.

Step 1 — the scaffolding

The same for all three recipes. Substitute your own container address for 172.31.254.2 (the default from the install; /interface/veth/print has it) and your own LAN prefix for 192.168.88.0/24.

Check /container/print detail first. remote-image has to say 0.3.9 or later, and the cmd has to contain --nat-lan. Both come from the current install command; a container installed before that has neither, and the symptom is the LAN losing the internet outright the moment you add the routing rule in step 2.

# A routing table of its own, so nothing here can disturb your normal
# default route.
/routing/table add name=via-fvpn fib
 
# Where marked traffic goes: the container, on the address RouterOS itself
# gave it.
/ip/route add dst-address=0.0.0.0/0 gateway=172.31.254.2 \
    routing-table=via-fvpn comment="fvpn: traffic leaves via the container"
 
# Collapse the LAN to one source address on the way IN to the container. The
# exit never sees this one either -- the container masquerades again onto its
# own lease -- so this is about keeping your LAN's layout out of the
# container's connection table, not about being admitted.
/ip/firewall/nat add chain=srcnat out-interface=veth-fvpn action=masquerade \
    comment="fvpn: hide LAN addressing from the container"
 
# The tunnel costs 80 bytes of MTU. Without this, large TCP flows stall
# rather than fail, which presents as "the internet is broken on Tuesdays".
/ip/firewall/mangle add chain=forward action=change-mss new-mss=clamp-to-pmtu \
    protocol=tcp tcp-flags=syn connection-mark=via-fvpn \
    comment="fvpn: clamp MSS on tunnelled TCP"
 
# Fasttrack bypasses mangle, so marked connections have to be accepted
# before they reach it. This rule is inert if it ends up below the fasttrack
# rule -- check with /ip/firewall/filter/print that it did not.
/ip/firewall/filter add chain=forward action=accept connection-mark=via-fvpn \
    place-before=[find action=fasttrack-connection] \
    comment="fvpn: marked flows skip fasttrack"

Then pick one of the next two. They both produce the same via-fvpn connection mark, so they'd fight over the same traffic if you pasted both.

Step 2a — optional: calls and messengers only

The setup that's been running on our own RB3011 for months. It splits the problem the way the traffic actually splits: signalling is a handful of vendor hostnames, media is a port range on relays nobody publishes.

# Signalling, by hostname. A hostname in a RouterOS address list is not a
# static entry -- the router resolves it and keeps the answers fresh, which
# is the only form that survives a CDN moving under you.
/ip/firewall/address-list
add list=fvpn-via address=g.whatsapp.net      comment="fvpn: signalling"
add list=fvpn-via address=media.whatsapp.net  comment="fvpn: signalling"
add list=fvpn-via address=mmg.whatsapp.net    comment="fvpn: signalling"
add list=fvpn-via address=static.whatsapp.net comment="fvpn: signalling"
add list=fvpn-via address=web.whatsapp.com    comment="fvpn: signalling"
add list=fvpn-via address=api.telegram.org    comment="fvpn: signalling"
add list=fvpn-via address=core.telegram.org   comment="fvpn: signalling"
add list=fvpn-via address=web.telegram.org    comment="fvpn: signalling"
add list=fvpn-via address=chat.signal.org     comment="fvpn: signalling"
add list=fvpn-via address=storage.signal.org  comment="fvpn: signalling"
 
/ip/firewall/mangle
add chain=prerouting action=mark-connection new-connection-mark=via-fvpn \
    dst-address-list=fvpn-via \
    comment="fvpn: signalling by resolved vendor hostname"
 
# Media, by port -- the half an address list can never cover, because a call
# lands on whichever TURN relay the vendor picked that minute.
add chain=prerouting action=mark-connection new-connection-mark=via-fvpn \
    protocol=udp dst-port=3478-3481 \
    comment="fvpn: STUN/TURN media - calls in every major app"
add chain=prerouting action=mark-connection new-connection-mark=via-fvpn \
    protocol=tcp dst-port=3478-3481,5349,5350 \
    comment="fvpn: STUN/TURN over TCP and TLS"
add chain=prerouting action=mark-connection new-connection-mark=via-fvpn \
    protocol=udp dst-port=5060,5061 comment="fvpn: SIP"
add chain=prerouting action=mark-connection new-connection-mark=via-fvpn \
    protocol=tcp dst-port=5060,5061 comment="fvpn: SIP over TCP/TLS"
add chain=prerouting action=mark-connection new-connection-mark=via-fvpn \
    protocol=tcp dst-port=5222,5223 comment="fvpn: XMPP signalling"
 
# One routing decision for the whole connection. Marking the connection
# first and the route second -- rather than marking the route directly -- is
# what keeps a call that started on the tunnel on the tunnel.
add chain=prerouting action=mark-routing new-routing-mark=via-fvpn \
    passthrough=no src-address=192.168.88.0/24 connection-mark=via-fvpn \
    comment="fvpn: route marked connections via the exit"

The hostnames are a starting point, not an authority. Vendors add and retire them without telling anyone. Check /ip/firewall/mangle/print stats for counters that stay at zero — that's a rule matching nothing, not a rule doing its job quietly.

Step 2b — optional: everything

/ip/firewall/mangle
# The container's own packets must never be marked, or its tunnel gets
# routed into itself. The install puts the container's interface in the LAN
# list, so the rule below WOULD match it. Substitute the container's
# address.
add chain=prerouting action=accept src-address=172.31.254.2 \
    comment="fvpn: never mark the container's own traffic"
 
# Everything from the LAN that is not addressed to this router.
# `dst-address-type=!local` is what keeps DNS, WebFig and the status page
# off the tunnel; `in-interface-list=LAN` rather than a source prefix keeps
# the rule correct when the LAN is renumbered.
add chain=prerouting action=mark-connection new-connection-mark=via-fvpn \
    in-interface-list=LAN dst-address-type=!local \
    comment="fvpn: everything from the LAN"
# `dst-address-type=!local` on this rule too, and it is not decoration. The
# install puts the container's veth in the LAN list, so replies coming back
# OUT of the container also match `in-interface-list=LAN`, and they still
# carry the connection mark. Mangle prerouting runs before dstnat, so at
# this moment their destination is still the router's own srcnat address --
# local. Without the test they would take the routing mark and be sent back
# into the container instead of on to the client that asked.
add chain=prerouting action=mark-routing new-routing-mark=via-fvpn \
    passthrough=no in-interface-list=LAN dst-address-type=!local \
    connection-mark=via-fvpn \
    comment="fvpn: route marked connections via the exit"

Count what you pasted before you test it. /ip/firewall/mangle/print must show all three of these rules. A terminal paste that drops one of the line continuations leaves you with the marking rules and no routing rule, which looks exactly like "the tunnel is ignored" — the connections carry connection-mark=via-fvpn, nothing ever consults the table, and every packet leaves over your normal connection.

Send everything and you send DNS too — which is the point, but it means the exit's resolver answers your LAN. Check that whatever depends on local names (a printer, a NAS, router.lan) still resolves before you decide this is the mode you want.

Step 3 — optional: a kill switch

Selective routing has a failure mode worth closing: when the tunnel stops, marked traffic can quietly go back to leaving over your normal connection. These three rules make it stop instead. Add them after whichever recipe you chose.

Every command in this section was run on RouterOS 7.24.1 before being written down. The syntax differs from RouterOS 6 in one place, marked below.

# 1. The route to the container counts only while the container answers.
/ip/route/set [find routing-table=via-fvpn dst-address=0.0.0.0/0] \
    check-gateway=ping
 
# 2. When it stops counting, drop rather than fall back. The higher distance
#    keeps this route inactive for as long as the real one is up. `blackhole`
#    is a bare flag here, not `type=blackhole` -- RouterOS 7 has no `type`
#    property on a route at all, and rejects it with "bad parameter type".
/ip/route/add dst-address=0.0.0.0/0 blackhole distance=10 \
    routing-table=via-fvpn comment="fvpn: kill switch - no exit, no traffic"
 
# 3. The leak the first two cannot see: a container that is alive and
#    answering ping while its own tunnel is down forwards to its default
#    gateway, which is you. Traffic wearing the address the srcnat above gave
#    it must never reach the internet directly. Substitute your router's own
#    address on the container's /30 if you changed it.
/ip/firewall/filter/add chain=forward action=drop src-address=172.31.254.1 \
    out-interface-list=WAN \
    place-before=[find comment="fvpn: marked flows skip fasttrack"] \
    comment="fvpn: kill switch - overlay source must not leave over WAN"

Rule 3 has to sit above the fasttrack-exemption rule from step 1, or that rule accepts the traffic first and the drop is never reached. place-before does it here; confirm with /ip/firewall/filter/print.

The stricter version: devices that may only ever use the tunnel

If what you actually want is "this laptop has no internet unless it's tunnelled", say so directly rather than inferring it from routing state:

/ip/firewall/address-list/add list=fvpn-only address=192.168.88.50 \
    comment="fvpn: the work laptop"
/ip/firewall/filter/add chain=forward action=drop src-address-list=fvpn-only \
    out-interface-list=WAN \
    place-before=[find comment="fvpn: marked flows skip fasttrack"] \
    comment="fvpn: kill switch - these devices are tunnel-only"

That device now reaches the internet through the container or not at all, whatever the routing tables happen to say on any given morning.

Checking it worked

# A rule sitting at zero packets is a rule matching nothing.
/ip/firewall/mangle/print stats
/ip/route/print where routing-table=via-fvpn
# The fasttrack-exemption rule has to be above the fasttrack rule.
/ip/firewall/filter/print

From a LAN device, load any "what is my IP" page. It should show the exit's country for the traffic you selected and your own for everything else. If it shows your own for everything, the usual cause is the first fact on this page: the rules are on a box that doesn't route the LAN.

Undoing it

Every line above carries a comment starting fvpn:, so removing them is exact — and won't touch the container's own objects, which the installer comments forestvpn.

/ip/firewall/mangle/remove [find comment~"^fvpn:"]
/ip/firewall/filter/remove [find comment~"^fvpn:"]
/ip/firewall/nat/remove [find comment~"^fvpn:"]
/ip/firewall/address-list/remove [find comment~"^fvpn:"]
/ip/route/remove [find comment~"^fvpn:"]
/ip/address/remove [find comment~"^fvpn:"]
/routing/table/remove [find name="via-fvpn"]

The container keeps running, keeps its account link, and goes back to tunnelling only its own traffic. If something else is wrong, see troubleshooting.

Back to Help

English
Ask us anything