Junos Routing Policy That Scales: Policy-Statement Patterns and Safe Defaults

Config grew over years. Multiple engineers added policies. Nobody documented communities. New engineer joins, asks “what does community 65000:999 mean?” — silence. “Don’t touch it — it works.”

This is how routing policies become unmaintainable. Junos provides powerful policy tools, but power without structure creates chaos.

Junos Policy Mental Model

Terms Evaluate Top to Bottom

policy-statement EXAMPLE {
term FIRST {
from { ... }
then accept;
}
term SECOND {
from { ... }
then reject;
}
term DEFAULT {
then reject; # Explicit default
}
}

First matching term wins. If FIRST matches, SECOND never evaluates. No match in any term? Implicit accept — the silent danger.

The Implicit Accept Problem

Terminal window
# Dangerous policy - implicit accept at end
policy-statement FILTER-ROUTES {
term BLOCK-BOGONS {
from {
route-filter 10.0.0.0/8 orlonger;
}
then reject;
}
# No default term = everything else ACCEPTED
}

Traffic you didn’t explicitly handle gets accepted. Always add explicit default:

Terminal window
# Safe policy - explicit default
policy-statement FILTER-ROUTES {
term BLOCK-BOGONS {
from {
route-filter 10.0.0.0/8 orlonger;
}
then reject;
}
term DEFAULT-DENY {
then reject;
}
}

Accept vs Next Policy vs Next Term

Terminal window
then accept; # Accept route, stop processing THIS policy
then reject; # Reject route, stop processing THIS policy
then next policy; # Continue to next policy in chain
then next term; # Continue to next term in THIS policy

Multiple policies can be chained:

Terminal window
set protocols bgp group PEERS import [ POLICY-1 POLICY-2 POLICY-3 ]
# Evaluates POLICY-1, then POLICY-2, then POLICY-3
# First explicit accept/reject wins

Policy Building Blocks

Prefix Lists

Named lists of prefixes. Reusable across policies.

Terminal window
# Define prefix-list
set policy-options prefix-list INTERNAL-NETWORKS 10.0.0.0/8
set policy-options prefix-list INTERNAL-NETWORKS 172.16.0.0/12
set policy-options prefix-list INTERNAL-NETWORKS 192.168.0.0/16
# Use in policy
set policy-options policy-statement ALLOW-INTERNAL term MATCH from prefix-list INTERNAL-NETWORKS
set policy-options policy-statement ALLOW-INTERNAL term MATCH then accept
set policy-options policy-statement ALLOW-INTERNAL term DEFAULT then reject

Route Filters

More granular than prefix-lists. Match exact prefixes or ranges.

Terminal window
# Exact match
route-filter 10.0.0.0/24 exact;
# Match this and longer (subnets)
route-filter 10.0.0.0/16 orlonger;
# Match longer only (not the /16 itself)
route-filter 10.0.0.0/16 longer;
# Match range
route-filter 10.0.0.0/16 prefix-length-range /24-/28;
# Match up to a length
route-filter 10.0.0.0/8 upto /24;

Practical example:

Terminal window
policy-statement CUSTOMER-ROUTES {
term ACCEPT-ALLOCATED {
from {
route-filter 203.0.113.0/24 orlonger; # Customer's allocation
route-filter 198.51.100.0/24 orlonger; # Second allocation
}
then accept;
}
term REJECT-REST {
then reject;
}
}

AS Path Filters

Match routes by AS path patterns.

Terminal window
# Define AS path regex
set policy-options as-path ORIGIN-65001 ".* 65001"
set policy-options as-path DIRECT-PEER "^65001$"
set policy-options as-path TRANSIT ".* 65001 .*"
# Use in policy
policy-statement PREFER-DIRECT {
term DIRECT {
from as-path DIRECT-PEER;
then {
local-preference 150;
accept;
}
}
term TRANSIT {
from as-path TRANSIT;
then {
local-preference 100;
accept;
}
}
}

AS path regex patterns:

  • ^ — start of path
  • $ — end of path
  • . — any single AS
  • .* — zero or more ASes
  • [0-9]+ — one or more digits (any AS number)

Communities

Tags attached to routes. The glue for policy communication.

Terminal window
# Define communities
set policy-options community CUSTOMER-ROUTES members 65000:100
set policy-options community NO-EXPORT members no-export
set policy-options community BLACKHOLE members 65000:666
# Match community
policy-statement CUSTOMER-IMPORT {
term TAGGED {
from community CUSTOMER-ROUTES;
then accept;
}
}
# Set community
policy-statement TAG-OUTBOUND {
term ADD-TAG {
then {
community add CUSTOMER-ROUTES;
accept;
}
}
}

Community Design That Scales

Naming Convention

Without documentation, 65000:100 means nothing. Create a system:

Terminal window
# Pattern: ASN:TYPE+VALUE
# Types:
# 1xx = Origin (where route came from)
# 2xx = Region
# 3xx = Customer type
# 4xx = Traffic engineering
# 666 = Blackhole
# Examples:
set policy-options community ORIGIN-CUSTOMER members 65000:100
set policy-options community ORIGIN-PEER members 65000:101
set policy-options community ORIGIN-TRANSIT members 65000:102
set policy-options community REGION-US-EAST members 65000:201
set policy-options community REGION-US-WEST members 65000:202
set policy-options community REGION-EU members 65000:203
set policy-options community TYPE-ENTERPRISE members 65000:301
set policy-options community TYPE-RESIDENTIAL members 65000:302
set policy-options community TE-BACKUP-ONLY members 65000:401
set policy-options community TE-PRIMARY members 65000:402
set policy-options community BLACKHOLE members 65000:666

Document in Config

Junos supports description on most objects. Use it:

Terminal window
set policy-options community ORIGIN-CUSTOMER members 65000:100
annotate policy-options community ORIGIN-CUSTOMER "Routes learned from direct customers"
set policy-options community BLACKHOLE members 65000:666
annotate policy-options community BLACKHOLE "Trigger RTBH - null route this prefix"

Community Groups

Group related communities for easier matching:

Terminal window
# Define community group
set policy-options community ALL-ORIGINS members "65000:10[0-9]"
set policy-options community ALL-REGIONS members "65000:2[0-9][0-9]"
# Match any origin community
policy-statement CHECK-ORIGIN {
term HAS-ORIGIN {
from community ALL-ORIGINS;
then accept;
}
term MISSING-ORIGIN {
then reject; # Reject routes without origin tag
}
}

Safe Defaults

Bogon Filtering

Always filter RFC1918, documentation prefixes, and other bogons:

Terminal window
# Bogon prefix-list (base prefixes)
set policy-options prefix-list BOGONS 0.0.0.0/8
set policy-options prefix-list BOGONS 10.0.0.0/8
set policy-options prefix-list BOGONS 100.64.0.0/10
set policy-options prefix-list BOGONS 127.0.0.0/8
set policy-options prefix-list BOGONS 169.254.0.0/16
set policy-options prefix-list BOGONS 172.16.0.0/12
set policy-options prefix-list BOGONS 192.0.0.0/24
set policy-options prefix-list BOGONS 192.0.2.0/24
set policy-options prefix-list BOGONS 192.168.0.0/16
set policy-options prefix-list BOGONS 198.18.0.0/15
set policy-options prefix-list BOGONS 198.51.100.0/24
set policy-options prefix-list BOGONS 203.0.113.0/24
set policy-options prefix-list BOGONS 224.0.0.0/4
set policy-options prefix-list BOGONS 240.0.0.0/4
# Apply with orlonger match (catches subnets too)
policy-statement REJECT-BOGONS {
term BOGONS {
from {
prefix-list-filter BOGONS orlonger;
}
then reject;
}
}

Max Prefix Protection

Limit prefixes accepted from peers:

Terminal window
# Limit with teardown
set protocols bgp group CUSTOMERS neighbor 192.0.2.1 family inet unicast prefix-limit maximum 1000
set protocols bgp group CUSTOMERS neighbor 192.0.2.1 family inet unicast prefix-limit teardown 80 idle-timeout 30
# Teardown at 80% (800 prefixes), wait 30 minutes before retry

Reject Unless Explicitly Allowed

Default-deny at BGP group level:

Terminal window
# Import policy for customer
policy-statement CUSTOMER-192-0-2-1-IMPORT {
term ACCEPT-ANNOUNCED {
from {
prefix-list CUSTOMER-192-0-2-1-PREFIXES;
}
then {
community add ORIGIN-CUSTOMER;
accept;
}
}
term REJECT-ALL {
then reject;
}
}
# Customer's allowed prefixes
set policy-options prefix-list CUSTOMER-192-0-2-1-PREFIXES 198.51.100.0/24

AS Path Sanity

Reject private ASNs and your own ASN from external peers:

Terminal window
# Private ASN range (64512-65534)
set policy-options as-path PRIVATE-ASN ".* (6451[2-9]|645[2-9][0-9]|6[5-9][0-4][0-9]{2}|655[0-2][0-9]|6553[0-4]) .*"
# Simpler alternative: match specific private ASNs you might see
set policy-options as-path PRIVATE-ASN-SIMPLE "64[5-9][0-9]{2}|65[0-4][0-9]{2}|655[0-3][0-4]"
# Own ASN in path (shouldn't happen from external)
set policy-options as-path OWN-ASN ".* 65000 .*"
policy-statement SANITY-CHECK {
term REJECT-PRIVATE-ASN {
from as-path PRIVATE-ASN;
then reject;
}
term REJECT-OWN-ASN {
from as-path OWN-ASN;
then reject;
}
}

Note: AS path regex for full private range coverage is complex. Many operators maintain external prefix/AS-path lists (e.g., from Team Cymru or RIPE) rather than hand-crafted regex.

Policy Structure Patterns

Layered Import Policy

Build policies in layers for maintainability:

Terminal window
# Layer 1: Sanity checks (apply to all)
policy-statement IMPORT-SANITY {
term REJECT-BOGONS { from prefix-list BOGONS; then reject; }
term REJECT-TOO-LONG { from route-filter 0.0.0.0/0 prefix-length-range /25-/32; then reject; }
term REJECT-DEFAULT { from route-filter 0.0.0.0/0 exact; then reject; }
term CONTINUE { then next policy; }
}
# Layer 2: Peer-specific acceptance
policy-statement IMPORT-PEER-65001 {
term ACCEPT-PREFIXES {
from prefix-list PEER-65001-PREFIXES;
then {
community add ORIGIN-PEER;
local-preference 100;
accept;
}
}
term REJECT-REST { then reject; }
}
# Apply both
set protocols bgp group PEERS neighbor 192.0.2.1 import [ IMPORT-SANITY IMPORT-PEER-65001 ]

Export Policy Template

Consistent export structure:

Terminal window
policy-statement EXPORT-TO-PEERS {
term EXPORT-CUSTOMERS {
from community ORIGIN-CUSTOMER;
then {
community delete ALL-INTERNAL; # Strip internal communities
accept;
}
}
term EXPORT-OWN {
from {
protocol direct;
prefix-list OWN-PREFIXES;
}
then accept;
}
term REJECT-REST {
then reject;
}
}

Debugging Policies

Test Policy Match

Terminal window
# Test which policy term matches a route
test policy POLICY-NAME 192.0.2.0/24
# Output shows:
# Route 192.0.2.0/24
# Term: ACCEPT-CUSTOMERS
# Action: accept

Show Received vs Active

Terminal window
# What peer is sending (before import policy)
show route receive-protocol bgp 192.0.2.1
# What we accepted (after import policy)
show route protocol bgp neighbor 192.0.2.1
# Compare to find filtered routes

Hidden Routes

Routes filtered by policy become “hidden”:

Terminal window
# Show hidden routes
show route hidden
# Why is it hidden?
show route 192.0.2.0/24 hidden extensive

Policy Decision Flow

┌─────────────────────────────────────────────────────────┐
│ Import Policy Chain │
├─────────────────────────────────────────────────────────┤
│ │
│ Policy 1 Policy 2 Policy 3 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Term A │─no─→│ Term A │─no─→│ Term A │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │yes │yes │yes │
│ ↓ ↓ ↓ │
│ [accept/reject] [accept/reject] [accept/reject] │
│ │
│ If no match in any term of any policy: │
│ → IMPLICIT ACCEPT (danger!) │
│ │
└─────────────────────────────────────────────────────────┘

The Lesson

Junos routing policy is powerful but requires discipline:

  1. Always explicit default — never rely on implicit accept
  2. Name everything meaningfully — communities, prefix-lists, policies
  3. Document in config — use annotate liberally
  4. Layer your policies — sanity checks separate from peer-specific logic
  5. Test before commit — use test policy command

A well-structured policy config is:

  • Readable by new engineers
  • Modifiable without fear
  • Auditable for compliance

The goal isn’t clever regex — it’s maintainability. If you can’t explain what 65000:247 means without checking documentation, your community scheme needs work.

Policy-statement is an engineering system. Treat it like code: structured, documented, tested.