Most BGP policy starts sensibly and ends as sediment. Someone needs a prefix preferred out of one site, so they write a route-map matching that prefix. Someone else needs a set of routes de-preferred toward a peer, so they write another. Two years later there are forty route-maps, each matching prefixes by hand, and no one can answer the simple question: *why does this route behave the way it does?*
BGP communities fix this, but only if you treat them as a deliberate scheme rather than a grab-bag of numbers. The pattern in this post — tag intent at the edge, act on tags in the core — has survived several redesigns of networks I’ve worked on precisely because it decouples *what you want* from *where you enforce it*. This is the version I keep coming back to.
## The core idea: separate marking from acting
A route-map that both matches a prefix *and* sets a local-preference is doing two jobs at once, and it welds policy to topology. The moment the prefix moves or a new site appears, every one of those route-maps is a place you can forget to update.
The scalable move is to split those jobs:
At the **edge**, where a route enters your AS, you decide what the route *means* and stamp a community on it. “This came from a transit provider.” “This is a customer route.” “Prefer this one.” “Blackhole this.”
In the **core and at exit points**, policy never looks at prefixes. It looks only at communities. “If it’s tagged transit, set local-pref 100.” “If it’s tagged blackhole, drop it.” “Don’t advertise customer-tagged routes to peers.”
Intent is written once, at ingress. Enforcement reads the tag anywhere. Add a site, and you configure only that site’s edge — the core already knows what to do with every community it will ever see.
## Designing the community namespace
Communities are 32-bit values written as `ASN:value`. The value half is yours to allocate, and allocating it thoughtfully is 80% of the benefit. Group communities by *category*, with ranges that leave room to grow. Assume your ASN is 65001:
“`text
Category Community Meaning
—————– ————– ————————————
Route source 65001:1000 Learned from a customer
65001:1001 Learned from a peer
65001:1002 Learned from a transit provider
65001:1003 Originated by us (internal)
Local preference 65001:2000 Set local-pref 50 (least preferred)
65001:2001 Set local-pref 100
65001:2002 Set local-pref 150
65001:2003 Set local-pref 200 (most preferred)
AS-path prepend 65001:3001 Prepend our ASN once to eBGP peers
65001:3002 Prepend twice
65001:3003 Prepend three times
Advertisement 65001:4000 Do not advertise to any peer/transit
65001:4001 Advertise to peers only
65001:4002 Advertise to transit only
Operational 65001:6666 Blackhole (drop this prefix)
“`
The categories matter more than the exact numbers. Source, preference, prepend, advertisement scope, operational actions — those five buckets cover the overwhelming majority of what real policy needs, and each has room for values you haven’t thought of yet. Write this table down somewhere permanent; it *is* your routing policy, in one page.
## Marking routes at ingress
Every eBGP session tags what it learns with a source community as the very first thing it does. Here’s a customer-facing session on IOS/IOS-XE:
“`text
ip community-list standard CUST permit 65001:1000
route-map FROM-CUSTOMER permit 10
set community 65001:1000 additive
!
router bgp 65001
neighbor 198.51.100.2 remote-as 64500
address-family ipv4 unicast
neighbor 198.51.100.2 route-map FROM-CUSTOMER in
“`
`additive` is the single most important keyword on that page. Without it, `set community` *replaces* any communities already present, silently erasing intent that another device set upstream. With `additive`, you’re layering tags, which is exactly the mental model — a route accumulates meaning as it travels.
A transit session tags its own source, and typically de-prefers by default:
“`text
route-map FROM-TRANSIT permit 10
set community 65001:1002 65001:2001 additive
!
router bgp 65001
neighbor 203.0.113.1 remote-as 174
address-family ipv4 unicast
neighbor 203.0.113.1 route-map FROM-TRANSIT in
“`
Now every route in the table carries, in its communities, a truthful record of where it came from and how it should be treated. That record travels with the route across your entire iBGP mesh.
## Acting on tags, everywhere else
Because the tags are already present, the interesting policy becomes short and prefix-free. Local-preference, applied inbound on internal routes, reads only communities:
“`text
ip community-list standard PREF-50 permit 65001:2000
ip community-list standard PREF-100 permit 65001:2001
ip community-list standard PREF-150 permit 65001:2002
ip community-list standard PREF-200 permit 65001:2003
route-map SET-LOCALPREF permit 10
match community PREF-200
set local-preference 200
route-map SET-LOCALPREF permit 20
match community PREF-150
set local-preference 150
route-map SET-LOCALPREF permit 30
match community PREF-100
set local-preference 100
route-map SET-LOCALPREF permit 40
match community PREF-50
set local-preference 50
route-map SET-LOCALPREF permit 100
“`
Notice this route-map contains no prefixes and no neighbor-specific logic. It is pure translation from tag to behaviour, and it is identical on every router that needs it. Copy it to a new box and it just works, because the meaning lives in the tags, not here.
Advertisement scope at exit points is the same shape:
“`text
ip community-list standard NO-EXPORT-ALL permit 65001:4000
ip community-list standard PEERS-ONLY permit 65001:4001
route-map TO-TRANSIT permit 10
match community NO-EXPORT-ALL
! matched -> this permit has no further terms that advertise; we deny below
route-map TO-TRANSIT deny 20
match community NO-EXPORT-ALL
route-map TO-TRANSIT deny 30
match community PEERS-ONLY
route-map TO-TRANSIT permit 100
“`
Read that as: routes tagged “no export” or “peers only” are not advertised to transit; everything else is. The exit policy never enumerates prefixes, so it never goes stale when prefixes change.
## The operational payoff: remotely triggered blackhole
The pattern’s value shows brightest under pressure. When a customer prefix is under DDoS, you want to blackhole it *now*, from anywhere, without editing route-maps. With a community for it, that’s a one-line action the customer or your NOC can take:
“`text
ip community-list standard BLACKHOLE permit 65001:6666
route-map SET-LOCALPREF permit 5
match community BLACKHOLE
set ip next-hop 192.0.2.1 ! null-routed next-hop on every router
“`
Where `192.0.2.1` is statically pointed at Null0 fabric-wide:
“`text
ip route 192.0.2.1 255.255.255.255 Null0
“`
Tag a /32 with `65001:6666` at any injection point and it is dropped everywhere the community-based policy runs. No emergency route-map edit, no per-router change, no scramble. The mechanism was built in cold blood, weeks earlier, and it’s just waiting.
## Verifying and reasoning about routes
The other quiet benefit is diagnosis. Because meaning is encoded on the route, `show` output *explains itself*:
“`text
router# show ip bgp 192.0.2.0/24
BGP routing table entry for 192.0.2.0/24
…
203.0.113.1 from 203.0.113.1 (203.0.113.1)
Origin IGP, localpref 100, valid, external, best
Community: 65001:1002 65001:2001
“`
`65001:1002 65001:2001` reads immediately as “learned from transit, local-pref 100.” You don’t have to reconstruct the policy in your head or hunt through route-maps — the route carries its own biography. That is the difference between a network you operate and a network you excavate.
## Guardrails that keep it clean
A community scheme decays if you let it, so a few rules protect it.
Strip communities at the boundary you don’t trust. Inbound from external peers, remove your *internal* communities so a peer can’t spoof your policy by sending `65001:2003` and stealing preference:
“`text
route-map FROM-CUSTOMER permit 10
set comm-list SCRUB-INTERNAL delete
set community 65001:1000 additive
“`
Document the namespace as a living artifact, version-controlled next to your configs, because a community scheme no one can read is just numbers again. And resist the urge to overload a single community with two meanings — if you find yourself wanting “prefer *and* don’t-export,” that’s two tags, not one clever one. The whole system works because each tag says exactly one thing.
## Why it survives redesigns
Networks get rebuilt — new hardware, new topology, new transit mix. What I’ve watched survive every one of those is the community table. The edge configs get rewritten for new gear, the core gets re-cabled, but the *meaning* of `65001:2003` never changes, so the policy intent carries straight across. You’re not migrating logic; you’re re-pointing it.
That’s the real argument for treating communities as policy rather than as occasional tags. You build a small, stable vocabulary for intent, mark routes with it once at the edge, and let every device act on that vocabulary. The core stays simple because it never learns about prefixes — only about meaning. And meaning, unlike topology, tends to stay put.