FRRouting BGP on a VPS: Install, bgpd Config, Route-Maps
To run BGP on a VPS with FRRouting (FRR), you install the frr package, enable the bgpd daemon, define a neighbor, and attach prefix-lists and route-maps in both directions. The filters matter most. Without them, a typo can leak routes or accept junk from your upstream.
This guide walks through a minimal but production-minded FRR setup on a Linux VDS. It uses documentation ranges: AS64496 is your ASN, AS64500 is the upstream, and 203.0.113.0/24 is your prefix. Replace them with your real values.
What you need before you start
- A VPS or VDS with a BGP session offered by the provider. Check that it supports your use case, including whether the VM sees a full table or only provider routes.
- Your own ASN and IP block, with a matching RPKI ROA and IRR route object.
- The session parameters from your provider: neighbor IP, remote ASN, whether MD5 or multihop is required, and any communities.
- Root access and a firewall you control.
On a BGP-enabled VDS from Nixcon, ownership of the ASN and prefix is checked automatically against RPKI ROAs and RIR/IRR records. Your ASN is then added to the AS-NIXCON AS-SET. Get your ROA and route objects right first, and the rest goes smoothly. See the BGP-enabled VDS page for the available session options.
Step 1: Install FRR
On Debian or Ubuntu:
sudo apt update
sudo apt install frr frr-pythontools
On RHEL-compatible systems, use the distribution package if available, or the FRR project's official package repository. The package names and paths below stay the same.
Enable the BGP daemon in /etc/frr/daemons:
bgpd=yes
zebra=yes
Then restart FRR and confirm it's running:
sudo systemctl enable --now frr
sudo systemctl restart frr
sudo vtysh -c "show version"
FRR stores its configuration in /etc/frr/frr.conf. You can edit the file directly or use vtysh. Running vtysh and then write memory saves the running config to that file.
Step 2: Prepare the host
Your prefix needs to exist in the routing table before FRR will announce it. The usual approach is to add a blackhole route for the whole block and put the individual service IPs on the loopback:
# Service IP from your block, bound locally
sudo ip addr add 203.0.113.10/32 dev lo
In frr.conf, add a static blackhole for the aggregate. Packets for unused addresses in your block get dropped instead of looping back to the upstream:
ip route 203.0.113.0/24 blackhole
Open TCP port 179 only to your neighbor:
sudo nft add rule inet filter input ip saddr 198.51.100.1 tcp dport 179 accept
Adjust this to your existing firewall layout. If the default policy is drop, you must add the rule, or the session will hang in Active.
Step 3: Write prefix-lists
Prefix-lists define which routes you may send and which you will accept. Start with the outbound list, which is the one that protects other networks from you:
ip prefix-list PL-OUT-V4 seq 10 permit 203.0.113.0/24
ip prefix-list PL-OUT-V4 seq 999 deny 0.0.0.0/0 le 32
ipv6 prefix-list PL-OUT-V6 seq 10 permit 2001:db8:100::/48
ipv6 prefix-list PL-OUT-V6 seq 999 deny ::/0 le 128
Only your own prefixes are permitted. Everything else is denied, including routes you learn from the upstream. This prevents you from becoming accidental transit.
Next, the inbound list. If your session carries only the provider's routes, this is a sanity filter. If you receive a full table, it is essential:
ip prefix-list PL-IN-V4 seq 5 deny 203.0.113.0/24 le 32
ip prefix-list PL-IN-V4 seq 10 deny 0.0.0.0/0
ip prefix-list PL-IN-V4 seq 20 deny 0.0.0.0/8 le 32
ip prefix-list PL-IN-V4 seq 30 deny 10.0.0.0/8 le 32
ip prefix-list PL-IN-V4 seq 40 deny 100.64.0.0/10 le 32
ip prefix-list PL-IN-V4 seq 50 deny 127.0.0.0/8 le 32
ip prefix-list PL-IN-V4 seq 60 deny 169.254.0.0/16 le 32
ip prefix-list PL-IN-V4 seq 70 deny 172.16.0.0/12 le 32
ip prefix-list PL-IN-V4 seq 80 deny 192.168.0.0/16 le 32
ip prefix-list PL-IN-V4 seq 90 deny 198.18.0.0/15 le 32
ip prefix-list PL-IN-V4 seq 100 deny 224.0.0.0/3 le 32
ip prefix-list PL-IN-V4 seq 1000 permit 0.0.0.0/0 le 24
Notes on this list:
- Sequence 5 rejects your own prefix coming back to you.
- The last line accepts anything up to a /24, the common IPv4 acceptance limit. Longer prefixes are usually filtered by the wider internet anyway.
- In a real deployment, also deny the other reserved ranges (including the documentation blocks 192.0.2.0/24, 198.51.100.0/24, and 203.0.113.0/24) from the Internet table. Only the example block above is excluded so the lab values keep working.
For IPv6, apply the same idea: deny ::/0, deny 2001:db8::/32 le 128, deny ULA fc00::/7 le 128, deny link-local fe80::/10 le 128, and permit up to /48.
Step 4: Build route-maps
Prefix-lists only match. Route-maps apply the decision and let you change attributes:
route-map RM-UPSTREAM-OUT permit 10
match ip address prefix-list PL-OUT-V4
!
route-map RM-UPSTREAM-IN permit 10
match ip address prefix-list PL-IN-V4
set local-preference 100
!
route-map RM-UPSTREAM-OUT-V6 permit 10
match ipv6 address prefix-list PL-OUT-V6
A route-map ends with an implicit deny. Anything that doesn't match a permit clause is dropped. This is the behavior you want, but it also explains many "my route isn't announced" tickets.
You can add set as-path prepend 64496 64496 to the out map for a backup path, or set provider-defined communities. Community values are provider-specific, so check the documentation of the network you peer with.
Step 5: Configure bgpd
Now tie everything together under router bgp:
router bgp 64496
bgp router-id 198.51.100.2
no bgp default ipv4-unicast
bgp log-neighbor-changes
neighbor 198.51.100.1 remote-as 64500
neighbor 198.51.100.1 description UPSTREAM-1
neighbor 198.51.100.1 password CHANGE-ME
!
address-family ipv4 unicast
network 203.0.113.0/24
neighbor 198.51.100.1 activate
neighbor 198.51.100.1 soft-reconfiguration inbound
neighbor 198.51.100.1 maximum-prefix 100 warning-only
neighbor 198.51.100.1 route-map RM-UPSTREAM-IN in
neighbor 198.51.100.1 route-map RM-UPSTREAM-OUT out
exit-address-family
Key points:
no bgp default ipv4-unicastforces you to activate each neighbor per address family explicitly. This avoids surprises when you add IPv6.network 203.0.113.0/24only works because the blackhole route from Step 2 exists in the RIB.- Set
maximum-prefixto a value that fits what you expect to receive. A provider-routes-only session needs a much lower value than a full table. Switch fromwarning-onlyto a hard limit once you know the normal count. - Remove the
passwordline if your provider doesn't use MD5, and addebgp-multihoporupdate-sourceonly if the provider's instructions call for it.
For IPv6, add a second neighbor with the provider's v6 address and an address-family ipv6 unicast block using the RM-UPSTREAM-OUT-V6 route-map and the matching inbound filter.
Apply and save:
sudo vtysh -c "write memory"
sudo systemctl reload frr
Step 6: Verify the session
sudo vtysh -c "show bgp summary"
sudo vtysh -c "show bgp ipv4 unicast neighbors 198.51.100.1 advertised-routes"
sudo vtysh -c "show bgp ipv4 unicast neighbors 198.51.100.1 routes"
sudo vtysh -c "show bgp ipv4 unicast 203.0.113.0/24"
You want to see state Established (a number in the PfxRcd column) and your prefix listed under advertised routes. Then look at the outside view. A public looking glass shows what the rest of the network sees. Nixcon's looking glass at lg.nixconllc.com supports BGP route, ping, and traceroute queries from Izmir, Frankfurt, and the US.
After changing a policy, refresh without resetting the session:
sudo vtysh -c "clear bgp ipv4 unicast 198.51.100.1 soft in"
sudo vtysh -c "clear bgp ipv4 unicast 198.51.100.1 soft out"
Common mistakes and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Session stuck in Active or Connect | Port 179 blocked, wrong neighbor IP, or MD5 mismatch | Check firewall, ip route get, and the password on both sides |
| Established, but prefix not advertised | No matching route in the RIB, or the out route-map denies it | Add the blackhole route, check the prefix-list |
| Established, 0 prefixes received | Inbound policy denies everything, or provider isn't sending yet | Review PL-IN-V4, use show bgp ... received-routes with soft-reconfiguration |
(Policy) in the PfxRcd column |
eBGP without a route-map, or the map is rejecting everything | Attach inbound and outbound route-maps to the neighbor |
| Prefix announced but rejected upstream | Missing or wrong ROA or IRR route object | Fix your RPKI ROA and route object, including max-length |
| Traffic arrives but no replies | Return traffic exits with a source IP outside your block | Bind services to your block IPs and check policy routing |
| Config lost after reboot | Changes not saved | Run write memory and keep frr.conf in version control |
Hardening and operations tips
- Keep
frr.confunder version control and review changes withdiffbefore reloading. - Use
vtysh -forfrr-reload.pyfor controlled changes instead of retyping live configuration. - Monitor session state and prefix counts. Flow-based monitoring helps detect attacks before the session suffers; NixMon provides flow-based network monitoring and DDoS detection.
- Consider whether you need DDoS-protected announcements. Nixcon offers BGP sessions with or without DDoS protection, and optionally a full routing table; otherwise you receive only Nixcon routes.
Summary
FRR is a lightweight, well-documented routing suite that fits comfortably on a VPS. The workflow is always the same: install frr, enable bgpd, anchor your prefix in the RIB, write strict outbound and sensible inbound prefix-lists, wrap them in route-maps, and verify from both the router and a public looking glass.
Frequently asked questions
Can I run FRRouting on a regular VPS?
Technically yes, FRR runs on any modern Linux. A BGP session, however, requires that your provider offers one on the VPS or VDS and verifies your ASN and prefix. Without that, the daemon has no peer to connect to.
Why does FRR show (Policy) instead of a prefix count?
Modern FRR requires inbound and outbound policy on eBGP neighbors by default. Attach a route-map in each direction and the session will exchange routes. Disabling the requirement is possible but not recommended.
Why isn't my network statement announcing the prefix?
The bgpd network command only announces prefixes that exist in the routing table with an exact match. Add a static blackhole route for the aggregate, or make sure the prefix is installed from another source.
Do I need a ROA before I announce my block?
You should have one. Many networks drop RPKI-invalid routes, and a provider may validate ownership against RPKI and RIR/IRR records before enabling the session. Create the ROA and IRR route object with the correct origin ASN and max-length first.