304 lines
12 KiB
Markdown
304 lines
12 KiB
Markdown
# MPLS / LDP enablement: verona ↔ climax ↔ core
|
||
|
||
Goal: bring up MPLS forwarding with LDP across the verona–climax–core spine
|
||
on RouterOS 7, leaving the rest of the network untouched. Once these three
|
||
are running labeled forwarding, expanding to other PoPs is incremental
|
||
(add the LDP instance + interface on each new router).
|
||
|
||
## Topology being labeled
|
||
|
||
```
|
||
verona (10.254.254.101)
|
||
└── ether3-climax-11ghz 10.250.1.25/29 ──┐
|
||
│ 10.250.1.24/29 (AF11 backhaul)
|
||
┌── ether6-verona-11ghz 10.250.1.30/29 ──┘
|
||
climax (10.254.254.102)
|
||
└── ether4-380-airfiber24 10.250.1.94/29 ──┐
|
||
│ 10.250.1.88/29 (AF24 backhaul)
|
||
┌── ether5-climax 10.250.1.89/29 ─┘
|
||
core (10.254.254.253)
|
||
```
|
||
|
||
OSPFv2 (`backbone-v2`, area 0) already redistributes connected, so all three
|
||
loopbacks are reachable. That is the IGP we attach LDP to. Nothing else
|
||
changes.
|
||
|
||
RouterOS versions in play: verona 7.20.8, climax 7.22, core 7.20.6 — all
|
||
include MPLS in the system package, no extra `.npk` to install.
|
||
|
||
## Pre-flight (run on each of the three before configuring)
|
||
|
||
```
|
||
# OSPF must be up and loopbacks reachable end-to-end.
|
||
/routing ospf neighbor print
|
||
/ip route print where dst-address~"10.254.254.10[12]/32" or dst-address~"10.254.254.253/32"
|
||
|
||
# Confirm the underlay MTU on the AF11 / AF24 ports — MPLS adds 4 bytes per
|
||
# label, so we want headroom. Check both physical (l2mtu) and IP MTU:
|
||
/interface print detail where name~"ether3-climax-11ghz|ether6-verona-11ghz|ether4-380-airfiber|ether5-climax"
|
||
```
|
||
|
||
Expected: l2mtu ≥ 1600 on the AF radios (default on CCR ports). If
|
||
anything is at 1500 we'll bump `mpls-mtu` to 1500 explicitly so we never
|
||
generate a 1504-byte frame the radio drops.
|
||
|
||
## Step 1 — verona (10.254.254.101)
|
||
|
||
```
|
||
# LDP instance, lsr-id and transport address pinned to the loopback.
|
||
/mpls ldp
|
||
add disabled=no lsr-id=10.254.254.101 transport-addresses=10.254.254.101 vrf=main
|
||
|
||
# Enable LDP discovery on the climax-facing link only.
|
||
/mpls ldp interface
|
||
add interface=ether3-climax-11ghz
|
||
|
||
# Pin the MPLS MTU explicitly. mpls-mtu is the cap on the *labeled* frame,
|
||
# so a 1500-byte IP payload + 4-byte label = 1504 needs at least 1508 to
|
||
# pass; 1508 also leaves room for one extra stacked label (VPN/FRR/etc.).
|
||
/mpls interface
|
||
add interface=ether3-climax-11ghz mpls-mtu=1508
|
||
```
|
||
|
||
## Step 2 — climax (10.254.254.102)
|
||
|
||
```
|
||
/mpls ldp
|
||
add disabled=no lsr-id=10.254.254.102 transport-addresses=10.254.254.102 vrf=main
|
||
|
||
/mpls ldp interface
|
||
add interface=ether6-verona-11ghz
|
||
add interface=ether4-380-airfiber24
|
||
|
||
/mpls interface
|
||
add interface=ether6-verona-11ghz mpls-mtu=1508
|
||
add interface=ether4-380-airfiber24 mpls-mtu=1508
|
||
```
|
||
|
||
## Step 3 — core (10.254.254.253)
|
||
|
||
```
|
||
/mpls ldp
|
||
add disabled=no lsr-id=10.254.254.253 transport-addresses=10.254.254.253 vrf=main
|
||
|
||
/mpls ldp interface
|
||
add interface=ether5-climax
|
||
|
||
/mpls interface
|
||
add interface=ether5-climax mpls-mtu=1508
|
||
```
|
||
|
||
Order doesn't matter — LDP discovery is symmetric. As soon as both ends of
|
||
a link have `/mpls ldp interface` populated, hellos start flowing on
|
||
224.0.0.2:646 and a TCP session forms between the two transport addresses.
|
||
|
||
## Verification (the actually-works part)
|
||
|
||
### A. Control plane: LDP adjacencies
|
||
|
||
Run on **each** router:
|
||
|
||
```
|
||
/mpls ldp neighbor print detail
|
||
```
|
||
|
||
Required state:
|
||
|
||
- `peer=` lists the neighbor's lsr-id (a loopback IP),
|
||
- `transport=` is the neighbor's loopback IP,
|
||
- flag column shows `O` (operational); `D` (dynamic) is fine — `nD` (not
|
||
discovered) or missing entries means hellos aren't reaching the far side
|
||
(firewall, wrong interface, MTU).
|
||
|
||
Expected counts:
|
||
|
||
| Router | LDP neighbors |
|
||
|--------|---------------|
|
||
| verona | 1 (climax) |
|
||
| climax | 2 (verona, core) |
|
||
| core | 1 (climax) |
|
||
|
||
### B. Control plane: label bindings exist for the remote loopbacks
|
||
|
||
```
|
||
/mpls ldp local-binding print where dst-address~"10.254.254.(101|102|253)/32"
|
||
/mpls ldp remote-binding print where dst-address~"10.254.254.(101|102|253)/32"
|
||
```
|
||
|
||
Each router should advertise an `implicit-null` (or label 3) for its own
|
||
loopback in `local-binding`, and learn labels for the two other loopbacks
|
||
in `remote-binding`.
|
||
|
||
### C. Forwarding plane: FIB has the labels
|
||
|
||
```
|
||
/mpls forwarding-table print
|
||
```
|
||
|
||
You should see entries with `out-label`, `out-interface`, and `nexthop`
|
||
matching the OSPF next-hop toward each remote loopback. On climax you'll
|
||
see two entries — one swapping toward verona, one toward core. On verona
|
||
and core you'll see one entry for each remote loopback (the second loopback
|
||
is reached via climax with stacked labels collapsing to a single label
|
||
because we have only three nodes).
|
||
|
||
### D. Data plane: traceroute prints labels
|
||
|
||
From **verona**, source-routed off the loopback:
|
||
|
||
```
|
||
/tool traceroute 10.254.254.253 src-address=10.254.254.101 count=3
|
||
```
|
||
|
||
A working MPLS path prints `MPLS Label=NNNN E=0 ...` on the first hop
|
||
(climax) — that is the smoking gun. Without MPLS the same traceroute
|
||
succeeds but no `MPLS Label=` field appears.
|
||
|
||
Repeat from **core** in the other direction:
|
||
|
||
```
|
||
/tool traceroute 10.254.254.101 src-address=10.254.254.253 count=3
|
||
```
|
||
|
||
### E. Data plane: MTU check
|
||
|
||
We pinned `mpls-mtu=1508` — verify a full-size packet still passes once
|
||
labeled (1500 IP + 4 label = 1504 on the wire, well below both the 1508
|
||
cap and the radio l2mtu of 2024):
|
||
|
||
```
|
||
# from verona, send 1500-byte ICMP DF=yes to core's loopback
|
||
/ping 10.254.254.253 src-address=10.254.254.101 size=1500 do-not-fragment count=5
|
||
```
|
||
|
||
100% return = path is large enough for label + 1500 payload. Any loss
|
||
means the labeled frame (1504 bytes) doesn't fit somewhere — most likely
|
||
an l2mtu issue on a backhaul radio. Either bump l2mtu on the radio side,
|
||
or *raise* `mpls-mtu` if you increased the IP MTU. **Do not lower
|
||
`mpls-mtu` below 1508** — that caps the labeled frame and will cause
|
||
ICMP-frag-needed for ordinary 1500-byte DF customer traffic.
|
||
|
||
## Rollback (per router)
|
||
|
||
If anything misbehaves, MPLS is purely additive — removing the two tables
|
||
returns the box to plain IP forwarding. OSPF is untouched.
|
||
|
||
```
|
||
/mpls ldp interface remove [find]
|
||
/mpls ldp remove [find]
|
||
# leave /mpls interface mpls-mtu settings as-is; harmless without LDP.
|
||
```
|
||
|
||
## Notes & caveats
|
||
|
||
- **Don't enable LDP on customer-facing or CGNAT interfaces.** Only the
|
||
three backbone ports listed above. LDP advertised onto a customer port
|
||
would expose label bindings to anyone who joined the broadcast domain.
|
||
- **No BGP-LU, no L3VPN, no TE yet.** This is plain IPv4 LDP only — the
|
||
point is to get labeled IGP forwarding working between these three
|
||
before layering services on top.
|
||
- **iBGP next-hop-self is not required** for this phase because we have no
|
||
BGP-painted services riding the LSPs yet. When that changes (e.g.,
|
||
bringing public /24s in over iBGP), confirm next-hop-self on the
|
||
ingress LSR so labeled paths terminate at a router with an LSP.
|
||
- **OSPFv3 / IPv6 LDP is intentionally out of scope.** RouterOS 7 LDP is
|
||
IPv4-only; v6 transport would need MPLSv6 / 6PE which we are not
|
||
setting up here.
|
||
- **BFD on the AF radios** (already enabled on the verona–climax and
|
||
climax–core OSPF templates with `use-bfd=yes`) is what fast-fails the
|
||
underlay; LDP itself follows OSPF and tears down accordingly.
|
||
- **Expanding to other PoPs:** repeat steps 1–3 on the new router with
|
||
its loopback as `lsr-id`/`transport-addresses`, then add `/mpls ldp
|
||
interface` on the link to whichever existing MPLS router it peers with.
|
||
No flag day.
|
||
|
||
---
|
||
|
||
# Deferred work (2026-05-09 session)
|
||
|
||
## Customer traceroute hop-collapse — partially done
|
||
|
||
**Goal**: customer traceroutes from any tower should look like
|
||
`customer → tower → core → edge → TWC → ...` — collapsing the intermediate
|
||
spine LSRs (climax, newhope) into invisible transit hops.
|
||
|
||
**What's done as of 2026-05-09**:
|
||
|
||
- `propagate-ttl=no` set on all 8 fleet routers (verona/climax/culleoka/
|
||
newhope/lowry/982/494/core). This switches MPLS from the default uniform
|
||
model to the pipe model: ingress LSR sets label-TTL=255 (independent of
|
||
IP-TTL), transit LSRs decrement label-TTL only, egress preserves the
|
||
IP-TTL across the LSP. Effect: any traffic that actually rides an LSP
|
||
collapses the intermediate label-switching hops in traceroute.
|
||
- This works **today** for traffic whose destination has an LDP label
|
||
binding — i.e., loopback-to-loopback (`10.254.254.x/32` between any
|
||
two fleet routers), tower-to-tower-customer-prefix (`100.64.x.x/32`,
|
||
the tower /44s), and the connected /29 backbone links. Verified:
|
||
`verona /tool/traceroute 10.254.254.109` returns a single hop (lowry),
|
||
the climax↔core↔newhope spine is invisible.
|
||
|
||
**What's still missing (the actual goal — customer→internet collapse)**:
|
||
|
||
ROS picks an outgoing label based on the **destination prefix's FEC**, not
|
||
on the recursive next-hop. Internet destinations like `1.1.1.1` have no
|
||
LDP label in any tower's forwarding table, so the packet leaves plain-IP
|
||
even when the tower's static default points at a labeled prefix
|
||
(`10.254.254.253` or `10.254.254.254`). We tested both gateway changes on
|
||
494 — neither caused label imposition. Reverted.
|
||
|
||
**To finish this** the fleet would need **BGP labeled unicast** (RFC 3107,
|
||
ROS `address-families=ip-labeled-unicast` on iBGP):
|
||
|
||
1. Core (and/or edge once MPLS-to-edge is solved) advertises BGP routes
|
||
to towers with labels attached. Simplest version: just the default
|
||
route `0.0.0.0/0` with next-hop=core's loopback and a label.
|
||
2. Towers receive labeled BGP routes. When forwarding to any destination
|
||
matching such a route, the tower pushes the BGP-LU label, then the LDP
|
||
transport label on top, and rides the LSP.
|
||
3. With `propagate-ttl=no` already set, the LSP is invisible in
|
||
traceroute. Customer sees `tower → core → edge → TWC → ...` (3 hops to
|
||
TWC, regardless of which tower they're on).
|
||
|
||
**Why we didn't do it today**:
|
||
- Three MPLS-related outages already (`asymmetric_ldp_parallel_paths`
|
||
memory entry). Don't add a fourth without a maintenance window.
|
||
- BGP-LU isn't tested in this fleet. Need to:
|
||
- Confirm ROS7 syntax for `address-families=ip-labeled-unicast` on the
|
||
existing iBGP-RR mesh.
|
||
- Decide whether to advertise LU labels for just the default route or
|
||
for the full reflected RIB.
|
||
- Decide what core advertises — just `0.0.0.0/0`, or also tower /44s
|
||
so return-path traffic from edge to tower customers can also ride
|
||
a labeled path with hop hiding (currently the return path
|
||
edge → core → tower is plain-IP edge→core then labeled core→tower).
|
||
- A future attempt should also revisit whether MPLS-to-edge is desirable
|
||
alongside this. With BGP-LU from core, edge doesn't need to be in LDP
|
||
for towers to ride an LSP toward edge — the LSP terminates at core
|
||
(where the BGP-LU label is popped), and edge sees plain IP exactly
|
||
like today. So **BGP-LU may be a cleaner path than extending LDP to
|
||
edge** for this specific goal.
|
||
|
||
## Extending LDP to edge — blocked, root cause unknown
|
||
|
||
See `~/.claude/projects/-Users-graham-dev-network-mikrotik-tool/memory/
|
||
asymmetric_ldp_parallel_paths.md` for the full incident notes. Three live
|
||
attempts on 2026-05-09 to bring up an LDP session between core and edge
|
||
all dropped fleet internet traffic within seconds. Hypotheses tried and
|
||
disproven: asymmetric LDP path redirect, BGP-table label flooding (we
|
||
tightened edge's advertise-filter to just `10.254.254.254/32`), and the
|
||
flood still happened. **Don't attempt again live without lab replication
|
||
or a console on edge.**
|
||
|
||
State left in place that survives across attempts:
|
||
- `mpls-mtu=1508` on edge `sfp-sfpplus7-core-direct` and
|
||
`sfp-sfpplus11-preseem`.
|
||
- `mpls-mtu=1508` on core `ether3-edge-direct` and
|
||
`sfp-sfpplus1-edge-preseem`.
|
||
- Fasttrack-bypass pairs (in/out) on chain=forward for those four
|
||
interfaces on both routers.
|
||
- Edge `/mpls ldp advertise-filter` restricted to advertise only
|
||
`10.254.254.254/32`.
|
||
- Edge `/mpls ldp set distribute-for-default-route=no`.
|
||
|
||
These are all harmless and would be required again on the next attempt;
|
||
do not roll them back.
|