Skip to content

All articles

Route-based IPsec on MikroTik: xfrm interfaces in RouterOS 7.25

Hub-and-spoke IPsec without a policy per subnet: the tunnel becomes an interface, routes come from BGP, and BFD catches failures.

· MikroTik, RouterOS 7.25, IPsec, BGP

Before RouterOS 7.25, IPsec on MikroTik was policy-based only: policies with subnet pairs decided what got encrypted. Every new network behind a tunnel meant a new policy on both ends plus NAT and fasttrack exceptions, and dynamic routing over plain IPsec was impossible — you had to wrap GRE or IPIP inside it.

7.25 introduces xfrm interfaces — the same mechanism as in the Linux kernel. An IPsec policy is bound to an interface, and the ordinary routing table decides what goes into it. The tunnel behaves like any other link: it has an address, routes point through it, BGP, OSPF and BFD run over it, and firewall rules can reference it.

Before you start: firewall for IPsec

The basics first. IPsec is not a single port but three kinds of traffic, and each must be explicitly allowed in the input chain on WAN:

  • UDP 500 — IKE. The peers agree on algorithms, check the PSK or certificates and create SAs here. Without it the tunnel never starts: the log shows retries and timeouts.
  • UDP 4500 — NAT-T. If IKE detects NAT between the peers, the rest of IKE and all ESP travel inside UDP 4500. For spokes behind PPPoE and LTE carrier NAT this is the main port.
  • ESP — IP protocol 50 (protocol=ipsec-esp), not a TCP or UDP port. It carries the encrypted packets themselves when there is no NAT between the peers. A forgotten ESP rule is the classic “IKE is up, SAs exist, but no traffic flows” case.

These rules must sit above the stock drop of everything from WAN — hence place-before in the examples (adjust the number to your rule list or just drag the rules in WinBox). Spokes need them too: the hub may start a rekey or DPD on its own, and such packets do not always match established,related.

hub — IPsec firewall
# Spokes come from any address, so no src-address
/ip firewall filter
add chain=input action=accept protocol=udp dst-port=500,4500 \
    in-interface-list=WAN comment="IPsec: IKE, NAT-T" place-before=0
add chain=input action=accept protocol=ipsec-esp \
    in-interface-list=WAN comment="IPsec: ESP" place-before=1
branch-a — IPsec firewall
# The spoke accepts IPsec from the hub only
/ip firewall filter
add chain=input action=accept protocol=udp dst-port=500,4500 \
    src-address=203.0.113.10 in-interface-list=WAN \
    comment="IPsec: IKE, NAT-T from hub" place-before=0
add chain=input action=accept protocol=ipsec-esp \
    src-address=203.0.113.10 in-interface-list=WAN \
    comment="IPsec: ESP from hub" place-before=1

Here and below, 203.0.113.10 is the hub's public address. Check yourself right away: after the first connection attempt these rules' counters must grow. If UDP 500 grows while ESP and UDP 4500 stay at zero, look for NAT between the peers or something dropping packets earlier in the chain.

Counter check
# Packet and byte counters of the IPsec rules, on the hub and on the spoke
/ip firewall filter print stats where comment~"IPsec"

Topology

Below is a trimmed version of a production setup: a hub on a cloud CHR with a public address, and branches on an RB952 (PPPoE) and an SXT LTE. Both branches sit behind carrier NAT on dynamic addresses, so the spokes always bring the tunnel up and the hub only responds.

Topology
       ┌──────────────────────────────┐
       │ hub (CHR, 203.0.113.10)      │
       │ AS 65000, 10.255.0.1         │
       │ xfrm-branch-a  xfrm-branch-b │
       └───────┬──────────────┬───────┘
      IKEv2/ESP│              │IKEv2/ESP
      (NAT-T)  │              │ (NAT-T)
┌──────────────┴───┐      ┌───┴──────────────┐
│ branch-a (PPPoE) │      │ branch-b (LTE)   │
│ AS 65101         │      │ AS 65102         │
│ 10.255.0.11      │      │ 10.255.0.12      │
│ LAN 192.168.10/24│      │ LAN 192.168.20/24│
└──────────────────┘      └──────────────────┘
  • 10.255.0.0/24 is the transit network inside the tunnels. Its addresses also serve as the spokes' IKE IDs.
  • Each spoke gets its own xfrm interface on the hub: separate BFD, separate counters and per-branch firewall rules.
  • eBGP distributes routes between sites: the hub is AS 65000, each spoke has its own private AS.

Hub: IKE, xfrm and templates

Profile and proposal are the usual ones. The proposal is chosen per spoke, in the template of its group. aes-256-gcm runs fine on the CHR, routers with hardware AES-CTR acceleration get aes-256-ctr with SHA256, and the rest stay on aes-128-cbc. Check which algorithms your model accelerates in the Hardware acceleration table of the MikroTik documentation. There is a single passive peer on the hub with no address — it accepts connections from any spoke.

hub — IKE and proposals
/ip ipsec profile
add name=ike2 dh-group=modp2048 enc-algorithm=aes-256 hash-algorithm=sha256 \
    dpd-interval=2m dpd-maximum-failures=5

# Proposal per spoke hardware: GCM for CHR, CTR for hardware AES-CTR, CBC for the rest
/ip ipsec proposal
add name=esp-gcm auth-algorithms="" enc-algorithms=aes-256-gcm pfs-group=modp2048
add name=esp-ctr auth-algorithms=sha256 enc-algorithms=aes-256-ctr pfs-group=modp2048
add name=esp-cbc enc-algorithms=aes-128-cbc pfs-group=modp2048

# A single passive peer for all spokes: no address, the hub only responds
/ip ipsec peer
add name=branches exchange-mode=ike2 local-address=203.0.113.10 passive=yes \
    profile=ike2 send-initial-contact=no

Now the key part. For each spoke, create an xfrm interface and a policy group with interface=. Put a template without addresses into the group — that is, 0.0.0.0/0 ↔ 0.0.0.0/0. In a policy-based setup such a template would capture all of the router's traffic; here it only applies to packets that routing has sent into this interface.

The identity links a spoke to its group. Since spokes arrive from unpredictable NATed addresses, we tell them apart by remote-id. The spoke's public IP does not matter — the ID it presents during IKE negotiation does.

hub — xfrm, groups, templates, identities
/interface xfrm
add name=xfrm-branch-a mtu=1400
add name=xfrm-branch-b mtu=1400

/interface list
add name=ipsec
/interface list member
add interface=xfrm-branch-a list=ipsec
add interface=xfrm-branch-b list=ipsec

# Policy group = binding to an xfrm interface
/ip ipsec policy group
add name=branch-a interface=xfrm-branch-a
add name=branch-b interface=xfrm-branch-b

# Templates without addresses: 0.0.0.0/0 <-> 0.0.0.0/0 inside their own interface
/ip ipsec policy
add group=branch-a proposal=esp-cbc template=yes comment=branch-a
add group=branch-b proposal=esp-cbc template=yes comment=branch-b

# Spokes are matched by IKE ID, not by IP: they sit behind NAT on dynamic addresses
/ip ipsec identity
add peer=branches remote-id=address:10.255.0.11 secret="<PSK-branch-a>" \
    generate-policy=port-strict policy-template-group=branch-a comment=branch-a
add peer=branches remote-id=address:10.255.0.12 secret="<PSK-branch-b>" \
    generate-policy=port-strict policy-template-group=branch-b comment=branch-b

# The same hub address on every interface, point-to-point to each spoke
/ip address
add address=10.255.0.1 network=10.255.0.11 interface=xfrm-branch-a
add address=10.255.0.1 network=10.255.0.12 interface=xfrm-branch-b

Note the addressing: every xfrm interface on the hub carries the same 10.255.0.1, but with the specific spoke's network=. The result is a set of point-to-point links: the route to 10.255.0.11/32 points into xfrm-branch-a, the one to 10.255.0.12/32 into xfrm-branch-b.

Spoke

The spoke has the same pieces, just one of each. The policy is explicit with peer=hub, so the spoke initiates. The selector is again 0.0.0.0/0 ↔ 0.0.0.0/0, but thanks to the group with interface=xfrm-hub only traffic routed into the tunnel gets encrypted. Everything else, including IKE to the hub itself, bypasses it.

The spoke is configured in two steps with a reboot in between. First, everything except the policy: the xfrm interface, address, peer, group and identity.

branch-a, step 1 — xfrm, peer, identity
/interface xfrm
add name=xfrm-hub mtu=1400

/ip address
add address=10.255.0.11/24 interface=xfrm-hub

/ip ipsec profile
add name=ike2 dh-group=modp2048 enc-algorithm=aes-256 hash-algorithm=sha256 \
    dpd-interval=2m dpd-maximum-failures=5
# esp-ctr if the router accelerates AES-CTR in hardware, otherwise esp-cbc
/ip ipsec proposal
add name=esp-ctr auth-algorithms=sha256 enc-algorithms=aes-256-ctr pfs-group=modp2048
add name=esp-cbc enc-algorithms=aes-128-cbc pfs-group=modp2048

/ip ipsec peer
add name=hub address=203.0.113.10/32 exchange-mode=ike2 profile=ike2

/ip ipsec policy group
add name=hub interface=xfrm-hub

# my-id must match remote-id in the hub identity
/ip ipsec identity
add peer=hub my-id=address:10.255.0.11 secret="<PSK-branch-a>" \
    generate-policy=port-strict policy-template-group=hub
branch-a — reboot
# Leave safe mode (Ctrl+X) so the changes are kept, then:
/system reboot
branch-a, step 2 — policy
# After the reboot, enter safe mode again (Ctrl+X)
# The default template is not needed
/ip ipsec policy
set 0 disabled=yes
add peer=hub group=hub src-address=0.0.0.0/0 dst-address=0.0.0.0/0 \
    tunnel=yes proposal=esp-cbc

The second branch is identical: 10.255.0.12, its own PSK, its own LAN. The hub already has xfrm-branch-b and an identity with remote-id=address:10.255.0.12 for it.

Routing: BGP and BFD

Once the tunnel is an interface, static routes to other sites' LANs just work: gateway=xfrm-hub. BGP is more convenient though: a new branch shows up in every site's table on its own, and when a tunnel drops its routes are withdrawn so traffic can take a backup path.

On the hub, one BGP connection listens on the whole transit /24, so new spokes need no BGP changes. BFD at 200 ms notices a dead tunnel within a second instead of waiting for hold-time and DPD.

hub — BGP and BFD
/routing id
add name=rid id=10.255.0.1

# The same filter on every router: all private ranges pass
/routing filter rule
add chain=bgp-local rule="if (dst in 10.0.0.0/8) { accept; }"
add chain=bgp-local rule="if (dst in 100.64.0.0/10) { accept; }"
add chain=bgp-local rule="if (dst in 172.16.0.0/12) { accept; }"
add chain=bgp-local rule="if (dst in 192.168.0.0/16) { accept; }"
add chain=bgp-local rule="reject;"

/routing bgp instance
add name=hub as=65000 router-id=rid

# One connection for the whole transit /24: spokes connect on their own
/routing bgp connection
add name=branches instance=hub local.address=10.255.0.1 local.role=ebgp \
    remote.address=10.255.0.0/24 hold-time=4m use-bfd=yes \
    output.filter-chain=bgp-local \
    output.redistribute=static,connected routing-table=main

/routing bfd configuration
add interfaces=xfrm-branch-a,xfrm-branch-b min-rx=200ms min-tx=200ms
branch-a — BGP and BFD
/routing id
add name=rid id=10.255.0.11

# The same filter as on the hub, but first drop everything
# pointing to WAN: LTE and ISP DHCP hand out addresses from RFC ranges
/routing filter rule
add chain=bgp-local comment="reject WAN" rule="if (gw-interface lte1) { reject; }"
add chain=bgp-local comment="reject WAN" rule="if (gw-interface ether1) { reject; }"
add chain=bgp-local rule="if (dst in 10.0.0.0/8) { accept; }"
add chain=bgp-local rule="if (dst in 100.64.0.0/10) { accept; }"
add chain=bgp-local rule="if (dst in 172.16.0.0/12) { accept; }"
add chain=bgp-local rule="if (dst in 192.168.0.0/16) { accept; }"
add chain=bgp-local rule="reject;"

/routing bgp instance
add name=hub as=65101 router-id=rid

/routing bgp connection
add name=hub instance=hub connect=yes local.role=ebgp \
    remote.address=10.255.0.1/32 hold-time=4m use-bfd=yes \
    input.filter=bgp-local output.filter-chain=bgp-local \
    output.redistribute=connected routing-table=main

/routing bfd configuration
add interfaces=xfrm-hub min-rx=200ms min-tx=200ms

Tunnel filtering, NAT and MTU

Decrypted traffic arrives from the xfrm interface, so it is convenient to filter it by interface list like any internal link — no ipsec-policy=in,ipsec matchers, which a policy-based setup needs.

hub — filtering tunnel traffic
# Decrypted traffic arrives from the xfrm interface,
# so filter it like any other interface, via the ipsec list
/ip firewall filter
add chain=forward action=drop in-interface-list=ipsec dst-address=192.168.99.0/24 \
    comment="example: branches must not reach the management VLAN"

IPsec NAT exemptions are no longer needed: traffic to other sites leaves via xfrm, not WAN, so it never hits a masquerade rule with out-interface-list=WAN. The stock defconf rule with ipsec-policy=out,none can stay as it is.

We set the xfrm MTU to 1400, leaving headroom for ESP, NAT-T and the PPPoE or LTE underneath. To keep TCP clear of fragmentation, clamp MSS on traffic through the tunnels:

MSS clamping
/ip firewall mangle
add chain=forward action=change-mss new-mss=clamp-to-pmtu protocol=tcp \
    tcp-flags=syn out-interface-list=ipsec passthrough=yes
add chain=forward action=change-mss new-mss=clamp-to-pmtu protocol=tcp \
    tcp-flags=syn in-interface-list=ipsec passthrough=yes

Verification

Verification commands
# IKE session is up, the spoke's post-NAT IP is visible
/ip ipsec active-peers print

# SAs are installed, counters grow with traffic
/ip ipsec installed-sa print

# BFD and BGP over the tunnel
/routing bfd session print
/routing bgp session print

# Routes to other sites' LANs arrived via BGP over xfrm
/ip route print where bgp

# LAN-to-LAN reachability
/ping 192.168.20.1 src-address=192.168.10.1 count=5
/tool traceroute 192.168.20.1 src-address=192.168.10.1

If active-peers shows a session but installed-sa is empty, check that the hub's identity picked the right group: the spoke's my-id and the hub's remote-id must match. If SAs exist but BGP stays down, check the hub's input firewall for traffic from the ipsec interface list.

Things to keep in mind

  • Safe mode only, and a spoke reboot between creating the xfrm group and adding the policy. If you lose access anyway, get in via MAC Winbox or the console and disable the policy.
  • A separate PSK per spoke. The identity is chosen by the remote-id the spoke claims itself — with a shared key any branch can impersonate another. For many sites, move to certificates.
  • A proposal without explicit auth-algorithms uses SHA1. If a device handles SHA256, AES-CTR or AEAD ciphers such as GCM, set them explicitly, as in esp-ctr and esp-gcm.
  • Set the router ID explicitly via /routing id. “Lowest address” auto-selection on an LTE or PPPoE router may pick the dynamic WAN address, and the ID changes on reconnect.
  • One BFD configuration per interface: duplicates in /routing bfd configuration pile up easily while experimenting.
  • Both ends must run RouterOS 7.25 or newer. The other end can also be Linux with strongSwan and xfrm interfaces — the design is the same.

Summary

xfrm interfaces remove the main pain of IPsec on MikroTik: the tunnel becomes an ordinary link. Subnets are added with routes, not policies; BGP and BFD run directly over ESP without GRE; firewall rules are written per interface. For hub-and-spoke with branches behind NAT, this is now the cleanest option on RouterOS.

Need the same network between your sites?

Networks service