How to set up a personal AC+AP style network?

How to set up a personal AC+AP style network?

Shown above is a GL.iNet mt3600be router, used in this blog post for demonstration. It uses an OpenWrt- based system, providing excellent flexibility. It will be used as a powerful MLO (Wi-Fi 7) Access Point.

mt3600be

As you can see, this is a CN version of a GL.iNet router. Features including VPN and AdGuard Home are blocked (hidden). For Wi-Fi 7 MLO, 6 GHz bandwidth may also be limited. To achieve the best experience, you can unlock it by replacing the country code in the factory image1. For the global version, you already have most features available.

Table of Contents

What is an AC+AP style network?

An AC+AP network is composed of a central Access Controller and one or more Access Points. It is widely used in enterprise environments for large Wi-Fi coverage and fast roaming. Campus Wi-Fi is a classic example of an AC+AP style network. It can cover an entire campus, and users normally cannot feel the latency when a device switches APs. AC+AP architecture utilizes 802.11k/v/r standards to achieve seamless AP transitions, which is a key advantage.

Final
Final setup

The image above is an illustration of the finished setup. The Raspberry Pi 5 on the left is my AC and gateway. The mt3600be on the right is used as a wireless access point, supporting Wi-Fi 7. The Wi-Fi card, mt7925, on top of the Pi is a secondary AP that I built in this post. This post can be seen as an extension of that earlier blog on how to DIY your own router.

Components of AC+AP network

  • AC: a central controller that manages one or more APs. In this setup, the Raspberry Pi also acts as the router/gateway.
  • AP: Wireless access point. mt3600be and mt7925 are used.
  • PoE Switch: Optional. Use an Ethernet cable for power and data for a simpler setup. The switch ensures all AC and AP devices are in one subnet.
  • Main router: Used to access WAN. A Raspberry Pi is used.

Difference to mesh architecture?

An AC+AP (Access Controller + Access Points) network is a centralized Wi-Fi architecture where multiple APs are managed by a single controller that handles configuration, channel planning, and roaming, typically with all APs connected via wired Ethernet cable. In contrast, a mesh network is distributed: each node acts as both an AP and a relay, forwarding traffic over wireless links to other nodes without requiring a central controller. The key difference is therefore centralized control with wired connection (AC+AP) versus distributed forwarding with wireless connection (mesh). As a result, AC+AP systems usually provide more stable performance and predictable latency, while mesh networks offer easier deployment but can suffer reduced throughput due to multi-hop wireless links.

AC+AP is designed to scale larger, targeted for enterprise scenarios, while mesh is targeted for smaller/home networks.

Final
I have 2 OpenWrt routers. In this image, I set both of them as Access Points, all connected via physical Ethernet cable. This is an AC+AP style network.

Access Controller & Main Router configuration

Now let’s dive into configuration. On OpenWrt, when you inspect interfaces, you can find one called br-lan; it is a virtual switch. On the AC (Raspberry Pi), you need a virtual switch as well, to put multiple network interfaces into one LAN.

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 2c:cf:67:f5:96:bd brd ff:ff:ff:ff:ff:ff
    inet 143.89.92.189/23 brd 143.89.93.255 scope global dynamic noprefixroute eth0
       valid_lft 322039sec preferred_lft 322039sec
    inet6 fe80::2ecf:67ff:fef5:96bd/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master br0 state UP group default qlen 1000
    link/ether 9c:69:d3:75:af:fe brd ff:ff:ff:ff:ff:ff
    inet6 fe80::9e69:d3ff:fe75:affe/64 scope link proto kernel_ll 
       valid_lft forever preferred_lft forever
5: wlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP group default qlen 1000
    link/ether 84:9e:56:9c:71:a5 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::869e:56ff:fe9c:71a5/64 scope link proto kernel_ll 
       valid_lft forever preferred_lft forever
6: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 84:9e:56:9c:71:a5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.205.1/24 scope global br0
       valid_lft forever preferred_lft forever

Above are the network interfaces used on my AC. br0 is the virtual switch and holds my network subnet IP address: 192.168.205.1/24. wlan1 is the mt7925 interface and provides a 5 GHz radio. eth1 is my USB-to-Ethernet adapter, connected externally to my GL.iNet mt3600be AP. eth0 is the WAN interface; all traffic will be masqueraded and exit through it.

Set up bridge on startup

To maintain a persistent bridge even after reboot, the best approach is to use a systemd service. Create a /usr/local/sbin/setup-br0.sh shell script that runs on startup.

#!/usr/bin/env bash
set -euo pipefail

BR=br0
LAN_IP=192.168.205.1/24

# Create bridge if missing
if ! ip link show "$BR" >/dev/null 2>&1; then
    ip link add name "$BR" type bridge
fi

# Make sure bridge is up and owns the LAN IP
ip link set dev "$BR" up
ip link set dev "$BR" type bridge stp_state 0 || true

ip addr flush dev "$BR" 2>/dev/null || true
ip addr add "$LAN_IP" dev "$BR"

# Add eth1 only if it exists
# eth1 is usb ethernet adaptor
if ip link show eth1 >/dev/null 2>&1; then
    ip addr flush dev eth1 2>/dev/null || true
    ip link set dev eth1 nomaster 2>/dev/null || true
    ip link set dev eth1 master "$BR"
    ip link set dev eth1 up
fi

# We don't set wlan1 master to br0 here is because
# hostapd will do the bridge configuration
# leave it to hostapd

You may need to modify the script based on your actual setup. For a Raspberry Pi 5 with a USB Ethernet adapter, you can use the script as is.

Create a systemd unit: /etc/systemd/system/setup-br0.service

[Unit]
Description=Create br0 bridge and attach eth1
Before=hostapd.service
After=NetworkManager.service
Requires=NetworkManager.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/setup-br0.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Then run sudo systemctl daemon-reload && sudo systemctl enable setup-br0; this will make the script execute at startup.

After reboot or direct execution of the script, you can verify it with bridge link show. All network interfaces that you want to place in the same LAN should share the same master bridge. In my case, as shown below, wlan1 and eth1 both have master br0.

3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 5
5: wlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 100

For the wlan1 interface, you don’t manually assign it to br0. Put that in the hostapd config file, and hostapd will configure it for you.

Configure routing (firewall)

Now that your bridge is configured, it is time to configure routing rules and access controls. I use nftables as the firewall, which gives more granular control than ufw or firewall-cmd. nftables is the default firewall in modern Linux operating systems, offering better usability than iptables.

#!/usr/sbin/nft -f

# add table ensures the table exists before flush
# Do not use flush ruleset, scope is too big
# And will modify iptables-nft rules
add table inet filter
flush table inet filter

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        iifname "lo" accept
        ct state established,related accept

        # DNS / DHCP / UPnP
        iifname { "br0", "wlan1", "eth1" } udp dport { 67, 53 } accept
        iifname { "br0", "wlan1", "eth1" } tcp dport 53 accept

        ip protocol icmp accept
        ip6 nexthdr ipv6-icmp accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        ct state established,related accept

        # Normal routing
        iifname "br0" oifname "eth0" accept

        # LAN, change to your subnet
        ip saddr 192.168.205.0/24 accept

        ip protocol icmp accept
        ip6 nexthdr ipv6-icmp accept
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }

    # ---------- NAT ----------
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;
    }

    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname "eth0" ip saddr 192.168.205.0/24 masquerade
    }
}

Above is an example of restricted cone NAT achieved with nftables2, the most common NAT type. If you are using P2P, consider using miniupnpd3 or change postrouting last line to oifname "eth0" ip saddr 192.168.205.0/24 masquerade fully-random, persistent, change source address field to your LAN address.


What does an AC+AP router need?

  • Routing: Covered, use nftables
  • DHCP / DNS: use dnsmasq4
  • Bridge LAN: Covered, use shell script to manually create and assign
  • Radio: use hostapd or add a router as a dummy access point (covered next).

Before you continue, you should finish the first 3 steps, or at least part of step 4. For me, I am using hostapd on wlan15, my MediaTek Wi-Fi card.

Access Point configuration on OpenWrt

Generic OpenWrt

On OpenWrt, to achieve the best roaming speed, 802.11k/v/r standards are required. OpenWrt firmware usually bundles wpad-basic-mbedtls, which does not include support for these standards. We will need to install wpad-openssl for full support.

wpad-openssl

On the latest OpenWrt 25.12 version, use apk add wpad-openssl or go to LuCI6 and install the package in System->Software, search wpad-openssl, and install it.

Then go to Network->Wireless and configure radio settings.

network

I recommend using interactive configuration instead of UCI because it is easier. On older OpenWrt versions, such as 21 on mt3600be, LuCI does not provide a section to edit roaming configuration, even though it is supported. In that case, UCI is needed.

Lastly, go to Edit radio, configure your Access Point, disable DHCP on br-lan, and set br-lan to a static IP address.

AP

GL.iNet OpenWrt

In GL.iNet Network mode, you can set OpenWrt to Access Point. It will automatically disable DHCP and request an IP address from the upstream router via DHCP.

Network mode GLiNet

This does not provide your router with a static IP address. When accessing the router admin panel remotely, you’ll need to look up the IP address on the main router. Or use the following:

uci set network.lan.ipaddr='192.168.205.2' # In the same subnet, avoid IP address collision
uci set network.lan.netmask='255.255.255.0'
uci commit network
/etc/init.d/network restart

To enable 802.11k/v/r, use the following:

uci set wireless.lan.ieee80211r='1'
uci set wireless.lan.mobility_domain='4f57'
uci set wireless.lan.ft_over_ds='1'
uci set wireless.lan.ft_psk_generate_local='1'

uci set wireless.lan.bss_transition='1'

uci set wireless.lan.rrm_neighbor_report='1'
uci set wireless.lan.rrm_beacon_report='1'

uci commit wireless
wifi reload

After this, your GL.iNet router will be a powerful access point.

Final

Useful Tips

  1. Use an Ethernet cable to connect to your router before configuring, in case you lose access midway.
  2. When configuring the router IP address, your gateway IP address will change, for example from 192.168.8.1 to 192.168.205.*. So your admin panel will not receive a response at the old address; go to the new IP address to access the admin panel.

For the generic OpenWrt demo, I used a Cudy TR3000 (256 MB version), a Wi-Fi 6 router with an mt7981 chipset. Flashing OpenWrt is very straightforward, and OpenWrt has official support7. This is a decent OpenWrt starter device.

Final
Final
Final
Internal details

REFERENCES:

Footnotes

  1. How to find “CN” string and replace to “US” for GL.iNet mt3600be: https://www.reddit.com/r/GlInet/comments/1qfeva7/guide_region_unlock_glmt3600be_beryl_7_cn_to

  2. nftables NAT documentation: https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_(NAT)#Masquerading

  3. OpenWrt configure upnpd: https://openwrt.org/docs/guide-user/firewall/upnp/miniupnpd

  4. Dnsmasq configuration tutorial on Fedora: https://docs.fedoraproject.org/en-US/fedora-server/administration/dnsmasq/

  5. Zihao Fu, configuring mt7925 on Raspberry Pi: https://zihaofu245.me/blog/2026-3-05-pi-router/#Hostapd

  6. LuCI documentation: https://openwrt.github.io/luci/jsapi/

  7. Cudy TR3000 OpenWrt documentation: https://openwrt.org/toh/cudy/tr3000