AMPRNet · 44Net Connect · deployment guide
How to take a block of public 44.x.x.x amateur radio address space, carry it home over two WireGuard tunnels, and announce it with BGP from a small MikroTik router. Written so you can follow it without ever having configured BGP before.
| document | 44Net Connect, dual tunnel, BGP-routed subnet |
| version | 1.2 |
| author | Mark, K9MEV · markvd.net |
| platform | MikroTik RouterOS 7.x |
| portal | connect.44net.cloud |
| build time | about two evenings |
| license | MIT, see the end of the document |
This is a field guide, not a theory paper. Follow it top to bottom and you will end up with a small block of globally routable IPv4 addresses, delivered to a router in your house, announced by your own autonomous system number over two independent tunnels, with a sane firewall in front of it.
I am Mark, K9MEV, an Amateur Extra class operator in grid EN51uu, Batavia, Illinois, and an IT
security and infrastructure leader by trade with 25 years of building and defending enterprise networks.
Outside the day job I operate my own small autonomous system, AS402766, with ARIN-assigned IPv4 and IPv6
space announced in the global routing table, and I serve as a volunteer AMPRNet IP coordinator for the
Illinois 44.72.0.0/16 allocation. Locally I am a member and webmaster of the Fox River Radio
League (W9CEQ) and run the K9MEV-1 APRS digipeater and K9MEV-10 igate.
This document came out of building the deployment it describes on my own bench. Everything in it has been run on real hardware against the live 44Net Connect service. More at markvd.net.
Every value that is specific to you appears as <LIKE_THIS>. Never paste a configuration from this document without replacing all of them. The screenshots have been redacted the same way: anything in a pink box was a real key, address or callsign in the original. Appendix B lists every placeholder and where to find its real value.
44Net (also called AMPRNet) is a block of real, public IPv4 address space (44.0.0.0/9 and 44.128.0.0/10) set aside for amateur radio use and administered by Amateur Radio Digital
Communications (ARDC). Addresses from it are routable on the global internet. They are free to licensed
amateurs, and there is no NAT anywhere in the path: a host with a 44 address is directly reachable from
anywhere, and reaches out with that address as its source.
That last sentence is the whole appeal, and also the whole risk. Read section 11 before you put anything on this network.
| Approach | What you get | Effort |
|---|---|---|
| Single-host tunnel | One 44 address on one machine: a laptop, a Pi, an AllStarLink node, a cloud VM. WireGuard config in, address out. | 15 minutes |
| Routed subnet, static | A whole subnet (say 8 or 16 addresses) routed down your tunnel to a router you control. The point of presence holds a static route for you. | An evening |
| Routed subnet, BGP | The same subnet, but you announce it with your own autonomous system number, over more than one tunnel, with automatic failover between them. This document. | Two evenings |
Historically the third option meant finding a friendly ISP willing to peer with you, or joining the old IPIP mesh. 44Net Connect replaced both with something much more approachable: ARDC runs points of presence (PoPs) in several cities, you build a WireGuard tunnel to one, and everything (addresses, routing, even a private ASN) is handed out from a web portal.
Two honest reasons. First, redundancy: with two tunnels to two different PoPs, a PoP outage or a bad path costs you a few seconds of reconvergence instead of a day of downtime. Second, this is amateur radio, and running a real routing protocol on your own address space is the point, not an inconvenience on the way to it.
One small router, two WireGuard tunnels to PoPs in two different cities, one subnet, one private ASN announced over both tunnels. Traffic normally takes whichever path BGP prefers, and if that tunnel drops, everything moves to the other one without anybody touching a keyboard.
The important structural choice, and the one that makes the whole thing safe to live with: the 44net
router is a dedicated appliance sitting behind your existing home router, not a replacement for it.
Its ether1 takes a DHCP address from your ordinary LAN and is used only for management and as
the path the WireGuard tunnels ride over. Its bridge carries the public 44net segment. Nothing about your
household internet changes, and a mistake on this box cannot take the family offline.
| Piece | In this build | Notes |
|---|---|---|
| Router | MikroTik hEX S 2025 (E60iUGS), RouterOS 7.20 | Any RouterOS 7 device with enough CPU for WireGuard. See section 4. |
| Position | Behind the existing home router, on its own segment | Not the household gateway. Management lives on the ether1 side. |
| Tunnels | Two, to PoPs in two different cities | Different regions, not two nodes in the same datacentre. |
| Address space | One /28, 16 addresses | A /29 is the usual starting point. Ask for what you will actually use. |
| ASN | One private ASN, auto-assigned | From the 4244000000 to 4294967294 range. Meaningful only inside 44Net. |
| Routing | eBGP over each tunnel, IPv4, in a dedicated routing table | You advertise your prefix; you accept a default and 44Net routes back, but only into that table. |
| IPv6 | Disabled on this box | One less attack surface while you get IPv4 working. Turn it on later if you want it. |
The PoP will happily send you a default route. If that landed in the main table, every packet the router forwards would try to leave through the tunnel, including the WireGuard packets themselves, which is a good way to build a loop. Putting the BGP session and everything it learns in its own table, then steering only 44net-sourced traffic into that table with a routing rule, keeps the two worlds cleanly apart. This is the single most important design decision in the build.
The wider internet never sees your ASN. ARDC announces the large aggregate to the world; your ASN and your prefix are visible inside 44Net Connect, which is what decides which tunnel your traffic arrives on. Do not expect to find yourself on a public looking glass.
Tunnel requests are usually instant, but network and ASN requests may sit in a queue for human review. Request them early in the evening, not five minutes before you want to be on the air.
Anything that runs RouterOS 7 will do this. WireGuard on RouterOS is done in software, so throughput is a CPU question, and the routers below are ARM devices that will comfortably carry more traffic than a 44Net subnet at home is ever likely to generate. Two are worth singling out.
| L009UiGS-RM | hEX S 2025 (E60iUGS) | |
|---|---|---|
| Form factor | 1U rackmount (four fit in 1U) | Small desktop box |
| CPU / RAM | Dual-core ARM 800 MHz, 512 MB | Dual-core ARM 950 MHz, 512 MB |
| Ports | 8 × gigabit, 1 × 2.5G SFP, USB 3.0 | 5 × gigabit, 1 × 2.5G SFP, USB 3.0 |
| Serial console | Yes, RJ45 console port | No |
| Power | DC jack, PoE-in, terminal block; PoE-out on ether8 | DC jack, PoE-in; PoE-out on ether5 |
| License level | 5 | 4 |
| Street price | about $107 | about $68 |
| Why pick it | The real console port. You will eventually lock yourself out at 23:40, and a serial cable saves the evening. | Cheapest sensible way in. Featureful, quiet, fanless, and fast enough that the tunnel will not be your bottleneck. |
MikroTik's published test results are for plain routing, not WireGuard. Expect a few hundred megabits per second of encrypted throughput on either device, single tunnel, large packets, and expect less with small packets. Check the current figures on the product page for the model you buy rather than trusting a number in a document. IPsec has hardware acceleration on these boxes; WireGuard does not.
MikroTik's own site keeps a distributor list; start from the product page for the model you want.
A virtual router is also the low-risk way to do this the first time: build it, break it, rebuild it, then buy the hardware.
Order matters here. Request them like this:
The screenshots below walk each screen. They are from a working deployment, with the sensitive fields replaced.
Go to My Resources → ASNs → Request an ASN. The portal is honest with you here: most users do not need an ASN, and it will say so. You do, because you are running BGP.
4244000000–4294967294, which is exactly what
you want.When it is approved it appears in the ASN list. Note the number down; it becomes <YOUR_ASN> everywhere below.
My Resources → Networks → Request Network Allocation, then pick the pool offered to you.
Then name the request and choose a size. Sizes run from a single address up to a /24.
/29 gives you 8 addresses and is the recommended
starting point; a /28 gives you 16 and leaves room to subdivide later.Anything /27 or smaller, meaning 32 addresses or fewer, is effectively auto approved and comes back almost immediately. That is not a reason to take one. 44Net space is a finite community resource, and a request that is obviously larger than your plans is the kind of thing that gets noticed. Count the hosts you actually intend to put on the segment, add a little headroom, and request that. You can always come back for more.
Once approved, the allocation shows up in your Networks list. That prefix is <YOUR_44NET_PREFIX> from here on.
My Resources → Tunnels → Request Tunnel. You will do this twice.
Then fill in the tunnel form.
If you let the portal generate the keypair, the private key is shown to you exactly once, on the page immediately after creation. Copy it somewhere safe before you navigate away. If you miss it, delete the tunnel and request a new one.
The tidier alternative: create the WireGuard interface on the router first (section 7), read its public
key with /interface/wireguard/print, and paste that into the Public Key field. Then
the private key never leaves the router.
Repeat for the second PoP. When both are up you will see something like this:
Before you leave the portal, collect these from each tunnel's detail page and the email it sent. You need all of them in section 7 and 8.
| Value | Placeholder |
|---|---|
| PoP hostname or IP, and UDP port | <POP_A_ENDPOINT> <POP_A_PORT> |
| PoP WireGuard public key | <POP_A_PUBKEY> |
| Your WireGuard private key (or the one you generated) | <WG_A_PRIVKEY> |
| Preshared key, if you generated one | <POP_A_PSK> |
| Your address on the tunnel | <TUNNEL_A_IP> |
| The PoP's address on the tunnel (your BGP neighbor) | <POP_A_PEER_IP> |
| The PoP's AS number | <POP_ASN> |
| Your ASN and your prefix | <YOUR_ASN> <YOUR_44NET_PREFIX> |
The neighbor address and the PoP's AS number come from the tunnel's own detail page. For a BGP tunnel the portal generates a ready-made BIRD configuration containing both. Read the numbers out of that block even if you are configuring MikroTik; they are the same numbers.
Work on the bench, with the router isolated from anything that matters. The sequence below is the one I actually use, and the order is deliberate: the router's own firewall gets sorted out before the address plan changes underneath you, so you never end up locked out of a box you can still hear humming.
/system reset-configuration). You want the factory defaults present, not a blank router.192.168.88.1, user admin,
no password. Set a password when it asks.On a device with a console port, plug in the serial cable now (115200 8N1). On one without, keep WinBox handy: connecting to the router's MAC address rather than its IP works even when the IP configuration is broken. You will want one of these before the evening is out.
Before you touch the firewall or the routing table, turn on safe mode: Ctrl-X in the terminal, or the Safe Mode button in WinBox. The prompt changes to show you are in it. Every change you make from that point is held provisionally, and if your session drops or the router stops hearing from you, everything done in that session is rolled back automatically and the router returns to the state it was in when you enabled it. Press Ctrl-X again to leave safe mode and keep the changes.
It is not magic. The undo buffer is finite, so very long sessions can overflow it, and it only protects you from losing contact with the router. A change that works perfectly and is completely wrong will be committed just the same. Use it anyway: locking yourself out with a firewall rule is the single most common way to lose an evening on this build.
The bridge is about to become a public 44net segment, so management has to move off it. In this design ether1 faces your ordinary home LAN, which is where you will manage the router from. Open the input chain for management on ether1 first, confirm it works, and only then take the old LAN apart.
/ip firewall filter print # Remove rules you do not want, in particular the fasttrack rule. Fasttrack # short-circuits connection processing, which skips the MSS clamping and the # policy routing this build depends on. It must not be present. /ip firewall filter remove [find action=fasttrack-connection] /ip firewall filter add chain=input in-interface=ether1 protocol=icmp action=accept add chain=input in-interface=ether1 protocol=tcp dst-port=22 action=accept add chain=input in-interface=ether1 protocol=tcp dst-port=8291 action=accept add chain=input connection-state=established,related action=accept add chain=input action=drop # Order matters. Use "print" then "move X Y" until the accepts sit above the drop. /ip address print # note the DHCP address ether1 picked up from your LAN
Now move to your normal workstation, SSH to that ether1 address, and confirm you are in. Do not proceed until that works.
/system identity set name=<ROUTER_NAME> /system clock set time-zone-name=<YOUR_TIMEZONE> /system ntp client set enabled=yes servers=pool.ntp.org # Turn off things this box has no business doing /ipv6 settings set disable-ipv6=yes /disk settings set auto-media-sharing=no auto-smb-sharing=no # Remove the factory 192.168.88.0/24 LAN. Check each index with "print" # before removing; the numbering is not guaranteed to match this listing. /ip address remove 0 /ip pool remove 0 /ip dhcp-server remove 0 /ip dhcp-server network remove 0 /ip dns static remove 0 /ip firewall nat remove 0 # Management discovery belongs on the ether1 side, not the public segment /tool mac-server set allowed-interface-list=WAN /tool mac-server mac-winbox set allowed-interface-list=WAN # IPv6 off means IPv6 shut, not IPv6 ignored /ipv6 firewall filter print /ipv6 firewall filter remove [find] /ipv6 firewall filter add chain=input action=drop /ipv6 firewall filter add chain=forward action=drop # Upgrade before you build anything on it /system package update check-for-updates /system package update download # after the reboot, bring the bootloader along too /system routerboard upgrade # Read the whole configuration back and check it says what you meant /export show-sensitive
/export after every milestone, and read it. Nearly every mistake in this build is visible
in an export and invisible from a status screen. Save the exports somewhere off the router, and note that
show-sensitive includes your private keys, so treat that output like a password file.
Two interfaces on two different listen ports, one peer each, one address each. The listen port matters less than you might think, because your router always initiates, but giving each tunnel its own port keeps the firewall rules readable.
# Both listeners, on the ether1 side only. Check placement with "print" and # "move" so these land above the input chain's drop rule. /ip firewall filter add chain=input action=accept protocol=udp dst-port=<WG_PORT_A> in-interface=<WAN_IF> \ comment="Allow WireGuard A" add chain=input action=accept protocol=udp dst-port=<WG_PORT_B> in-interface=<WAN_IF> \ comment="Allow WireGuard B"
# --- tunnel A ---------------------------------------------------------- /interface wireguard add name=ampr-pop-a listen-port=<WG_PORT_A> mtu=1380 \ private-key="<WG_A_PRIVKEY>" comment="<POP_A_NAME> 44net WireGuard tunnel" /interface wireguard peers add name=pop-a interface=ampr-pop-a \ public-key="<POP_A_PUBKEY>" preshared-key="<POP_A_PSK>" \ endpoint-address=<POP_A_ENDPOINT> endpoint-port=<POP_A_PORT> \ allowed-address=0.0.0.0/0 persistent-keepalive=20s \ comment="<POP_A_NAME> PoP server peer" /ip address add address=<TUNNEL_A_IP>/24 network=<TUNNEL_A_NETWORK> interface=ampr-pop-a # --- tunnel B, to a PoP in a different city ---------------------------- /interface wireguard add name=ampr-pop-b listen-port=<WG_PORT_B> mtu=1380 \ private-key="<WG_B_PRIVKEY>" comment="<POP_B_NAME> 44net WireGuard tunnel" /interface wireguard peers add name=pop-b interface=ampr-pop-b \ public-key="<POP_B_PUBKEY>" preshared-key="<POP_B_PSK>" \ endpoint-address=<POP_B_ENDPOINT> endpoint-port=<POP_B_PORT> \ allowed-address=0.0.0.0/0 persistent-keepalive=20s \ comment="<POP_B_NAME> PoP server peer" /ip address add address=<TUNNEL_B_IP>/24 network=<TUNNEL_B_NETWORK> interface=ampr-pop-b # Group them so later rules can say "the tunnels" instead of naming both /interface list add name=44NET /interface list member add list=44NET interface=ampr-pop-a add list=44NET interface=ampr-pop-b
The portal shows your tunnel address as a /32, for example 44.x.x.x/32. On
RouterOS it is easier to configure it with the PoP's own subnet mask, so the router builds a connected route
toward the far end and the BGP session has somewhere to go. Both of these work:
address=<TUNNEL_A_IP>/24 network=<TUNNEL_A_NETWORK>, where the network is the
PoP subnet the address came out of, for example the x.x.x.0 of your tunnel address. This is
what the generated configurations from the portal do, and it is what I run.address=<TUNNEL_A_IP>/32 network=<POP_A_PEER_IP>, the strict point to point
form, which creates a host route to the neighbor and nothing else.What does not work is a bare /32 with no network=, because then the router has
no route to its BGP neighbor and the session never leaves idle.
1380 is a deliberately conservative interface MTU that survives most paths without
fragmenting. Whatever you pick, the TCP maximum segment size that goes with it is MTU minus 40. For
an MTU of 1380 that is 1340.
/ip firewall mangle
add chain=forward action=change-mss new-mss=1340 protocol=tcp tcp-flags=syn \
tcp-mss=1341-65535 out-interface=ampr-pop-a passthrough=yes \
comment="MSS clamping for WireGuard A"
add chain=forward action=change-mss new-mss=1340 protocol=tcp tcp-flags=syn \
tcp-mss=1341-65535 out-interface=ampr-pop-b passthrough=yes \
comment="MSS clamping for WireGuard B"
Setting new-mss equal to the interface MTU is an easy slip and it produces exactly the
symptom clamping is supposed to prevent: a 1380 byte segment plus 40 bytes of headers is a 1420 byte
packet on a 1380 byte link. Subtract the 40. new-mss=clamp-to-pmtu does the arithmetic for
you if you would rather not think about it.
/interface/wireguard/peers/print detail
# last-handshake within the last couple of minutes, rx and tx climbing
/ping <POP_A_PEER_IP> count=5
/ping <POP_B_PEER_IP> count=5
Almost always one of: the endpoint address or port typed wrong, a public key pasted with a character missing, a preshared key set on one side only, or the input chain dropping your own return traffic. WireGuard is silent by design and will not tell you which. Check them in that order.
BGP has two jobs here. It tells each PoP to send traffic for your prefix down your tunnel, and it learns back a default route plus the rest of 44Net. All of that lands in a routing table of its own, which is what keeps it from disturbing anything else on the router.
Three small objects have to exist before a session is worth starting. The routing table holds everything BGP learns. The address list names the one prefix you are willing to advertise. The static route gives the router an active path for that prefix in that table, which is the condition RouterOS puts on advertising it.
# Exactly one entry, and it must be the network address of your block. # A host address with a prefix length on the end, like x.x.x.49/28, will # not match and your announcement will silently never happen. /ip firewall address-list add list=44net-connect-prefixes address=<YOUR_44NET_PREFIX> comment="my allocation" /routing table add name=44net-connect fib # The origin. Without a matching active route in this table, output.network # has nothing to advertise. The connected route from /ip address lives in # main, not here, so this line is not optional. /ip route add dst-address=<YOUR_44NET_PREFIX> gateway=bridge routing-table=44net-connect \ comment="BGP origin for our allocation"
/routing bgp instance
add name=44net-connect as=<YOUR_ASN> router-id=<YOUR_44NET_GATEWAY> \
routing-table=44net-connect
/routing bgp connection
add name=bgp-pop-a instance=44net-connect as=<YOUR_ASN> \
local.address=<TUNNEL_A_IP> local.role=ebgp \
remote.address=<POP_A_PEER_IP>/32 \
routing-table=44net-connect output.network=44net-connect-prefixes \
connect=yes listen=no keepalive-time=1m disabled=no
add name=bgp-pop-b instance=44net-connect as=<YOUR_ASN> \
local.address=<TUNNEL_B_IP> local.role=ebgp \
remote.address=<POP_B_PEER_IP>/32 \
routing-table=44net-connect output.network=44net-connect-prefixes \
connect=yes listen=no keepalive-time=1m disabled=no
Three details in there are worth understanding rather than copying:
remote.as. Leave it unset and RouterOS accepts whatever AS number the PoP
presents, which saves you hunting for it and survives the far end renumbering. Set it if you want the
session to refuse anything else.connect=yes listen=no. Your router always initiates. That is why the session works
without an input chain rule for TCP 179: the replies come back as an established connection. If you
would rather the PoP be able to initiate, add an accept for port 179 from the neighbor address.output.network is the whole advertisement policy. Only prefixes on that address
list can leave this router. If you want belt and braces, add an output filter chain as well, as below.The address list is the guard rail. Keep exactly one entry in it, keep it equal to your own allocation, and check it after any change. A router announcing somebody else's prefix is how people lose their access to the service.
/routing filter rule
add chain=44net-out comment="announce only our allocation" \
rule="if (dst == <YOUR_44NET_PREFIX>) { accept } reject"
add chain=44net-in comment="accept a default and 44net space, nothing else" \
rule="if (dst == 0.0.0.0/0 || dst in 44.0.0.0/9 || dst in 44.128.0.0/10) { accept } reject"
/routing bgp connection
set [find name=bgp-pop-a] input.filter=44net-in output.filter-chain=44net-out
set [find name=bgp-pop-b] input.filter=44net-in output.filter-chain=44net-out
The chain runs against every route the PoP sends you, before anything is installed in the
44net-connect table. Clause by clause:
| Clause | Effect |
|---|---|
dst == 0.0.0.0/0 | Accepts the default route the PoP offers. This is the line that gives your 44net hosts the general internet: anything they send to a non-44 destination follows this route out through the tunnel, onto the internet, with its 44 address intact. |
dst in 44.0.0.0/9dst in 44.128.0.0/10 | Accepts the two blocks that make up 44Net, so traffic between you and other amateur networks follows a specific route rather than the default. Without these you would still reach 44net through the default route, but you would lose the ability to see and prefer specific paths. |
reject | Discards everything else. If a PoP ever leaked a full internet table, or somebody else's space, none of it would reach your routing table. |
Because all of this lands in 44net-connect and nowhere else, accepting a default route here
is safe. Only traffic that a routing rule deliberately sent to that table can use it.
Some operators want the segment to be purely an amateur network: reachable from 44Net, able to reach 44Net, with no path to or from the commercial internet at all. Drop the default clause:
/routing filter rule
add chain=44net-in-strict comment="44net destinations only, no default route" \
rule="if (dst in 44.0.0.0/9 || dst in 44.128.0.0/10) { accept } reject"
/routing bgp connection
set [find name=bgp-pop-a] input.filter=44net-in-strict
set [find name=bgp-pop-b] input.filter=44net-in-strict
Be clear about what that buys and what it costs, because it is more absolute than it first looks:
1.1.1.1 over DHCP, nothing on the
segment resolves anything. Point hosts at a resolver inside 44Net instead, or at the router itself.If what you actually want is 44net reachability plus a normal internet connection for the hosts, do not do this. Give those machines a second interface on your ordinary LAN and keep the 44 addresses for the services you are publishing.
Left alone, both paths look equally good and the choice can move on its own. If you would rather it be deliberate, raise the local preference on routes learned from your preferred PoP so your outbound traffic favours it, and prepend your AS a couple of times toward the other PoP so inbound traffic favours the same one. When the primary fails, both preferences stop mattering and traffic moves anyway.
/routing filter rule
add chain=44net-in-b comment="learned from the backup: less preferred" \
rule="if (dst == 0.0.0.0/0 || dst in 44.0.0.0/9 || dst in 44.128.0.0/10) \
{ set bgp-local-pref 90; accept } reject"
add chain=44net-out-b comment="announce to the backup with prepends" \
rule="if (dst == <YOUR_44NET_PREFIX>) { set bgp-path-prepend 2; accept } reject"
/routing bgp connection
set [find name=bgp-pop-b] input.filter=44net-in-b output.filter-chain=44net-out-b
/routing/bgp/session/print detail # established, with a prefix count in both directions /ip/route/print where routing-table=44net-connect # a default route from the PoPs, plus 44net prefixes /routing/bgp/advertisements/print # your prefix going out, and only your prefix
The addresses are yours and the sessions are up. What remains is to put the block on a physical segment, and then to make sure traffic sourced from it uses the tunnels while everything else carries on as before.
A /28 gives sixteen addresses and thirteen usable hosts once you account for the network
address, the broadcast address and the router. Worked through for a block ending in .48/28:
| Address | Use |
|---|---|
x.x.x.48 | Network address, not assignable |
x.x.x.49 | The router, and the default gateway for the segment |
x.x.x.50 to x.x.x.62 | Hosts: servers, nodes, gateways |
x.x.x.63 | Broadcast, not assignable |
255.255.255.240 | Subnet mask, wildcard 0.0.0.15 |
The portal's Subnet Calculator under Network Tools will do this for whatever size you were given.
# The factory bridge already has ether2 through ether5 and sfp1 on it. /ip address add address=<YOUR_44NET_GATEWAY>/<PREFIX_LEN> interface=bridge \ comment="Address for subnet <YOUR_44NET_PREFIX>" # DHCP is convenient for laptops and test hosts. Anything you intend to # publish a service on should be statically addressed instead. /ip pool add name=44net-pool ranges=<FIRST_DHCP_HOST>-<LAST_DHCP_HOST> /ip dhcp-server add name=44net-dhcp interface=bridge address-pool=44net-pool disabled=no /ip dhcp-server network add address=<YOUR_44NET_PREFIX> gateway=<YOUR_44NET_GATEWAY> dns-server=<DNS_SERVERS>
A DHCP client that plugs into this bridge gets a globally routable address with no NAT in front of it. That is the point of the exercise, and it is also why section 11 is not optional. Do not use this segment as a general purpose LAN, and do not bridge it to your household network.
Two routing rules, in this order. The first keeps your own LAN reachable through the main table, so you do not lose management or the path between your household and the 44net segment. The second sends anything sourced from your 44net block into the BGP table, where the default route learned from the PoPs is waiting.
/routing rule
add action=lookup dst-address=<HOME_LAN_PREFIX> table=main \
comment="keep the local LAN reachable through the main table"
add action=lookup-only-in-table src-address=<YOUR_44NET_PREFIX> table=44net-connect \
comment="44net hosts egress through the tunnels"
It is tempting to write that first rule as 192.0.0.0/8 because your LAN is a 192.168
address. Do not. 192.0.0.0/8 is mostly ordinary public internet space, including large blocks
in daily use, and diverting all of it into the main table means your 44net hosts try to reach those sites
through your ISP with a 44 source address, which fails in a way that is very hard to spot. Use
192.168.0.0/16, or better, the exact prefix your own LAN uses. The same care applies to any
firewall rule you write for local traffic.
The factory masquerade rule was removed back in section 6, and it must stay removed. If you add NAT rules for any other reason later, make sure they cannot match your 44net block:
/ip firewall nat
add chain=srcnat src-address=<YOUR_44NET_PREFIX> action=accept place-before=0 \
comment="never NAT 44net space"
Fasttrack stays off for the same class of reason. A fasttracked connection skips the rest of the processing path, including the mangle rules doing MSS clamping, which turns a working tunnel into an intermittently working one.
Forward and reverse DNS for 44Net space is managed through the 44Net Portal rather than the Connect portal. If you are going to run services, set the records up once the addresses are live. Plenty of mail and IRC servers care about matching reverse DNS.
If you enabled allow-remote-requests on the router's own resolver, remember that the 44net
segment is public: serve DNS to it deliberately or not at all, and never expose the resolver to the tunnels.
Work outward. Each layer only makes sense once the one below it is solid.
| Layer | Test | What good looks like |
|---|---|---|
| 1. Tunnel | /interface/wireguard/peers/print detail | A handshake within the last couple of minutes on both peers |
| 2. Link | /ping <POP_A_PEER_IP> | Replies, with stable latency |
| 3. Session | /routing/bgp/session/print detail | Established on both |
| 4. Learned | /ip/route/print where routing-table=44net-connect | A default route and 44net prefixes, in that table only |
| 5. Announced | /routing/bgp/advertisements/print | Your prefix, and only your prefix |
| 6. Main table clean | /ip/route/print where routing-table=main | No default route from the PoPs anywhere near it |
| 7. Portal | Network Tools, then My Network | Both tunnels drawn against their PoPs |
| 8. Outbound | From a host on the 44net segment, browse to any site that echoes your address back | Your 44 address, not your ISP address |
| 9. Inbound | From a phone on mobile data, ping a 44net host | Replies |
| 10. Failover | See below | A few seconds of loss, then normal |
Do it at both layers, because they fail differently. Killing the BGP session leaves the tunnel up and tests route withdrawal. Killing the interface tests the whole path. Run each with a continuous ping going from a 44net host to somewhere outside.
# BGP layer /routing bgp connection disable bgp-pop-a /routing bgp connection enable bgp-pop-a /routing bgp connection disable bgp-pop-b /routing bgp connection enable bgp-pop-b # Tunnel layer /interface wireguard disable ampr-pop-a /interface wireguard enable ampr-pop-a /interface wireguard disable ampr-pop-b /interface wireguard enable ampr-pop-b
The backup path is the one that is never exercised and therefore never known to work. Run the drill when you build it, and again any time you change the configuration.
There is no NAT here and no accidental protection. Every address in your block is directly reachable from every scanner on the planet, and they will find you within minutes of the prefix going live. Worse, RouterOS defaults the forward chain policy to accept, so a firewall that merely lacks a rule is a firewall that lets everything through. Both chains need an explicit drop at the end.
Management lives on the ether1 side. The 44net segment gets nothing except what it genuinely needs, which is DHCP and ICMP, and the tunnels get nothing at all beyond the replies to conversations the router started.
/ip firewall filter
add chain=input action=accept connection-state=established,related,untracked \
comment="established and related"
add chain=input action=drop connection-state=invalid
add chain=input action=accept protocol=icmp limit=20,10:packet comment="icmp, rate limited"
add chain=input action=accept dst-address=127.0.0.1
# WireGuard listeners, from the ether1 side only
add chain=input action=accept protocol=udp dst-port=<WG_PORT_A>,<WG_PORT_B> \
in-interface=<WAN_IF> comment="WireGuard"
# Management, from your own network only
add chain=input action=accept protocol=tcp dst-port=22,8291 \
in-interface=<WAN_IF> src-address=<MGMT_PREFIX> comment="ssh and winbox"
# The bridge is a public segment now. It gets DHCP and nothing else.
add chain=input action=accept in-interface=bridge protocol=udp dst-port=67 \
comment="DHCP for 44net hosts"
add chain=input action=drop in-interface=bridge \
comment="no router services from the public segment"
# Optional, only if you want the PoP to be able to initiate the session
#add chain=input action=accept protocol=tcp dst-port=179 src-address=<POP_A_PEER_IP>
#add chain=input action=accept protocol=tcp dst-port=179 src-address=<POP_B_PEER_IP>
add chain=input action=drop comment="default deny"
The default configuration ends its input chain with drop all not coming from LAN, and the
bridge is a member of the LAN interface list. The moment that bridge becomes your public 44net segment,
that rule is welcoming every host on it to SSH, WinBox and the DNS resolver. Either take the bridge out of
the LAN list, or do what the rules above do and drop input from the bridge explicitly, above the LAN
accept.
Start closed and open exactly what you intend to publish. A pair of rules that accept everything between the bridge and the tunnels is quick to write and leaves every host on the segment fully exposed on every port, which is almost never what you want.
/ip firewall filter add chain=forward action=accept connection-state=established,related,untracked add chain=forward action=drop connection-state=invalid # Your 44net hosts may reach out to anything add chain=forward action=accept src-address=<YOUR_44NET_PREFIX> \ comment="44net hosts outbound" # Your own LAN may reach the 44net segment add chain=forward action=accept src-address=<HOME_LAN_PREFIX> \ dst-address=<YOUR_44NET_PREFIX> comment="home LAN to 44net" # Inbound from the world: ICMP, then only the services you mean to publish add chain=forward action=accept protocol=icmp dst-address=<YOUR_44NET_PREFIX> \ limit=20,10:packet comment="icmp to 44net hosts" add chain=forward action=accept protocol=tcp dst-port=80,443 \ dst-address=<YOUR_SERVER_IP> comment="web on one host" add chain=forward action=drop comment="default deny"
SSH on a public address collects thousands of login attempts a day. In rough order of preference:
PasswordAuthentication no, root login
disabled, a non-standard port to cut the noise, and something like fail2ban to shun repeat
offenders.A non-standard port is noise reduction, not security. The key-only rule is the one that matters. The same logic applies to port 80 and 443: publishing them is fine, publishing them to a host running something you have not patched since installation is not.
/ip service set telnet disabled=yes set ftp disabled=yes set www disabled=yes set api disabled=yes set api-ssl disabled=yes set ssh port=<SSH_PORT> address=<MGMT_PREFIX> set winbox address=<MGMT_PREFIX> # Discovery and MAC access on the management side only /ip neighbor discovery-settings set discover-interface-list=WAN /tool mac-server set allowed-interface-list=WAN /tool mac-server mac-winbox set allowed-interface-list=WAN # A real user, and no default admin /user add name=<ADMIN_USER> group=full password="<STRONG_PASSWORD>" /user disable admin # Log somewhere you will actually look /system logging action set remote remote=<SYSLOG_SERVER> /system logging add topics=critical,error,warning action=remote
A WireGuard private key is a password: anyone holding it can be your router. Do not paste one into a
chat window, a ticket, a forum post or a document you intend to publish, and if you ever do, rotate it.
Rotating means generating a new keypair on the router and updating the public key in the portal, which
takes about two minutes. /export show-sensitive output deserves the same care.
/export, and keep a copy off the router.| Symptom | Usual cause |
|---|---|
| No WireGuard handshake at all | Endpoint host or port wrong; public key mistyped; preshared key on one side only; the input chain dropping your own return traffic |
| Handshake fine, cannot ping the PoP address | The tunnel address has no network=, so there is no route to the far end |
| BGP stuck in idle or connect | Neighbor address wrong; local.address not set to your tunnel address; no route to the neighbor; the session pointed at the wrong routing table |
| Session established, nothing advertised | The address list entry is a host address rather than the network address; the address list name does not match output.network; no active route for the prefix in the BGP routing table |
| Session established, no routes received | An input filter that is too strict, or the PoP has not attached your network to that tunnel in the portal |
| Prefix advertised but hosts unreachable from outside | Give it a few minutes; then check the portal shows BGP and your prefix on the tunnel; then check the forward chain, which may have no rule permitting inbound to that host |
| 44net hosts reach 44net but not the wider internet | Missing routing rule; the default route from the PoP did not arrive; a NAT rule catching the block |
| Some sites work from 44net hosts and some do not | An over-broad local-traffic rule such as 192.0.0.0/8 diverting real public destinations into the main table |
| Ping works, web and SSH hang | MTU and MSS. Check that the clamp is 40 below the interface MTU, and that fasttrack is not bypassing the mangle rule |
| Throughput is fine at first, then collapses | Fasttrack re-enabled by a configuration restore, or a rule reordered above the mangle chain |
| Everything worked, then stopped after a reboot | A rule or route left disabled, or configuration that was never exported and got lost in a restore |
| Both tunnels up but the path keeps changing | No local preference or prepend policy. Make one path deliberately preferred (section 8.3) |
| Locked out of the router | Serial console, or WinBox to the MAC address. This is why you set one of them up in section 6 |
The whole build in order, as it runs on a hEX S 2025 under RouterOS 7.20. Replace every placeholder before running any of it, and work down the file rather than pasting it in one go: the firewall section changes what you can reach the router on.
# ====================================================================== # 44Net Connect: dual WireGuard tunnel, BGP-routed subnet # Dedicated router behind the existing home firewall. # ether1 = management and tunnel transport, DHCP from the home LAN # bridge = the public 44net segment # ====================================================================== # --- identity and housekeeping ---------------------------------------- /system identity set name=<ROUTER_NAME> /system clock set time-zone-name=<YOUR_TIMEZONE> /system ntp client set enabled=yes servers=pool.ntp.org /ipv6 settings set disable-ipv6=yes /disk settings set auto-media-sharing=no auto-smb-sharing=no # --- remove the factory LAN (check indexes with print first) ----------- /ip address remove 0 /ip pool remove 0 /ip dhcp-server remove 0 /ip dhcp-server network remove 0 /ip dns static remove 0 /ip firewall nat remove 0 /ip firewall filter remove [find action=fasttrack-connection] # --- shut IPv6 rather than ignoring it --------------------------------- /ipv6 firewall filter remove [find] /ipv6 firewall filter add chain=input action=drop /ipv6 firewall filter add chain=forward action=drop # --- WireGuard --------------------------------------------------------- /interface wireguard add name=ampr-pop-a listen-port=<WG_PORT_A> mtu=1380 \ private-key="<WG_A_PRIVKEY>" comment="<POP_A_NAME> 44net WireGuard tunnel" add name=ampr-pop-b listen-port=<WG_PORT_B> mtu=1380 \ private-key="<WG_B_PRIVKEY>" comment="<POP_B_NAME> 44net WireGuard tunnel" /interface wireguard peers add name=pop-a interface=ampr-pop-a public-key="<POP_A_PUBKEY>" \ preshared-key="<POP_A_PSK>" endpoint-address=<POP_A_ENDPOINT> \ endpoint-port=<POP_A_PORT> allowed-address=0.0.0.0/0 persistent-keepalive=20s \ comment="<POP_A_NAME> PoP server peer" add name=pop-b interface=ampr-pop-b public-key="<POP_B_PUBKEY>" \ preshared-key="<POP_B_PSK>" endpoint-address=<POP_B_ENDPOINT> \ endpoint-port=<POP_B_PORT> allowed-address=0.0.0.0/0 persistent-keepalive=20s \ comment="<POP_B_NAME> PoP server peer" /interface list add name=44NET /interface list member add list=44NET interface=ampr-pop-a add list=44NET interface=ampr-pop-b # --- addressing -------------------------------------------------------- /ip address add address=<TUNNEL_A_IP>/24 network=<TUNNEL_A_NETWORK> interface=ampr-pop-a add address=<TUNNEL_B_IP>/24 network=<TUNNEL_B_NETWORK> interface=ampr-pop-b add address=<YOUR_44NET_GATEWAY>/<PREFIX_LEN> interface=bridge \ comment="Address for subnet <YOUR_44NET_PREFIX>" # --- MSS clamping (MTU minus 40) --------------------------------------- /ip firewall mangle add chain=forward action=change-mss new-mss=1340 protocol=tcp tcp-flags=syn \ tcp-mss=1341-65535 out-interface=ampr-pop-a passthrough=yes comment="MSS clamp A" add chain=forward action=change-mss new-mss=1340 protocol=tcp tcp-flags=syn \ tcp-mss=1341-65535 out-interface=ampr-pop-b passthrough=yes comment="MSS clamp B" # --- DHCP for the 44net segment (optional) ----------------------------- /ip pool add name=44net-pool ranges=<FIRST_DHCP_HOST>-<LAST_DHCP_HOST> /ip dhcp-server add name=44net-dhcp interface=bridge address-pool=44net-pool disabled=no /ip dhcp-server network add address=<YOUR_44NET_PREFIX> \ gateway=<YOUR_44NET_GATEWAY> dns-server=<DNS_SERVERS> # --- BGP --------------------------------------------------------------- /ip firewall address-list add list=44net-connect-prefixes address=<YOUR_44NET_PREFIX> comment="my allocation" /routing table add name=44net-connect fib /ip route add dst-address=<YOUR_44NET_PREFIX> gateway=bridge routing-table=44net-connect \ comment="BGP origin for our allocation" /routing bgp instance add name=44net-connect as=<YOUR_ASN> router-id=<YOUR_44NET_GATEWAY> \ routing-table=44net-connect /routing bgp connection add name=bgp-pop-a instance=44net-connect as=<YOUR_ASN> \ local.address=<TUNNEL_A_IP> local.role=ebgp remote.address=<POP_A_PEER_IP>/32 \ routing-table=44net-connect output.network=44net-connect-prefixes \ connect=yes listen=no keepalive-time=1m disabled=no add name=bgp-pop-b instance=44net-connect as=<YOUR_ASN> \ local.address=<TUNNEL_B_IP> local.role=ebgp remote.address=<POP_B_PEER_IP>/32 \ routing-table=44net-connect output.network=44net-connect-prefixes \ connect=yes listen=no keepalive-time=1m disabled=no # --- policy routing ---------------------------------------------------- /routing rule add action=lookup dst-address=<HOME_LAN_PREFIX> table=main \ comment="keep the local LAN in the main table" add action=lookup-only-in-table src-address=<YOUR_44NET_PREFIX> table=44net-connect \ comment="44net hosts egress through the tunnels" # --- firewall: input --------------------------------------------------- /ip firewall filter 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 limit=20,10:packet add chain=input action=accept dst-address=127.0.0.1 add chain=input action=accept protocol=udp dst-port=<WG_PORT_A>,<WG_PORT_B> \ in-interface=<WAN_IF> comment="WireGuard" add chain=input action=accept protocol=tcp dst-port=22,8291 in-interface=<WAN_IF> \ src-address=<MGMT_PREFIX> comment="ssh and winbox" add chain=input action=accept in-interface=bridge protocol=udp dst-port=67 \ comment="DHCP for 44net hosts" add chain=input action=drop in-interface=bridge \ comment="no router services from the public segment" add chain=input action=drop comment="default deny" # --- firewall: forward ------------------------------------------------- add chain=forward action=accept connection-state=established,related,untracked add chain=forward action=drop connection-state=invalid add chain=forward action=accept src-address=<YOUR_44NET_PREFIX> \ comment="44net hosts outbound" add chain=forward action=accept src-address=<HOME_LAN_PREFIX> \ dst-address=<YOUR_44NET_PREFIX> comment="home LAN to 44net" add chain=forward action=accept protocol=icmp dst-address=<YOUR_44NET_PREFIX> \ limit=20,10:packet add chain=forward action=accept protocol=tcp dst-port=80,443 \ dst-address=<YOUR_SERVER_IP> comment="web on one host" add chain=forward action=drop comment="default deny" # --- NAT guard --------------------------------------------------------- /ip firewall nat add chain=srcnat src-address=<YOUR_44NET_PREFIX> action=accept place-before=0 \ comment="never NAT 44net space" # --- management surface ------------------------------------------------ /ip service set telnet disabled=yes set ftp disabled=yes set www disabled=yes set api disabled=yes set api-ssl disabled=yes set ssh port=<SSH_PORT> address=<MGMT_PREFIX> set winbox address=<MGMT_PREFIX> /ip neighbor discovery-settings set discover-interface-list=WAN /tool mac-server set allowed-interface-list=WAN /tool mac-server mac-winbox set allowed-interface-list=WAN /user add name=<ADMIN_USER> group=full password="<STRONG_PASSWORD>" /user disable admin # --- save and check ---------------------------------------------------- /export file=44net-<YYYYMMDD>
RouterOS evaluates each chain top to bottom and appends new rules at the end, so rules added later in
a session land below rules added earlier. After pasting, run /ip firewall filter print and
use move until each chain reads the way it does above, with the default deny last.
| Placeholder | What it is | Where you get it |
|---|---|---|
| <YOUR_ASN> | Your autonomous system number | Portal, then ASNs |
| <YOUR_44NET_PREFIX> | Your allocation, network address and length, e.g. 44.x.x.48/28 | Portal, then Networks |
| <PREFIX_LEN> | The mask length on its own, e.g. 28 | Same |
| <YOUR_44NET_GATEWAY> | First usable address in your block, used on the bridge and as the BGP router ID | You choose |
| <FIRST_DHCP_HOST> <LAST_DHCP_HOST> | The DHCP range inside your block | You choose |
| <YOUR_SERVER_IP> | A host you are publishing a service on | You choose |
| <POP_A_NAME> | The PoP node name, useful in comments | Portal, e.g. a city edge node |
| <POP_A_ENDPOINT> <POP_A_PORT> | PoP host and UDP port for the tunnel | Tunnel detail page |
| <POP_A_PUBKEY> | PoP WireGuard public key | Tunnel detail page |
| <POP_A_PSK> | Preshared key, if you generated one | The tunnel request form |
| <WG_A_PRIVKEY> | Your WireGuard private key for that tunnel | Shown once at creation, or generated on the router |
| <WG_PORT_A> | Local UDP listen port, e.g. 51820 | You choose |
| <TUNNEL_A_IP> | Your address on the tunnel | Tunnel detail page |
| <TUNNEL_A_NETWORK> | The PoP subnet that address came from, e.g. its x.x.x.0 | Derived from the tunnel address |
| <POP_A_PEER_IP> | The PoP's address on the tunnel, your BGP neighbor | Tunnel detail page, or the generated BIRD config |
| and the same set again with B for the second tunnel. | ||
| <WAN_IF> | The interface facing your existing LAN, usually ether1 | Your router |
| <HOME_LAN_PREFIX> | Your existing private LAN, e.g. 192.168.0.0/16 or narrower | Your network |
| <MGMT_PREFIX> | The addresses you manage the router from | Your network |
| <SSH_PORT> <ADMIN_USER> <STRONG_PASSWORD> | Management credentials | You choose |
| <ROUTER_NAME> <YOUR_TIMEZONE> <DNS_SERVERS> <SYSLOG_SERVER> | Local details | Your network |
44Net Connect is being actively developed and the portal changes month to month. If a screen here does not match what you see, trust the portal and let me know so the next revision is right.
This document, including its diagrams and example configurations, is released under the MIT License. Use it, republish it, adapt it for your club, translate it, or lift the configuration wholesale. Attribution is appreciated and not required.
44Net address space is administered by Amateur Radio Digital Communications and is for licensed amateur radio use. Nothing here is affiliated with or endorsed by ARDC.