Skip to content

Commit f35e3b3

Browse files
authored
Merge pull request #1429 from kernelkit/ospf-point-to-multipoint
ospf: add point-to-multipoint and hybrid interface type support Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2 parents 5bb21e5 + 312b856 commit f35e3b3

22 files changed

Lines changed: 1242 additions & 40 deletions

File tree

doc/ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ All notable changes to the project are documented in this file.
2525
- `set ssh known-hosts <host> <keytype> <pubkey>` — pre-enroll a host key
2626
received out-of-band, e.g. after a factory reset changes the device host key
2727
- `no ssh known-hosts <host>` — remove a stale or changed host key entry
28+
- Add OSPF point-to-multipoint (P2MP) and hybrid interface type support
29+
2830

2931
### Fixes
3032

doc/routing.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,66 @@ an Ethernet interface can be done as follows.
152152
admin@example:/config/routing/…/ospf/area/0.0.0.0/interface/e0/>
153153
</code></pre>
154154

155+
156+
### Point-to-Multipoint
157+
158+
Point-to-Multipoint (P2MP) is used when multiple OSPF routers share a
159+
common network segment but should form individual adjacencies rather
160+
than electing a Designated Router (DR). This is common in NBMA-like
161+
environments, DMVPN, or hub-and-spoke topologies.
162+
163+
Infix supports two P2MP variants via the `interface-type` setting:
164+
165+
| **Interface Type** | **Behavior** |
166+
|:----------------------|:-----------------------------------------------|
167+
| `hybrid` | P2MP with multicast Hellos (broadcast-capable) |
168+
| `point-to-multipoint` | P2MP with unicast Hellos (non-broadcast) |
169+
170+
#### Hybrid (broadcast-capable P2MP)
171+
172+
Use `hybrid` when all neighbors on the segment can receive multicast.
173+
Hello packets are sent to the standard OSPF multicast address (224.0.0.5)
174+
and neighbors are discovered automatically — no manual neighbor
175+
configuration is needed.
176+
177+
<pre class="cli"><code>admin@example:/config/> <b>edit routing control-plane-protocol ospfv2 name default ospf</b>
178+
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 interface-type hybrid</b>
179+
admin@example:/config/routing/…/ospf/> <b>leave</b>
180+
admin@example:/>
181+
</code></pre>
182+
183+
#### Non-broadcast P2MP
184+
185+
Use `point-to-multipoint` when the network does not support multicast.
186+
Hello packets are sent as unicast directly to each configured neighbor.
187+
Since neighbors cannot be discovered automatically, they must be
188+
configured explicitly using static neighbors (see below).
189+
190+
<pre class="cli"><code>admin@example:/config/> <b>edit routing control-plane-protocol ospfv2 name default ospf</b>
191+
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 interface-type point-to-multipoint</b>
192+
admin@example:/config/routing/…/ospf/> <b>leave</b>
193+
admin@example:/>
194+
</code></pre>
195+
196+
197+
### Static Neighbors
198+
199+
When using non-broadcast interface types (such as `point-to-multipoint`),
200+
OSPF cannot discover neighbors via multicast. Static neighbors must be
201+
configured so the router knows where to send unicast Hello packets.
202+
203+
<pre class="cli"><code>admin@example:/config/> <b>edit routing control-plane-protocol ospfv2 name default ospf</b>
204+
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 static-neighbors neighbor 10.0.123.2</b>
205+
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 static-neighbors neighbor 10.0.123.3</b>
206+
admin@example:/config/routing/…/ospf/> <b>leave</b>
207+
admin@example:/>
208+
</code></pre>
209+
210+
> [!NOTE]
211+
> Static neighbors are only needed for non-broadcast interface types.
212+
> With `hybrid` (or `broadcast`), neighbors are discovered automatically
213+
> via multicast.
214+
155215
### OSPF global settings
156216

157217
In addition to *area* and *interface* specific settings, OSPF provides

src/confd/src/routing.c

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp)
154154
return num_interfaces;
155155
}
156156

157+
static const char *ospf_network_type(const char *yang_type)
158+
{
159+
if (!strcmp(yang_type, "hybrid"))
160+
return "point-to-multipoint";
161+
if (!strcmp(yang_type, "point-to-multipoint"))
162+
return "point-to-multipoint non-broadcast";
163+
164+
/* broadcast, non-broadcast, point-to-point pass through unchanged */
165+
return yang_type;
166+
}
167+
157168
int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
158169
{
159170
struct lyd_node *interface, *interfaces, *area;
@@ -203,7 +214,7 @@ int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FIL
203214
if (passive)
204215
fputs(" ip ospf passive\n", fp);
205216
if (interface_type)
206-
fprintf(fp, " ip ospf network %s\n", interface_type);
217+
fprintf(fp, " ip ospf network %s\n", ospf_network_type(interface_type));
207218
if (cost)
208219
fprintf(fp, " ip ospf cost %s\n", cost);
209220
}
@@ -226,6 +237,28 @@ int parse_ospf_redistribute(sr_session_ctx_t *session, struct lyd_node *redistri
226237
return 0;
227238
}
228239

240+
static void parse_ospf_static_neighbors(struct lyd_node *areas, FILE *fp)
241+
{
242+
struct lyd_node *area, *interface, *interfaces, *neighbors, *neighbor;
243+
244+
LY_LIST_FOR(lyd_child(areas), area) {
245+
interfaces = lydx_get_child(area, "interfaces");
246+
247+
LY_LIST_FOR(lyd_child(interfaces), interface) {
248+
neighbors = lydx_get_child(interface, "static-neighbors");
249+
if (!neighbors)
250+
continue;
251+
252+
LY_LIST_FOR(lyd_child(neighbors), neighbor) {
253+
const char *id = lydx_get_cattr(neighbor, "identifier");
254+
255+
if (id)
256+
fprintf(fp, " neighbor %s\n", id);
257+
}
258+
}
259+
}
260+
}
261+
229262
int parse_ospf_areas(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
230263
{
231264
int areas_configured = 0;
@@ -315,6 +348,7 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
315348
fputs("router ospf\n", fp);
316349
num_areas = parse_ospf_areas(session, areas, fp);
317350
parse_ospf_redistribute(session, lydx_get_child(ospf, "redistribute"), fp);
351+
parse_ospf_static_neighbors(areas, fp);
318352
default_route = lydx_get_child(ospf, "default-route-advertise");
319353
if (default_route) {
320354
/* enable is obsolete in favor for enabled. */

src/confd/yang/confd.inc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ MODULES=(
1414
"ietf-routing@2018-03-13.yang"
1515
"ietf-ipv6-unicast-routing@2018-03-13.yang"
1616
"ietf-ipv4-unicast-routing@2018-03-13.yang"
17-
"ietf-ospf@2022-10-19.yang -e bfd -e explicit-router-id"
17+
"ietf-ospf@2022-10-19.yang -e bfd -e explicit-router-id -e hybrid-interface"
1818
"ietf-rip@2020-02-20.yang"
1919
"iana-bfd-types@2021-10-21.yang"
2020
"ietf-bfd-types@2022-09-22.yang"
@@ -31,7 +31,7 @@ MODULES=(
3131
"ieee802-dot1q-types@2022-10-29.yang"
3232
"infix-ip@2025-11-02.yang"
3333
"infix-if-type@2026-01-07.yang"
34-
"infix-routing@2025-12-02.yang"
34+
"infix-routing@2026-03-04.yang "
3535
"ieee802-dot1ab-lldp@2022-03-15.yang"
3636
"infix-lldp@2025-05-05.yang"
3737
"infix-dhcp-common@2025-12-21.yang"

src/confd/yang/confd/infix-routing.yang

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ module infix-routing {
2626
contact "kernelkit@googlegroups.com";
2727
description "Deviations and augments for ietf-routing, ietf-ospf, and ietf-rip.";
2828

29+
revision 2026-03-04 {
30+
description "Remove interface-type deviation to expose standard ietf-ospf
31+
interface types including point-to-multipoint and hybrid.
32+
Un-deviate static-neighbors for non-broadcast P2MP support.";
33+
}
34+
2935
revision 2025-12-02 {
3036
description "Add configurable OSPF debug logging container.
3137
Add RIP (Routing Information Protocol) support.";
@@ -247,18 +253,6 @@ module infix-routing {
247253
}
248254

249255
/* OSPF */
250-
typedef infix-ospf-interface-type {
251-
type enumeration {
252-
enum broadcast {
253-
description
254-
"Specifies an OSPF broadcast multi-access network.";
255-
}
256-
enum point-to-point {
257-
description
258-
"Specifies an OSPF point-to-point network.";
259-
}
260-
}
261-
}
262256

263257

264258
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol" {
@@ -367,11 +361,6 @@ module infix-routing {
367361
}
368362
}
369363

370-
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/ospf:interface-type" {
371-
deviate replace {
372-
type infix-ospf-interface-type;
373-
}
374-
}
375364

376365
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:auto-cost" {
377366
deviate not-supported;
@@ -463,10 +452,6 @@ module infix-routing {
463452
}
464453

465454
/* OSPF Area Interface */
466-
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/ospf:static-neighbors"
467-
{
468-
deviate not-supported;
469-
}
470455
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/ospf:multi-areas" {
471456
deviate not-supported;
472457
}
File renamed without changes.

src/statd/python/cli_pretty/cli_pretty.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4923,7 +4923,7 @@ def show_ospf_interfaces(json_data):
49234923
state = target_iface.get('state', 'down')
49244924
cost = target_iface.get('cost', 0)
49254925
priority = target_iface.get('priority', 1)
4926-
iface_type = target_iface.get('interface-type', 'unknown')
4926+
iface_type = target_iface.get('interface-type', '')
49274927
hello_interval = target_iface.get('hello-interval', 10)
49284928
dead_interval = target_iface.get('dead-interval', 40)
49294929
retransmit_interval = target_iface.get('retransmit-interval', 5)
@@ -4999,9 +4999,11 @@ def show_ospf_interfaces(json_data):
49994999
network_type_map = {
50005000
'point-to-point': 'POINTOPOINT',
50015001
'broadcast': 'BROADCAST',
5002-
'non-broadcast': 'NBMA'
5002+
'non-broadcast': 'NBMA',
5003+
'point-to-multipoint': 'POINTOMULTIPOINT',
5004+
'hybrid': 'POINTOMULTIPOINT'
50035005
}
5004-
network_type = network_type_map.get(iface_type, iface_type.upper())
5006+
network_type = network_type_map.get(iface_type, iface_type.upper() if iface_type else 'LOOPBACK')
50055007

50065008
print(f"{name} is up")
50075009
if ip_address:
@@ -5041,30 +5043,50 @@ def show_ospf_interfaces(json_data):
50415043
return
50425044

50435045
# Display table view (no specific interface)
5044-
hdr = f"{'INTERFACE':<12} {'AREA':<12} {'STATE':<10} {'COST':<6} {'PRI':<4} {'DR':<15} {'BDR':<15} {'NBRS':<5}"
5045-
print(Decore.invert(hdr))
5046+
type_display_map = {
5047+
'point-to-point': 'P2P',
5048+
'broadcast': 'Broadcast',
5049+
'non-broadcast': 'NBMA',
5050+
'point-to-multipoint': 'P2MP',
5051+
'hybrid': 'Hybrid'
5052+
}
5053+
5054+
def fmt_state(state):
5055+
if state in ('dr', 'bdr'):
5056+
return state.upper()
5057+
if state == 'dr-other':
5058+
return 'DROther'
5059+
return state.capitalize()
5060+
5061+
table = SimpleTable([
5062+
Column('INTERFACE'),
5063+
Column('AREA'),
5064+
Column('TYPE'),
5065+
Column('STATE'),
5066+
Column('COST', 'right'),
5067+
Column('PRI', 'right'),
5068+
Column('DR'),
5069+
Column('BDR'),
5070+
Column('NBRS', 'right')
5071+
])
50465072

50475073
for iface in all_interfaces:
50485074
name = iface.get('name', 'unknown')
50495075
area_id = iface.get('_area_id', '0.0.0.0')
50505076
state = iface.get('state', 'down')
5077+
iface_type = iface.get('interface-type', '')
50515078
cost = iface.get('cost', 0)
50525079
priority = iface.get('priority', 1)
50535080
dr_id = iface.get('dr-router-id', '-')
50545081
bdr_id = iface.get('bdr-router-id', '-')
50555082
neighbors = iface.get('neighbors', {}).get('neighbor', [])
5056-
nbr_count = len(neighbors)
50575083

5058-
# Capitalize state nicely
5059-
state_display = state.upper() if state in ['dr', 'bdr'] else state.capitalize()
5060-
if state == 'dr-other':
5061-
state_display = 'DROther'
5062-
5063-
# Shorten router IDs for display
5064-
dr_display = dr_id if dr_id != '-' else '-'
5065-
bdr_display = bdr_id if bdr_id != '-' else '-'
5084+
table.row(name, area_id,
5085+
type_display_map.get(iface_type, iface_type.capitalize() if iface_type else '-'),
5086+
fmt_state(state),
5087+
cost, priority, dr_id, bdr_id, len(neighbors))
50665088

5067-
print(f"{name:<12} {area_id:<12} {state_display:<10} {cost:<6} {priority:<4} {dr_display:<15} {bdr_display:<15} {nbr_count:<5}")
5089+
table.print()
50685090

50695091

50705092
def show_ospf_neighbor(json_data):

src/statd/python/yanger/ietf_ospf.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,15 @@ def add_areas(control_protocols):
124124
interface["enabled"] = iface["ospfEnabled"]
125125
if iface["networkType"] == "POINTOPOINT":
126126
interface["interface-type"] = "point-to-point"
127-
if iface["networkType"] == "BROADCAST":
127+
elif iface["networkType"] == "BROADCAST":
128128
interface["interface-type"] = "broadcast"
129+
elif iface["networkType"] == "POINTOMULTIPOINT":
130+
if iface.get("p2mpNonBroadcast", False):
131+
interface["interface-type"] = "point-to-multipoint"
132+
else:
133+
interface["interface-type"] = "hybrid"
134+
elif iface["networkType"] == "NBMA":
135+
interface["interface-type"] = "non-broadcast"
129136

130137
if iface.get("state"):
131138
# Wev've never seen "DependUpon", and has no entry in

test/case/routing/Readme.adoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Tests verifying standard routing protocols and configuration:
1010
- OSPF with BFD (Bidirectional Forwarding Detection)
1111
- OSPF default route advertisement and propagation
1212
- OSPF debug logging configuration and verification
13+
- OSPF point-to-multipoint hybrid (broadcast) interface type
14+
- OSPF point-to-multipoint (non-broadcast) interface type with static neighbors
1315
- RIP basic neighbor discovery and route exchange
1416
- RIP passive interface configuration
1517
- RIP route redistribution (connected, static, and OSPF)
@@ -46,6 +48,14 @@ include::ospf_debug/Readme.adoc[]
4648

4749
<<<
4850

51+
include::ospf_point_to_multipoint_hybrid/Readme.adoc[]
52+
53+
<<<
54+
55+
include::ospf_point_to_multipoint/Readme.adoc[]
56+
57+
<<<
58+
4959
include::rip_basic/Readme.adoc[]
5060

5161
<<<

test/case/routing/all.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
- name: OSPF Debug Logging
3030
case: ospf_debug/test.py
3131

32+
- name: OSPF Point-to-Multipoint Hybrid
33+
case: ospf_point_to_multipoint_hybrid/test.py
34+
35+
- name: OSPF Point-to-Multipoint
36+
case: ospf_point_to_multipoint/test.py
37+
3238
- name: RIP Basic
3339
case: rip_basic/test.py
3440

0 commit comments

Comments
 (0)