MikroTik core homelab
MikroTik hardware is reasonably priced with solid construction. The config can be complex, but that complexity buys you flexibility that consumer routers can’t touch. This is how I run an RB5009UG+S+ as the core router for my homelab.
Hardware and physical layout
The RB5009UG+S+ has 8 gigabit ports, a 2.5G port, and an SFP+ cage. It runs RouterOS 7.
ether1 → POE switch (2.5G, also backup connectivity)
sfp1 → same switch (SFP+ primary uplink)
ether2 → workshop
ether7 → access point (untagged VLAN)
ether8 → WAN (ISP)
SFP+ is the primary link to the switch. ether1 provides POE power to the switch and serves as a backup path if the SFP+ link drops.
VLAN design
Five VLANs keep traffic separated:
/interface bridge
add name=bridge_main vlan-filtering=yes
/interface vlan
add comment=private interface=bridge_main name=private_vlan vlan-id=5
add comment=servers interface=bridge_main name=servers_vlan vlan-id=10
add comment=work interface=bridge_main name=work_vlan vlan-id=20
add comment=pxe interface=bridge_main name=pxe_vlan vlan-id=30
add comment="ipv6 only" interface=bridge_main name=ipv6_vlan vlan-id=40
add comment=iot interface=bridge_main name=iot_vlan vlan-id=50
| VLAN | ID | Purpose |
|---|---|---|
| Private | 5 | Phones, laptops, access points |
| Servers | 10 | Kubernetes, VMs, BMCs, KVM |
| Work | 20 | Internet-only, isolated from LAN |
| PXE | 30 | Netboot provisioning |
| IPv6 only | 40 | Devices that only get IPv6 |
| IoT | 50 | Shelly relays, smart plugs, 3D printers |
All VLANs are tagged on trunk ports (ether1 and sfp1) and the bridge itself. Access ports get untagged membership in their respective VLAN; ether7 is untagged on the private VLAN:
/interface bridge vlan
add bridge=bridge_main tagged=sfp1,bridge_main,ether1 untagged=ether7 vlan-ids=5
add bridge=bridge_main tagged=sfp1,bridge_main,ether1 vlan-ids=10
add bridge=bridge_main tagged=ether1,bridge_main,sfp1 vlan-ids=20
add bridge=bridge_main tagged=bridge_main,sfp1,ether1 vlan-ids=30
add bridge=bridge_main tagged=bridge_main,sfp1,ether1 vlan-ids=40
add bridge=bridge_main tagged=bridge_main,sfp1,ether1 vlan-ids=50
Each VLAN gets its own gateway IP, now on its own VLAN interface instead of directly on the bridge:
/ip address
add address=10.10.0.1/22 interface=private_vlan network=10.10.0.0
add address=10.10.10.1/22 interface=servers_vlan network=10.10.10.0
add address=10.10.20.1/22 interface=work_vlan network=10.10.20.0
add address=10.10.30.1/24 interface=pxe_vlan network=10.10.30.0
add address=10.10.50.1/24 interface=iot_vlan network=10.10.50.0
Each VLAN also gets added to the appropriate interface list for firewall zone membership:
/interface list member
add interface=private_vlan list=LAN
add interface=servers_vlan list=LAN
add interface=work_vlan list=WORK
add interface=pxe_vlan list=LAN
add interface=iot_vlan list=LAN
add interface=ipv6_vlan list=LAN
add interface=ether8 list=WAN
The /22 mask on the private and servers subnets gives 1022 addresses each; room to spare. The work subnet uses the same size for consistency but could be tighter.
WireGuard
Three WireGuard interfaces cover three use cases:
| Interface | Port | Use case |
|---|---|---|
wg-road |
51821 |
Road warrior: phones, laptops, iPads |
wg-road |
same interface | Site-to-site: travel router, remote house |
wg-vpn |
51822 |
VPN provider tunnel |
Road warrior
Each device gets a static /32 from a shared /16 pool, a DNS server, and an endpoint domain. The client-allowed-address controls what routes the device sends through the tunnel; 0.0.0.0/0 for full tunnel, or specific subnets for split tunnel.
/interface wireguard
add listen-port=51821 mtu=1420 name=wg-road
add listen-port=51822 mtu=1370 name=wg-vpn
/interface wireguard peers
add allowed-address=10.100.0.2/32 client-address=10.100.0.2/16 \
client-allowed-address=::/0,0.0.0.0/0 client-dns=1.1.1.1 \
client-endpoint=vpn.example.com interface=wg-road \
name="Laptop" public-key="<REDACTED>"
add allowed-address=10.100.0.3/32 client-address=10.100.0.3/16 \
client-allowed-address=::/0 client-dns=1.1.1.1 \
client-endpoint=vpn.example.com interface=wg-road \
name="Phone" public-key="<REDACTED>"
Site-to-site
Two WireGuard peers connect remote sites back to the core:
add allowed-address=10.100.0.4/32,10.50.0.0/24 interface=wg-road \
name=remote-site public-key="<REDACTED>"
add allowed-address=10.100.0.8/32,10.60.0.0/16 client-address=10.100.0.8/16 \
client-allowed-address=0.0.0.0/0,::/0 client-dns=1.1.1.1 \
client-endpoint=vpn.example.com interface=wg-road \
name=travel-router public-key="<REDACTED>"
The allowed-address field on the router side lists the subnets reachable behind each peer. The router then has static routes pointing to the peer’s WireGuard IP for those subnets (covered in the routing section below).
VPN provider tunnel
wg-vpn connects to a VPN provider. The peer gets 0.0.0.0/0 in allowed-address; all traffic marked for the VPN routing table goes through this interface:
add allowed-address=0.0.0.0/0 endpoint-address=192.0.2.1 \
endpoint-port=51820 interface=wg-vpn name=vpn-endpoint \
public-key="<REDACTED>"
Multiple provider endpoints are configured for failover, with only one active at a time.
DHCP with DNS auto-registration
Three DHCP servers, one per VLAN. Each has the same lease script that auto-registers DNS entries from DHCP hostnames. When a device sends a hostname in its DHCP request, it becomes resolvable at hostname.home.example.com with a 7-day TTL.
The IoT VLAN gets its own DHCP server with a shorter lease time; IoT devices tend to be long-lived and don’t need frequent renewal:
/ip dhcp-server
add address-pool=private interface=private_vlan lease-time=1w name=private
add address-pool=servers interface=servers_vlan lease-time=1w name=servers
add address-pool=work interface=work_vlan lease-time=1w name=work
add address-pool=iot interface=iot_vlan lease-time=14d name=iot
The lease script (simplified):
:local dnsDomain "home.example.com"
:local dnsTtl "7d 00:00:00"
:local leaseClientHostname $"lease-hostname"
:if ([:len [$leaseClientHostname]] > 0) do={
:local hostnames "$leaseClientHostname.$dnsDomain,$leaseClientHostname"
:foreach h in=[:toarray $hostnames] do={
/ip dns static add address="$leaseActIP" name="$h" ttl="$dnsTtl"
}
}
This means every device; Shelly relays (on the IoT VLAN), access points, 3D printers, laptops; gets a predictable DNS name without manual entry. The script also handles cleanup: when a lease expires or changes IP, old DNS entries are removed and new ones added.
PXE boot via netboot.xyz
DHCP option sets serve netboot.xyz for BIOS and UEFI clients. Matchers use DHCP option 93 to detect UEFI and hand out the correct boot file:
/ip dhcp-server option
add code=67 name=pxe-bios value="'netboot.xyz.kpxe'"
add code=67 name=pxe-uefi value="'netboot.xyz.efi'"
/ip dhcp-server option sets
add name=pxe-bios options=pxe-bios
add name=pxe-uefi options=pxe-uefi
/ip dhcp-server matcher
add address-pool=servers code=93 name=pxe-uefi option-set=pxe-uefi \
server=servers value=0x0007
add address-pool=private code=93 name=pxe-uefi-private \
option-set=pxe-uefi server=private value=0x0007
Option 93 value 0x0007 means “UEFI x86-64.” When a client sends this, the match fires and the DHCP server hands back the netboot.xyz UEFI image. For BIOS clients (no option 93), the BIOS option set is the default on the DHCP server.
A dedicated PXE VLAN means you can netboot machines in isolation without touching production subnets.
DNS
The router runs its own DNS resolver with Quad9 upstream and DNS-over-HTTPS:
/ip dns
set allow-remote-requests=yes cache-size=32768KiB \
doh-max-server-connections=10 doh-timeout=3s \
servers=9.9.9.9,149.112.112.112,2620:fe::fe,2620:fe::9
Static DNS entries from the lease script handle internal hostnames. A few manual entries cover static-IP infrastructure; the router itself, KVMs, and the ingress proxy.
All LAN clients use the router as their DNS server (set in DHCP network config). IPv6 uses the router’s link-local address as advertised DNS via RA.
Firewall and NAT
The input chain drops everything from WAN except the WireGuard interface (stateless; no response unless the key matches) and SSH, which is restricted by source address list. New inbound connections from WAN are dropped in the forward chain unless explicitly DNAT’d, and DNAT rules are further locked down by source address list. Hairpin NAT handles internal clients reaching public services without split DNS.
Forward chain; fasttrack with a carve-out
Fasttrack bypasses the firewall for established connections, which improves throughput dramatically on this hardware. But fasttracked packets skip mangle rules, so traffic that needs routing-mark handling must be excluded:
add action=fasttrack-connection chain=forward connection-mark=no-mark \
connection-state=established,related
add action=accept chain=forward connection-state=established,related,untracked
add action=drop chain=forward connection-state=invalid
add action=drop chain=forward connection-nat-state=!dstnat \
connection-state=new in-interface-list=WAN
Packets with a connection mark set by the mangle rules skip fasttrack and fall through to the normal forward path, where the routing mark is applied.
Policy routing; send traffic through VPN
The mangle rules mark traffic in two directions:
- Ingress from VPN; packets arriving on
wg-vpnget a routing mark so reply traffic goes back out the same interface - Egress to VPN; packets from specific source IPs (or to specific destination domain lists) get a routing mark that pushes them through the VPN routing table
/ip firewall mangle
add action=change-mss chain=forward new-mss=clamp-to-pmtu \
out-interface=wg-vpn protocol=tcp tcp-flags=syn
add action=mark-routing chain=prerouting in-interface=wg-vpn \
new-routing-mark=vpn passthrough=no
add action=mark-routing chain=prerouting \
dst-address-list=!private-ranges new-routing-mark=vpn \
src-address-list=route-to-vpn
add action=mark-routing chain=prerouting \
dst-address-list=dest-route-to-vpn new-routing-mark=vpn
add action=mark-connection chain=prerouting connection-state=new \
new-connection-mark=vpn routing-mark=vpn
The MSS clamp was needed due to a specific provider issue; some TCP connections would silently hang without it.
BGP and internal routing
The router runs iBGP with the k8s cluster. kube-vip advertises service VIPs via BGP, and the router redistributes them into the main routing table. This means any device on the network can reach k8s services by their VIP without additional config:
/routing bgp instance
add as=64500 name=bgp-instance router-id=10.10.10.1
/routing bgp connection
add afi=ip,ipv6 instance=bgp-instance local.role=ibgp \
name=k8s remote.address=10.10.10.0/22 .as=64500 routing-table=main
Static routes cover the WireGuard subnets:
/ip route
add dst-address=10.50.0.0/24 gateway=10.100.0.4
add dst-address=10.60.0.0/16 gateway=10.100.0.8
add dst-address=0.0.0.0/0 gateway=wg-vpn routing-table=vpn
The default route for the VPN table sends all marked traffic through wg-vpn. Internal subnets have explicit routes in the VPN table so they stay reachable from VPN-connected devices.
IPv6
The ISP delegates a /56 prefix via DHCPv6-PD. The router advertises the prefix on LAN interfaces and gets its own address via EUI-64:
/ipv6 dhcp-client
add add-default-route=yes interface=ether8 pool-name=isp-pool \
pool-prefix-length=64 request=prefix use-interface-duid=yes
An MSS clamp is needed because the ISP has broken PMTU discovery on IPv6:
/ipv6 firewall mangle
add action=change-mss chain=forward new-mss=clamp-to-pmtu \
out-interface=ether8 protocol=tcp tcp-flags=syn tcp-mss=!0-1400
The IPv6 firewall mirrors the v4 rules with the same allow-established, drop-invalid, drop-WAN pattern.
Monitoring
Logs ship to Loki for centralised viewing:
/system logging action
add name=loki remote=10.99.0.21 remote-log-format=syslog target=remote
/system logging
add action=loki topics=info
add action=loki topics=error
add action=loki topics=warning
add action=loki topics=critical
SNMP is enabled for bandwidth graphing, and NTP keeps the clock in sync using the UK pool:
/snmp
set enabled=yes trap-interfaces=all
/system ntp client
set enabled=yes
/system ntp client servers
add address=0.uk.pool.ntp.org
add address=1.uk.pool.ntp.org
Key points
- The RB5009 is reasonably priced hardware that punches well above its weight. RouterOS complexity is the trade-off for flexibility
- VLANs with bridge vlan-filtering keep traffic tiers separated cleanly; tagged on trunks, untagged on access ports
- DNS auto-registration from DHCP lease script means every device gets a predictable hostname without manual entries
- WireGuard on the router covers all three VPN patterns: road warrior, site-to-site, and tunnel provider; no separate VPN appliance needed
- Policy routing via mangle marks sends selected traffic through the VPN provider while leaving the rest on the WAN
- Inbound openings are minimal; WireGuard is stateless, SSH is source-locked, and DNAT rules use dual source + destination address lists for defence in depth
- Fasttrack with a connection-mark carve-out gives line-rate throughput for most traffic while preserving policy routing
- iBGP from the router to the k8s cluster distributes service VIPs across the whole network
- MSS clamping on both IPv4 and IPv6 works around ISP PMTU issues; without it, some connections silently hang