Forty gaming PCs, six console booths, a counter and a lounge full of waiting parents. The customers are the threat model here, the product is low latency, and every PC has to be clean for the next person. We build it on MikroTik from the seats up in 12 tested steps: diskless seats booting over 10G, customers isolated from each other by the switch chip, a game cache that downloads each title once, UPnP only where consoles need it, two WANs with games pinned to one, queuing that protects ping times, and a tournament mode you can switch on with one script.
Full network · all steps built
highlighted = lines added in the selected step · earlier steps shown plain · later steps hidden
This is part 2 of the Build series. The law firm was about keeping trusted staff away from things they didn't need. A gaming center turns that around: the people sitting at the keyboards are strangers, some of them are bored teenagers with a USB stick, and a few of them will try to cheat, sniff or crash the person next to them. At the same time the business sells exactly one thing, a fast and smooth game, so every security control has to cost zero latency.
It's built on MikroTik because that's what most real gaming centers run: a CCR handles 1 Gb/s of NAT with queues on every packet for a few hundred euros, and the CRS3xx switch chip can isolate ports at line rate. The design ideas carry over to any vendor.
01
A fictional but typical center: one hall, 40 PCs, 6 console booths, open 14 hours a day, and an owner who measures the network in complaints per evening.
The arena has 40 gaming PCs in four rows of ten, the first row beside a small stage used for weekend tournaments. There are 6 console booths with a PS5 and a big TV each, a counter with the POS PC and a card terminal, a lounge where parents wait and want Wi-Fi, and 8 cameras. Customers pay per hour through a cloud session-management service that runs a client on every PC. The owner wants three things above all: nobody complains about lag, nobody can mess with the PC next to them, and nobody leaves anything behind on a PC.
| Requirement | Where it comes from, and what it forces |
|---|---|
| R1 Clean seats | Customers install things, plug in USB sticks and leave logged-in accounts behind. Every seat must be factory-clean after every reboot, so the seats are diskless and boot an image over the network. |
| R2 Seat isolation | Customers attack each other: ARP poisoning to snoop logins, LAN flooding to lag a rival. Seats must not reach each other at Layer 2, which means isolation in the switch, not on the router. |
| R3 Latency first | One customer downloading a 120 GB game must not add a single millisecond to someone else's match. Interactive traffic is queued ahead of bulk on every packet. |
| R4 Download once | Forty seats patching the same game on release day would saturate any line. A LAN game cache fetches each title once and serves it at 10G. |
| R5 Consoles need open NAT | PS5 matchmaking and voice chat suffer behind strict NAT. Consoles get UPnP; nothing else does. |
| R6 Stay online, stay stable | Two ISPs. But a game that switches public IP mid-match drops anyway, so games stay on WAN1 and only bulk downloads use WAN2, until something fails. |
| R7 Tournaments | On weekends, seats 1–10 must see each other for LAN play, then go back to isolated. That has to be one action for staff, not a config change. |
| R8 Money & cameras | The card terminal and POS are on their own network, and so are the cameras. Neither is ever reachable from a seat. |
02
Most of this is forced by the brief. These four choices aren't, and each one is a trade-off worth stating out loud.
Diskless platforms boot every seat from a golden image on a server over iSCSI; each seat's writes go to an overlay that's thrown away at reboot. That solves R1 completely. The network decision is where the boot server sits. When a full room powers on at opening time, every seat reads several gigabytes at once. If that traffic had to cross the router, the router would become the bottleneck and the opening queue would last ten minutes. So BOOT01 sits in the seats' VLAN, on a 10G port of the seat switch, and the boot storm never leaves the switch chip.
The textbook way to stop clients talking to each other is a VLAN per client or private VLANs. Forty VLANs would mean forty subnets, forty DHCP scopes and a boot server with forty legs. Instead, the CRS354's switch chip does port isolation: each seat port may only forward to the uplink and the two server ports. Seats share one subnet, can't exchange a single frame, and the boot and cache servers are still one hop away at line rate.
Load-balancing every connection across two WANs (PCC) sounds efficient, but games and anti-cheat systems dislike a player whose public IP changes between connections, and some platforms flag it. So interactive traffic always uses WAN1, and only the game cache's downloads use WAN2, where a slow line doesn't matter. If either WAN fails, everything moves to the other one.
FastTrack makes RouterOS much faster by skipping most of the packet path, including the queues. That would defeat R3. A CCR2004 handles 1 Gb/s of NAT with a queue tree and full firewall without FastTrack, so we leave it off and accept the extra CPU load. On smaller routers, this trade-off goes the other way.
03
One /16, one /24 per VLAN, third octet = VLAN ID. Seats get fixed addresses by MAC, so "seat 17" is always .117.
| VLAN · subnet | Who lives here · gateway · DHCP |
|---|---|
| 10 SEATS · 10.30.10.0/24 | Seats 1–40 at .101–.140 (reservations by MAC), BOOT01 data .10, CACHE01 data .20. GW .1 on R1. DHCP on R1 with PXE options; DNS = CACHE01. |
| 20 CONSOLES · 10.30.20.0/24 | PS5s .11–.16 (reservations). DNS = CACHE01 (PSN updates are cacheable too). UPnP allowed. |
| 30 SRV-MGMT · 10.30.30.0/24 | BOOT01 and CACHE01 management interfaces (.10, .20), tagged on their 10G ports. Admin only. |
| 40 COUNTER · 10.30.40.0/24 | POS PC .10, card terminal .11, staff Wi-Fi. Internet + boot admin UI + NVR viewing. |
| 50 GUEST · 10.30.50.0/24 | Lounge Wi-Fi. Internet only, client isolation, 30 Mb/s shared. |
| 60 CCTV · 10.30.60.0/24 | 8 cameras .21–.28, NVR .10. No internet. |
| 99 MGMT · 10.30.99.0/24 | R1 .1, SW-SEATS .11, SW-FRONT .12, cAPs .21–.23, monitoring box .20. |
| WAN1 · 203.0.113.10/29 | Fibre, static, gateway .9. Games, consoles, seats, guests. |
| WAN2 · DHCP | VDSL from a second ISP. Game-cache downloads, plus everything if WAN1 dies. |
04
Rows start the connection, columns receive it. Default deny. Two enforcement points: the switch chip for seat-to-seat, R1's forward chain for everything else.
Two cells explain most of the design. Seats → Boot · cache is "✓ L2" because those servers live in the seat VLAN on override ports, so that traffic never reaches R1 at all. And Seats → Seats is ✕ even though they share a subnet, because the switch chip never forwards one seat port to another.
04b
Every requirement from the brief, the steps that implement it, the config that does it, and the test that proves it.
| Requirement | Implemented in | Key config | Proven by |
|---|---|---|---|
| R1 Clean seats | step 2 · step 4 | BOOT01 in the seats' VLAN on 10G; DHCP next-server + boot file; read-only golden image | "Opening rush" and "Dirty seat" drills, section 17 |
| R2 Seat isolation | step 1 · step 12 | port-isolation forwarding-override on ether1-40; DHCP snooping on both switches | "ARP poison" and "Room sweep" drills |
| R3 Latency first | step 9 | connection-bytes bulk mark; queue tree priority 1/8 with PCQ and fq-codel; no FastTrack | "Lag under load" drill |
| R4 Download once | step 2 · step 6 | CACHE01 as the seats' DNS; DNS and DoT bypass dropped | "Release day" drill |
| R5 Consoles need open NAT | step 7 | UPnP internal on v20-consoles only | "Console NAT" drill |
| R6 Stay online | step 8 | recursive routes; via-wan2 table for the cache; LAN-first routing rule | "Fibre cut" drill |
| R7 Tournaments | step 10 | tournament-on/off scripts + scheduler on SW-SEATS | "Tournament" drill |
| R8 Money & cameras | step 3 · step 5 | camera ports learn=no + static hosts; forward chain keeps V40/V60 away from seats | "Money & cameras" drill |
The build
Seats first, internet last. Each step ends with a checkpoint, and nothing goes up a layer until the current one passes.
Twelve steps: the seat switch, the game servers, the front-of-house switch, the router, the firewall, the cache, WAN1 and consoles, WAN2 and failover, queuing, tournament mode, guest Wi-Fi, and finally hardening. Each step that adds equipment opens with the topology: built parts solid, the step's additions in orange, later steps faded.
Safety first: when you change bridge VLAN settings on a remote MikroTik, use Safe Mode (Ctrl+X) or a console cable. Turning on vlan-filtering with one wrong line cuts off your own management access. See the VLAN post.
05 · Step 1 of 12
Forty seats on one CRS354, one VLAN, and a switch chip that refuses to forward a single frame from one seat to another.
▲ Show step 1 in the interactive topology · see what this step adds, and click any device for its config
The CRS354-48G-4S+2Q+ has 48 gigabit ports for the seats and four 10G SFP+ ports: one up to the router and two for the game servers, with one spare. All seat ports are untagged members of VLAN 10. The uplink is a tagged trunk. Hardware offload stays on, so the switch chip does all of this at line rate, not the switch's CPU.
/system identity set name=SW-SEATS /interface bridge add name=bridge vlan-filtering=no # seats: untagged VLAN 10 only :for i from=1 to=40 do={ /interface bridge port add bridge=bridge interface=("ether" . $i) pvid=10 \ frame-types=admit-only-untagged-and-priority-tagged } /interface bridge port add bridge=bridge interface=sfp-sfpplus1 frame-types=admit-only-vlan-tagged comment="uplink R1" add bridge=bridge interface=sfp-sfpplus2 pvid=10 comment="BOOT01 (step 2)" add bridge=bridge interface=sfp-sfpplus3 pvid=10 comment="CACHE01 (step 2)" /interface bridge vlan add bridge=bridge vlan-ids=10 tagged=sfp-sfpplus1 add bridge=bridge vlan-ids=30 tagged=sfp-sfpplus1,sfp-sfpplus2,sfp-sfpplus3 add bridge=bridge vlan-ids=99 tagged=bridge,sfp-sfpplus1 /interface vlan add name=v99-mgmt interface=bridge vlan-id=99 /ip address add address=10.30.99.11/24 interface=v99-mgmt /ip route add gateway=10.30.99.1 # R1 arrives in step 4 /interface ethernet disable ether41,ether42,ether43,ether44,ether45,ether46,ether47,ether48 # last, from the console port or in Safe Mode: /interface bridge set bridge vlan-filtering=yes
Now the isolation. On CRS3xx switches, each port can be given an explicit list of ports it's allowed to forward to. A seat port may forward only to the uplink and the two server ports:
! SW-SEATS — Catalyst 9300-48T + C9300-NM-8X: protected ports are Cisco's port isolation interface range GigabitEthernet1/0/1 - 40 description SEAT switchport mode access switchport access vlan 10 switchport protected ! protected ports never forward to each other spanning-tree portfast spanning-tree bpduguard enable interface range TenGigabitEthernet1/1/1 - 3 description R1 uplink / BOOT01 / CACHE01 ! NOT protected: every seat reaches these ! careful: an unprotected port can reach every protected one (see tournament mode)
:for i from=1 to=40 do={
/interface ethernet switch port-isolation add port=("ether" . $i) \
forwarding-override=sfp-sfpplus1,sfp-sfpplus2,sfp-sfpplus3
}Two seats in the same subnet never send their traffic to the router. Seat 12 ARPs for seat 13, gets its MAC and sends frames straight across the switch. A router firewall rule can't see that traffic, and that's exactly where ARP poisoning and LAN floods happen. The block has to be in the switch. Port isolation does it in hardware, which also means it can't be bypassed by changing an IP address. The port isolation menu has moved between RouterOS releases, so check /interface ethernet switch port-isolation print on yours, and confirm the bridge ports still show the H (hardware-offloaded) flag afterwards.
/interface bridge port print: every port has the H flag (hardware offload)./interface bridge vlan print: VLAN 10 lists ether1–40 as (dynamic) untagged and sfp-sfpplus1 as tagged.arp -a on each shows no entry for the other.06 · Step 2 of 12
Two boxes on 10G in the seats' own VLAN. One serves every seat's operating system, the other serves every game download after the first.
▲ Show step 2 in the interactive topology · see what this step adds, and click any device for its config
BOOT01 runs the diskless platform: an NVMe-backed golden Windows image plus a writeback overlay per seat. Its 10G port is untagged in VLAN 10 (address 10.30.10.10) and tagged in VLAN 30 for management (10.30.30.10). CACHE01 runs LANCache: a caching proxy plus a DNS server that answers the game launchers' CDN hostnames with its own address. It's set up the same way: 10.30.10.20 in VLAN 10, 10.30.30.20 in VLAN 30.
| Server | What matters for the network |
|---|---|
| BOOT01 | Reads, not writes, dominate: at opening time 40 seats each read a few GB within about two minutes. One 10G port on the same switch handles that at line rate. NVMe is the real limit, not the network. |
| CACHE01 | 16 TB of disk is enough for the 40–60 titles a center actually runs. Its DNS forwards everything it doesn't cache to a filtering resolver upstream (step 6). |
| Both | Spare 10G port sfp-sfpplus4 is the upgrade path: LACP the boot server when the room grows, or add a second cache. |
07 · Step 3 of 12
A second, smaller switch for everything that isn't a seat. Different devices, different VLANs, same rules.
▲ Show step 3 in the interactive topology · see what this step adds, and click any device for its config
/system identity set name=SW-FRONT /interface bridge add name=bridge vlan-filtering=no /interface bridge port add bridge=bridge interface=sfp-sfpplus1 frame-types=admit-only-vlan-tagged comment="uplink R1" add bridge=bridge interface=ether1 pvid=20 comment="booth 1" # … ether2-6: booths 2-6, same add bridge=bridge interface=ether9 pvid=40 comment="POS PC" add bridge=bridge interface=ether10 pvid=40 comment="card terminal" add bridge=bridge interface=ether13 pvid=60 comment="cam 1" # … ether14-20: cams 2-8 add bridge=bridge interface=ether21 pvid=60 comment="NVR" add bridge=bridge interface=ether22 pvid=99 comment="cAP 1 (trunk)" add bridge=bridge interface=ether23 pvid=99 comment="cAP 2 (trunk)" add bridge=bridge interface=ether24 pvid=99 comment="cAP 3 (trunk)" /interface bridge vlan add bridge=bridge vlan-ids=20,40,60 tagged=sfp-sfpplus1 add bridge=bridge vlan-ids=40,50 tagged=sfp-sfpplus1,ether22,ether23,ether24 add bridge=bridge vlan-ids=99 tagged=bridge,sfp-sfpplus1 /interface vlan add name=v99-mgmt interface=bridge vlan-id=99 /ip address add address=10.30.99.12/24 interface=v99-mgmt /ip route add gateway=10.30.99.1 # cameras: no learning, only their own MAC may use the port /interface bridge port set [find interface=ether13] learn=no /interface bridge host add bridge=bridge interface=ether13 mac-address=AC:CC:8E:00:00:21 # … repeat per camera; then: /interface bridge set bridge vlan-filtering=yes
RouterOS has no "sticky MAC". The equivalent for the outdoor cameras is to turn off MAC learning on the port and add the camera's MAC as a static host. A laptop plugged into a camera cable then gets nowhere (see port security). The three cAPs sit on ether22–24 as trunks (management untagged, staff and guest SSIDs tagged) and get their SSIDs in step 11.
/interface bridge vlan print: 20 on ether1–6, 40 on ether9–10, 60 on ether13–21, and 40, 50 and 99 on the AP ports ether22–24.08 · Step 4 of 12
R1 joins both switches, owns every gateway, and tells each seat where to boot from.
▲ Show step 4 in the interactive topology · see what this step adds, and click any device for its config
/system identity set name=R1
/interface bridge add name=br-lan vlan-filtering=yes
/interface bridge port
add bridge=br-lan interface=sfp-sfpplus1 frame-types=admit-only-vlan-tagged comment="SW-SEATS"
add bridge=br-lan interface=sfp-sfpplus2 frame-types=admit-only-vlan-tagged comment="SW-FRONT"
/interface bridge vlan
add bridge=br-lan vlan-ids=10,30 tagged=br-lan,sfp-sfpplus1
add bridge=br-lan vlan-ids=20,40,50,60 tagged=br-lan,sfp-sfpplus2
add bridge=br-lan vlan-ids=99 tagged=br-lan,sfp-sfpplus1,sfp-sfpplus2
/interface vlan
add name=v10-seats interface=br-lan vlan-id=10
add name=v20-consoles interface=br-lan vlan-id=20
add name=v30-srv interface=br-lan vlan-id=30
add name=v40-counter interface=br-lan vlan-id=40
add name=v50-guest interface=br-lan vlan-id=50
add name=v60-cctv interface=br-lan vlan-id=60
add name=v99-mgmt interface=br-lan vlan-id=99
/ip address
add address=10.30.10.1/24 interface=v10-seats
add address=10.30.20.1/24 interface=v20-consoles
add address=10.30.30.1/24 interface=v30-srv
add address=10.30.40.1/24 interface=v40-counter
add address=10.30.50.1/24 interface=v50-guest
add address=10.30.60.1/24 interface=v60-cctv
add address=10.30.99.1/24 interface=v99-mgmt
/interface list add name=LAN
/interface list member
add list=LAN interface=v10-seats
# … the same for every v* interface! R1 — C8300: IOS DHCP pool with the PXE options ip dhcp excluded-address 10.30.10.1 10.30.10.149 ip dhcp pool SEATS network 10.30.10.0 255.255.255.0 default-router 10.30.10.1 dns-server 10.30.10.20 next-server 10.30.10.10 bootfile ipxe.efi lease 0 12 ip dhcp pool SEAT-17 host 10.30.10.117 255.255.255.0 client-identifier 0158.1122.0000.11 ! 01 + the seat's MAC
/ip pool add name=p-seats ranges=10.30.10.150-10.30.10.199 # spares; real seats use reservations add name=p-consoles ranges=10.30.20.50-10.30.20.99 add name=p-counter ranges=10.30.40.50-10.30.40.99 add name=p-guest ranges=10.30.50.20-10.30.50.250 /ip dhcp-server add name=d-seats interface=v10-seats address-pool=p-seats lease-time=12h add name=d-consoles interface=v20-consoles address-pool=p-consoles lease-time=12h add name=d-counter interface=v40-counter address-pool=p-counter lease-time=1d add name=d-guest interface=v50-guest address-pool=p-guest lease-time=1h /ip dhcp-server network add address=10.30.10.0/24 gateway=10.30.10.1 dns-server=10.30.10.20 \ next-server=10.30.10.10 boot-file-name=ipxe.efi comment="seats: PXE to BOOT01" add address=10.30.20.0/24 gateway=10.30.20.1 dns-server=10.30.10.20 add address=10.30.40.0/24 gateway=10.30.40.1 dns-server=10.30.40.1 add address=10.30.50.0/24 gateway=10.30.50.1 dns-server=10.30.50.1 /ip dhcp-server lease add server=d-seats mac-address=58:11:22:00:00:01 address=10.30.10.101 comment="Seat 1" # … one reservation per seat; the diskless platform maps the same MAC to the seat number /ip dns set allow-remote-requests=yes servers=1.1.1.2,1.0.0.2
Here's what happens when a seat powers on:
ipxe.efi only works for UEFI firmware. Every gaming PC from the last decade boots UEFI, so one boot file is enough. In a mixed room you'd hand out different files depending on DHCP option 93 (client architecture), which RouterOS 7 can do with /ip dhcp-server matcher. Some diskless platforms prefer to answer the PXE part themselves as a proxyDHCP server. In that case, remove next-server and boot-file-name here, and let R1 hand out only the addresses.
/ip dhcp-server lease print where server=d-seats shows it at 10.30.10.117./interface monitor-traffic sfp-sfpplus1 on R1 shows almost nothing, because the boot traffic stayed in the switch.09 · Step 5 of 12
Input chain protects the router, forward chain is the matrix. Established/related first, explicit accepts, logged drop last.
/ip firewall address-list add list=CACHE address=10.30.10.20 add list=BOOT address=10.30.10.10 add list=POS address=10.30.40.10 add list=NVR address=10.30.60.10 /ip firewall filter # --- the router itself add chain=input action=accept connection-state=established,related,untracked add chain=input action=drop connection-state=invalid add chain=input action=accept protocol=icmp add chain=input action=accept in-interface=v99-mgmt comment="manage only from MGMT" add chain=input action=accept in-interface-list=LAN protocol=udp dst-port=53,67 add chain=input action=accept in-interface-list=LAN protocol=tcp dst-port=53 add chain=input action=drop log=yes log-prefix=IN-DROP # --- through the router add chain=forward action=accept connection-state=established,related,untracked add chain=forward action=drop connection-state=invalid add chain=forward action=accept connection-nat-state=dstnat comment="UPnP mappings (step 7)" add chain=forward action=accept in-interface=v10-seats out-interface-list=WAN comment="seats -> internet" add chain=forward action=accept in-interface=v20-consoles dst-address-list=CACHE add chain=forward action=accept in-interface=v20-consoles out-interface-list=WAN add chain=forward action=accept in-interface=v40-counter out-interface-list=WAN add chain=forward action=accept src-address-list=POS dst-address-list=BOOT protocol=tcp dst-port=443 \ comment="staff console of the diskless platform (wake/restart seats)" add chain=forward action=accept src-address-list=POS dst-address-list=NVR protocol=tcp dst-port=443 add chain=forward action=accept in-interface=v50-guest out-interface-list=WAN add chain=forward action=accept in-interface=v30-srv out-interface-list=WAN protocol=tcp dst-port=443 add chain=forward action=accept in-interface=v99-mgmt add chain=forward action=drop log=yes log-prefix=FWD-DROP
The WAN interface list is created in step 7, so the out-interface-list=WAN rules match nothing until then. Staff wake and restart seats from the diskless platform's own console, which runs on BOOT01 inside VLAN 10 where Wake-on-LAN broadcasts work. That avoids routing WoL broadcasts across VLANs (for that, see Wake-on-LAN on RouterOS).
/log print where message~"FWD-DROP" shows it.10 · Step 6 of 12
The cheapest bandwidth upgrade in the building: download each game once.
The seats' DNS server is CACHE01 (set in step 4). When Steam asks for its CDN hostname, the cache answers with its own address. The launcher then downloads from the cache at LAN speed. If the cache doesn't have the file yet, it fetches it once from the real CDN, stores it, and passes it on.
Every other DNS name is forwarded upstream to Cloudflare's malware-blocking resolver (1.1.1.2), or the family resolver (1.1.1.3) if the center admits under-16s on its own. To stop seats from bypassing the cache, and the filtering with it, with a hard-coded resolver, the forward chain drops DNS and DNS-over-TLS from seats. A seat's queries to CACHE01 never cross R1 (same VLAN), so the drop only hits bypass attempts:
/ip firewall filter
add chain=forward action=drop in-interface=v10-seats protocol=udp dst-port=53,853 \
place-before=[find comment="seats -> internet"] comment="no DNS bypass"
add chain=forward action=drop in-interface=v10-seats protocol=tcp dst-port=53,853 \
place-before=[find comment="seats -> internet"]The usual trick, dst-nat any port-53 traffic to the resolver you want, breaks here. The cache is in the same subnet as the seats, so its reply goes straight back to the seat at Layer 2, from an address the seat never asked. The seat drops it. That's the classic hairpin-NAT problem, and the drop rule avoids it entirely. DNS-over-HTTPS can't be blocked by port, so the seat image itself disables DoH in browsers and launchers by policy. That's one more advantage of a single golden image.
LANCache works because the big launchers (Steam, Battle.net, Epic, Riot, PlayStation updates) still fetch game content over plain HTTP from predictable hostnames. Anything delivered only over HTTPS passes through uncached, because the cache can't read inside TLS and we're not going to break TLS on customers' machines. In practice the big downloads are cached and the long tail isn't.
nslookup lancache.steamcontent.com returns 10.30.10.20.nslookup example.com 8.8.8.8 from a seat times out (bypass blocked).11 · Step 7 of 12
The fibre goes in, everything is masqueraded behind one public IP, and the console VLAN alone gets UPnP.
▲ Show step 7 in the interactive topology · see what this step adds, and click any device for its config
! R1 — IOS has no UPnP. Open NAT for six consoles needs six public addresses: ! ask the ISP for a /28 and give each booth a 1:1 static NAT. ip access-list standard NAT-ALL permit 10.30.0.0 0.0.255.255 ip nat inside source list NAT-ALL interface GigabitEthernet0/0/0 overload ip nat inside source static 10.30.20.11 203.0.113.17 ! booth 1 ip nat inside source static 10.30.20.12 203.0.113.18 ! booth 2 ! … booths 3-6 interface GigabitEthernet0/0/0 ip nat outside
/interface list add name=WAN /interface list member add list=WAN interface=ether1 /ip address add address=203.0.113.10/29 interface=ether1 comment="WAN1 fibre" /ip route add gateway=203.0.113.9 comment="temporary, replaced in step 8" /ip firewall nat add chain=srcnat action=masquerade out-interface-list=WAN /ip upnp set enabled=yes show-dummy-rule=no /ip upnp interfaces add interface=v20-consoles type=internal add interface=ether1 type=external
A PS5 behind ordinary NAT reports "NAT Type 3": it can make outgoing connections but can't receive them, so matchmaking and party voice sometimes fail. UPnP lets the console ask R1 for a temporary inbound mapping:
Every seat leaves through 203.0.113.10. If one customer cheats or gets reported in an online game, some anti-cheat systems and game services ban or rate-limit the IP address, and then the whole center is banned for the evening. Talk to the ISP before opening: a /29 gives you a few spare public addresses. You can source-NAT the console VLAN and the stream booth to their own address, so a ban on the seats' address doesn't take out the consoles. Some game publishers also run registration programs for internet cafés, which put the center's IP range on an allow-list.
/ip firewall connection print count-only grows./ip firewall nat print dynamic shows the console's UPnP mapping while it's in a party, and it disappears afterwards.12 · Step 8 of 12
The second ISP does two jobs: it carries the cache's downloads every day, and it carries everything on the day the fibre dies.
▲ Show step 8 in the interactive topology · see what this step adds, and click any device for its config
RouterOS detects a dead upstream with recursive routes. Each default route points at a public "probe" address (1.1.1.1 for WAN1, 9.9.9.9 for WAN2) that is itself only reachable through that WAN. check-gateway=ping pings the probe; when it stops answering, the default route that depends on it goes inactive and the next one takes over:
! R1 — IP SLA + track instead of recursive routes, PBR instead of a routing table ip sla 1 icmp-echo 1.1.1.1 source-interface GigabitEthernet0/0/0 frequency 5 ip sla schedule 1 life forever start-time now track 1 ip sla 1 reachability ip sla 2 icmp-echo 9.9.9.9 source-interface GigabitEthernet0/0/1 ip sla schedule 2 life forever start-time now track 2 ip sla 2 reachability ip route 1.1.1.1 255.255.255.255 203.0.113.9 ip route 9.9.9.9 255.255.255.255 198.51.100.1 ! VDSL modem in router mode, /30 to R1 ip route 0.0.0.0 0.0.0.0 203.0.113.9 track 1 ip route 0.0.0.0 0.0.0.0 198.51.100.1 250 ! cache downloads ride WAN2 while it's healthy; LAN traffic is excluded first ip access-list extended CACHE-OUT deny ip host 10.30.10.20 10.30.0.0 0.0.255.255 permit ip host 10.30.10.20 any route-map PBR-LAN permit 10 match ip address CACHE-OUT set ip next-hop verify-availability 198.51.100.1 1 track 2 interface Vlan10 ip policy route-map PBR-LAN
/interface pppoe-client add name=pppoe-wan2 interface=ether2 user=arena@isp2 password=******** \
add-default-route=no use-peer-dns=no disabled=no
/interface list member add list=WAN interface=pppoe-wan2
/ip route remove [find comment="temporary, replaced in step 8"]
/ip route
add dst-address=1.1.1.1/32 gateway=203.0.113.9 scope=10 comment="probe via WAN1"
add dst-address=9.9.9.9/32 gateway=pppoe-wan2 scope=10 comment="probe via WAN2"
add dst-address=0.0.0.0/0 gateway=1.1.1.1 target-scope=11 check-gateway=ping distance=1 comment="main: WAN1"
add dst-address=0.0.0.0/0 gateway=9.9.9.9 target-scope=11 check-gateway=ping distance=2 comment="main: WAN2"
# a second table where the preference is reversed
/routing table add name=via-wan2 fib
/ip route
add dst-address=0.0.0.0/0 gateway=9.9.9.9 target-scope=11 check-gateway=ping distance=1 routing-table=via-wan2
add dst-address=0.0.0.0/0 gateway=1.1.1.1 target-scope=11 check-gateway=ping distance=2 routing-table=via-wan2
/routing rule
add dst-address=10.30.0.0/16 action=lookup-only-in-table table=main comment="LAN stays LAN"
add src-address=10.30.10.20/32 action=lookup table=via-wan2 comment="cache downloads ride WAN2"The via-wan2 table only has default routes, no connected networks. Without the first rule, when CACHE01 answers a console in VLAN 20, that reply would match the cache's source-address rule, find the default route in via-wan2, and leave for the internet. The dst-address=10.30.0.0/16 → main rule has to come first. The same class of bug shows up in every policy-routing setup that forgets its own LAN. Also: 1.1.1.1 and 9.9.9.9 are now pinned to one WAN each, so never use either as a DNS server. That's why the resolvers are 1.1.1.2 and 1.0.0.2.
When WAN1 dies, every game in progress drops: the public IP changed, and no routing trick can move a live UDP session to a new address. Connections that were open on WAN1 also keep their old NAT mapping until they time out, which can take minutes. A /tool netwatch on 1.1.1.1 that runs /ip firewall connection remove [find reply-dst-address~"203.0.113.10"] when it goes down makes clients reconnect immediately. Failover keeps the room online; it doesn't keep the match alive.
/ip route print where dst-address=0.0.0.0/0: "main: WAN1" active, "main: WAN2" standby./interface monitor-traffic pppoe-wan2 shows the cache pulling it in; WAN1 is quiet.13 · Step 9 of 12
Never let a download sit in front of a game packet. Separate the two classes, prioritise the small one, then share each class fairly per seat.
Telling "game" from "download" by port is hopeless: games use hundreds of ports and launchers use plain HTTPS. What reliably separates them is size. A TCP connection that has moved more than 5 MB is a download. Everything else (game UDP, voice chat, browsing, the session client) is interactive. The router marks connections once, marks packets by direction, and the queue tree does the rest:
! R1 — NBAR2 classifies by application instead of by size class-map match-any GAME match protocol attribute category gaming match protocol attribute category voice-and-video class-map match-any BULK match protocol attribute category file-sharing policy-map WAN1-CHILD class GAME priority percent 30 class BULK bandwidth remaining percent 10 class class-default fair-queue policy-map WAN1-OUT class class-default shape average 190000000 service-policy WAN1-CHILD interface GigabitEthernet0/0/0 service-policy output WAN1-OUT ! the same classes, shaped at 950M, go on the LAN-facing interface's output for downloads ! check the category names on your release: show ip nbar attribute category
/ip firewall mangle
add chain=forward action=mark-connection new-connection-mark=bulk protocol=tcp \
connection-bytes=5000000-0 connection-mark=no-mark passthrough=yes comment="past 5 MB = a download"
add chain=forward action=mark-packet in-interface=ether1 connection-mark=bulk new-packet-mark=dl-bulk passthrough=no
add chain=forward action=mark-packet in-interface=ether1 new-packet-mark=dl-int passthrough=no
add chain=forward action=mark-packet out-interface=ether1 connection-mark=bulk new-packet-mark=ul-bulk passthrough=no
add chain=forward action=mark-packet out-interface=ether1 new-packet-mark=ul-int passthrough=no
/queue type
add name=pcq-down kind=pcq pcq-classifier=dst-address pcq-rate=0
add name=fq kind=fq-codel
/queue tree
add name=DOWN parent=global max-limit=950M
add name=down-int parent=DOWN packet-mark=dl-int priority=1 max-limit=950M queue=pcq-down
add name=down-bulk parent=DOWN packet-mark=dl-bulk priority=8 max-limit=950M queue=pcq-down
add name=UP parent=ether1 max-limit=190M
add name=up-int parent=UP packet-mark=ul-int priority=1 max-limit=190M queue=fq
add name=up-bulk parent=UP packet-mark=ul-bulk priority=8 max-limit=190M queue=fq1. FastTrack. Fast-tracked packets skip the queue tree. That's why decision 4 left it off. 2. Shaping at 100%. If max-limit equals the line rate, the queue forms in the ISP's equipment instead of here, and priorities never apply. Shape 5% below. 3. Per-seat fairness on upload. A queue on ether1 sees packets after masquerade, when every seat has the same source address, so a PCQ keyed on src-address would put the whole room into one bucket. Upload uses fq-codel (fair per flow) instead. Download is fine: the destination address was translated back to the seat before the queue sees it. Priority only works on child queues that have their own max-limit, which is why each one repeats it.
/queue tree print stats: down-bulk is dropping and queuing; down-int is not.14 · Step 10 of 12
Saturday evening: seats 1–10 need to see each other for LAN play. Sunday morning they must not. One script each way.
No VLAN changes, no new subnet, no DHCP changes. The bracket's seats stay in VLAN 10 with their usual addresses, boot server and cache. The only change is the isolation list on their ten switch ports. These scripts live on SW-SEATS, where the isolation is:
! SW-SEATS — EEM with cron timers. Because an unprotected port can reach every ! protected one, the bracket also moves to its own VLAN 70 (with its own SVI on R1). event manager applet TOURNAMENT-ON event timer cron cron-entry "0 17 * * 6" action 1.0 cli command "enable" action 2.0 cli command "configure terminal" action 3.0 cli command "interface range GigabitEthernet1/0/1 - 10" action 4.0 cli command "switchport access vlan 70" action 5.0 cli command "no switchport protected" action 6.0 syslog priority warnings msg "TOURNAMENT ON: seats 1-10 in VLAN 70" event manager applet TOURNAMENT-OFF event timer cron cron-entry "30 23 * * 6" action 1.0 cli command "enable" action 2.0 cli command "configure terminal" action 3.0 cli command "interface range GigabitEthernet1/0/1 - 10" action 4.0 cli command "switchport access vlan 10" action 5.0 cli command "switchport protected" action 6.0 syslog priority warnings msg "TOURNAMENT OFF: seats 1-10 isolated again"
/system script
add name=tournament-on source={
:local bracket "sfp-sfpplus1,sfp-sfpplus2,sfp-sfpplus3"
:for i from=1 to=10 do={ :set bracket ($bracket . ",ether" . $i) }
:for i from=1 to=10 do={
/interface ethernet switch port-isolation set [find port=("ether" . $i)] forwarding-override=$bracket
}
:log warning "TOURNAMENT ON: seats 1-10 can reach each other"
}
add name=tournament-off source={
:for i from=1 to=10 do={
/interface ethernet switch port-isolation set [find port=("ether" . $i)] \
forwarding-override=sfp-sfpplus1,sfp-sfpplus2,sfp-sfpplus3
}
:log warning "TOURNAMENT OFF: seats 1-10 isolated again"
}
/system scheduler
add name=t-on start-date=2026-11-07 start-time=17:00:00 interval=7d on-event=tournament-on
add name=t-off start-date=2026-11-07 start-time=23:30:00 interval=7d on-event=tournament-offFor irregular events, the manager runs /system script run tournament-on from the management laptop. The scheduled t-off means a forgotten "off" never leaves the bracket open past Saturday night. Writing to the log at warning level means the state change shows up on the monitoring box (step 12). For more on RouterOS scripting, see scripting.
tournament-on: seat 3 pings seat 7; seat 3 still can't ping seat 23.tournament-off: seat 3 → seat 7 fails again. /log print where topics~"warning" shows both changes.15 · Step 11 of 12
The parents in the lounge get fast-enough Wi-Fi that can't see anything else. The staff get a hidden SSID onto the counter VLAN.
▲ Show step 11 in the interactive topology · see what this step adds, and click any device for its config
/interface wifi security
add name=sec-guest authentication-types=wpa2-psk,wpa3-psk passphrase="rotate-me-monthly"
add name=sec-staff authentication-types=wpa2-psk,wpa3-psk passphrase="********"
/interface wifi datapath
add name=dp-guest bridge=bridge vlan-id=50 client-isolation=yes
add name=dp-staff bridge=bridge vlan-id=40
/interface wifi configuration
add name=cfg-guest ssid=ARENA-Guest security=sec-guest datapath=dp-guest country=Greece
add name=cfg-staff ssid=ARENA-Staff security=sec-staff datapath=dp-staff country=Greece hide-ssid=yes
/interface wifi capsman set enabled=yes interfaces=v99-mgmt
/interface wifi provisioning
add action=create-dynamic-enabled supported-bands=5ghz-ax master-configuration=cfg-guest slave-configurations=cfg-staff
add action=create-dynamic-enabled supported-bands=2ghz-ax master-configuration=cfg-guest slave-configurations=cfg-staff
# 30 Mb/s for the whole lounge, split evenly per device
/queue simple add name=guest-wifi target=10.30.50.0/24 max-limit=30M/30M \
queue=pcq-upload-default/pcq-download-defaultThe bridge=bridge in the datapath refers to each cAP's own local bridge: the APs switch traffic locally onto VLANs 40 and 50 of their trunk, so nothing hairpins through R1. The AP-side bridge setup is the same as in the CAPsMAN post and the hotel lab. Hiding the staff SSID is not a security control. It only stops customers' phones from listing it. The password is the control.
16 · Step 12 of 12
Close the management plane, stop rogue DHCP on the switches, and make sure a dead router costs 20 minutes, not a weekend.
/interface list add name=MGMT
/interface list member add list=MGMT interface=v99-mgmt
/ip service disable telnet,ftp,www,api,api-ssl
/ip service set ssh address=10.30.99.0/24
/ip service set winbox address=10.30.99.0/24
/tool mac-server set allowed-interface-list=MGMT
/tool mac-server mac-winbox set allowed-interface-list=MGMT
/ip neighbor discovery-settings set discover-interface-list=MGMT
/user add name=netadmin group=full password=********
/user disable admin
/system ntp client set enabled=yes servers=time.cloudflare.com
/system logging action add name=remote target=remote remote=10.30.99.20
/system logging add topics=warning action=remote
/system logging add topics=firewall action=remote
/system scheduler add name=nightly-export interval=1d start-time=04:00:00 \
on-event="/export show-sensitive file=nightly"/interface bridge set bridge dhcp-snooping=yes /interface bridge port set [find interface=sfp-sfpplus1] trusted=yes
DHCP snooping on the switches drops DHCP offers from anything except the uplink, so a customer's laptop running a DHCP server can't take over the room. The nightly export is collected by the monitoring box over SFTP and kept off-device. Store it encrypted, because show-sensitive includes the PPPoE and Wi-Fi passwords (see backups and hardening).
nightly.rsc from all three devices.17
Every drill maps to a requirement. Run them with the room full of test seats, not with one laptop.
| Drill (requirement) | What must happen |
|---|---|
| Opening rush (R1) | Power on all 40 seats at once. All reach the login screen within ~3 minutes; R1's uplink to SW-SEATS stays near idle. |
| Dirty seat (R1) | Install a program and save a file on seat 9, reboot: both are gone. |
| ARP poison (R2) | Run an ARP-spoofing tool on seat 12 against seat 13 and the gateway. Seat 13 never sees it; the gateway's ARP entry for seat 13 doesn't change. |
| Room sweep (R2) | nmap -sn 10.30.10.0/24 from a seat finds only .1, .10 and .20. |
| Release day (R4) | Ten seats install the same 60 GB update: WAN traffic ≈ 60 GB once, the rest is cache hits. |
| Lag under load (R3) | One seat saturates a download; ten seats play an online match. No in-game ping rise above ~2 ms. |
| Console NAT (R5) | Every booth tests NAT Type 2; a party chat across two booths works. |
| Fibre cut (R6) | Pull the fibre upstream of the ONT: the room keeps browsing via WAN2 in ~10–20 s. Matches in progress drop (expected). |
| Tournament (R7) | Scheduled on/off at the set times; the log shows both; seats outside the bracket are never reachable. |
| Money & cameras (R8) | From every VLAN except counter and management, 10.30.40.10 and 10.30.60.0/24 are unreachable. |
18
In a gaming center the attacker pays by the hour and sits next to the victim. Here's what's left for them to try.
19
No Packet Tracer for RouterOS, but nearly all of this runs on free CHR images.
| This post | In the lab |
|---|---|
| R1 CCR2004 | A CHR with three interfaces (two trunks, two WANs). Everything in steps 4–10 except the radios works the same, including recursive failover. Use two upstream CHRs as "ISPs". |
| SW-SEATS / SW-FRONT | CHR bridges with VLAN filtering. No switch chip, so port isolation doesn't exist. Emulate it with bridge horizon=1 on the seat ports (ports with the same horizon value don't forward to each other) and leave the server and uplink ports without a horizon. |
| Seats | Small Linux VMs (or VPCS) in VLAN 10. Diskless boot can be tested with iPXE + a tiny iSCSI target on a Linux VM. |
| Wi-Fi | Skip it, or tag a VM's NIC into VLAN 50 to play guest. |
20
Indicative EU street prices, late 2026, excluding VAT and the gaming PCs themselves.
| Item | Indicative (EUR, ex VAT) |
|---|---|
| R1 · CCR2004-16G-2S+ | 450 – 550 |
| SW-SEATS · CRS354-48G-4S+2Q+ | 450 – 550 |
| SW-FRONT · CRS328-24P-4S+ (PoE) | 420 – 520 |
| 3× cAP ax | 300 – 380 |
| 4× 10G DAC cables | 60 – 100 |
| BOOT01 · NVMe server + 10G NIC | 2,500 – 4,500, plus the diskless platform's per-seat licence |
| CACHE01 · 16 TB + 10G NIC | 1,200 – 2,500 |
| UPS for the closet | 600 – 900 |
| Network + servers total | ≈ 5,900 – 10,000, plus structured cabling (~50 runs) |
Gaming PCs don't need PoE, so the seat switch is a plain CRS354. The cameras and APs do, which is why SW-FRONT is the PoE model. The UPS is not optional, though: a 2-second power dip reboots 40 diskless seats at once, which is a full opening-time boot storm in the middle of a tournament. Put R1, both switches and both servers on it.
Appendix
Same brief and the same VLANs on Catalyst and a Cat8000-class router. The Cisco tabs on the config blocks above show the key steps side by side; this appendix covers what changes underneath.
| Role | This post → Cisco |
|---|---|
| SW-SEATS (48 + 10G) | CRS354-48G-4S+2Q+ → Catalyst 9300-48T + C9300-NM-8X |
| SW-FRONT (PoE) | CRS328-24P-4S+ → Catalyst 9200L-24P-4G |
| R1 | CCR2004-16G-2S+ → C8300-1N1S-6T (NAT + NBAR2 + shaping at 1 Gb/s) |
| Wi-Fi | 3× cAP ax + CAPsMAN → 3× C9115AXI with EWC |
| Indicative total (network only) | ≈ 1,700 – 2,100 → ≈ 15,000 – 20,000 EUR ex VAT |
| Feature | This post → Cisco |
|---|---|
| Port isolation | forwarding-override lists → switchport protected |
| Camera port lock | learn=no + static host → port-security mac-address sticky |
| PXE options | next-server / boot-file-name → next-server / bootfile in the DHCP pool |
| UPnP for consoles | UPnP → not available: 1:1 static NAT per booth from a public /28 |
| Recursive failover | check-gateway recursive routes → IP SLA + track |
| Cache on WAN2 | routing table + rule → PBR with verify-availability |
| Latency-first queuing | connection-bytes marks + queue tree → NBAR2 categories + LLQ under a shaper |
| Tournament mode | script + scheduler → EEM applets with cron timers |
| DHCP snooping | bridge dhcp-snooping → ip dhcp snooping (and DAI) |
Classification by application instead of by size. RouterOS calls any TCP connection past 5 MB "bulk"; Cisco NBAR2 recognises the game and download applications themselves. That's more precise, but it depends on NBAR2 knowing the titles your center runs, and the download direction has to be shaped on the LAN side, because the WAN policy only sees upload. No UPnP means open NAT for consoles needs public addresses. Protected ports aren't override lists: an unprotected port reaches every protected one, so tournament mode also moves the bracket to its own VLAN.
Cost is the real difference. Roughly ten times the network budget for the same result. Cisco makes sense here when the center is part of a chain that already runs Cisco and manages it centrally.
Takeaways
This is part 2 of the Build series. Part 1: the law firm. Next: the coffee shop.
Lag complaints, seats that get "infected", a slow line on patch day: these are network problems with known fixes. I design and build networks like this one on MikroTik and test them under a full room before opening night.
Book a Discovery Call →