diff --git a/doc/routing.md b/doc/routing.md index 13573b454..e473ba854 100644 --- a/doc/routing.md +++ b/doc/routing.md @@ -131,6 +131,66 @@ an Ethernet interface can be done as follows. admin@example:/config/routing/…/ospf/area/0.0.0.0/interface/e0/> + +### Point-to-Multipoint + +Point-to-Multipoint (P2MP) is used when multiple OSPF routers share a +common network segment but should form individual adjacencies rather +than electing a Designated Router (DR). This is common in NBMA-like +environments, DMVPN, or hub-and-spoke topologies. + +Infix supports two P2MP variants via the `interface-type` setting: + +| **Interface Type** | **Behavior** | +|:----------------------|:-----------------------------------------------| +| `hybrid` | P2MP with multicast Hellos (broadcast-capable) | +| `point-to-multipoint` | P2MP with unicast Hellos (non-broadcast) | + +#### Hybrid (broadcast-capable P2MP) + +Use `hybrid` when all neighbors on the segment can receive multicast. +Hello packets are sent to the standard OSPF multicast address (224.0.0.5) +and neighbors are discovered automatically — no manual neighbor +configuration is needed. + +
admin@example:/config/> edit routing control-plane-protocol ospfv2 name default ospf
+admin@example:/config/routing/…/ospf/> set area 0.0.0.0 interface e0 interface-type hybrid
+admin@example:/config/routing/…/ospf/> leave
+admin@example:/>
+
+
+#### Non-broadcast P2MP
+
+Use `point-to-multipoint` when the network does not support multicast.
+Hello packets are sent as unicast directly to each configured neighbor.
+Since neighbors cannot be discovered automatically, they must be
+configured explicitly using static neighbors (see below).
+
+admin@example:/config/> edit routing control-plane-protocol ospfv2 name default ospf
+admin@example:/config/routing/…/ospf/> set area 0.0.0.0 interface e0 interface-type point-to-multipoint
+admin@example:/config/routing/…/ospf/> leave
+admin@example:/>
+
+
+
+### Static Neighbors
+
+When using non-broadcast interface types (such as `point-to-multipoint`),
+OSPF cannot discover neighbors via multicast. Static neighbors must be
+configured so the router knows where to send unicast Hello packets.
+
+admin@example:/config/> edit routing control-plane-protocol ospfv2 name default ospf
+admin@example:/config/routing/…/ospf/> set area 0.0.0.0 interface e0 static-neighbors neighbor 10.0.123.2
+admin@example:/config/routing/…/ospf/> set area 0.0.0.0 interface e0 static-neighbors neighbor 10.0.123.3
+admin@example:/config/routing/…/ospf/> leave
+admin@example:/>
+
+
+> [!NOTE]
+> Static neighbors are only needed for non-broadcast interface types.
+> With `hybrid` (or `broadcast`), neighbors are discovered automatically
+> via multicast.
+
### OSPF global settings
In addition to *area* and *interface* specific settings, OSPF provides
diff --git a/src/confd/src/routing.c b/src/confd/src/routing.c
index 5fa199fce..5bb8b45af 100644
--- a/src/confd/src/routing.c
+++ b/src/confd/src/routing.c
@@ -154,6 +154,17 @@ int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp)
return num_interfaces;
}
+static const char *ospf_network_type(const char *yang_type)
+{
+ if (!strcmp(yang_type, "hybrid"))
+ return "point-to-multipoint";
+ if (!strcmp(yang_type, "point-to-multipoint"))
+ return "point-to-multipoint non-broadcast";
+
+ /* broadcast, non-broadcast, point-to-point pass through unchanged */
+ return yang_type;
+}
+
int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
{
struct lyd_node *interface, *interfaces, *area;
@@ -203,7 +214,7 @@ int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FIL
if (passive)
fputs(" ip ospf passive\n", fp);
if (interface_type)
- fprintf(fp, " ip ospf network %s\n", interface_type);
+ fprintf(fp, " ip ospf network %s\n", ospf_network_type(interface_type));
if (cost)
fprintf(fp, " ip ospf cost %s\n", cost);
}
@@ -226,6 +237,28 @@ int parse_ospf_redistribute(sr_session_ctx_t *session, struct lyd_node *redistri
return 0;
}
+static void parse_ospf_static_neighbors(struct lyd_node *areas, FILE *fp)
+{
+ struct lyd_node *area, *interface, *interfaces, *neighbors, *neighbor;
+
+ LY_LIST_FOR(lyd_child(areas), area) {
+ interfaces = lydx_get_child(area, "interfaces");
+
+ LY_LIST_FOR(lyd_child(interfaces), interface) {
+ neighbors = lydx_get_child(interface, "static-neighbors");
+ if (!neighbors)
+ continue;
+
+ LY_LIST_FOR(lyd_child(neighbors), neighbor) {
+ const char *id = lydx_get_cattr(neighbor, "identifier");
+
+ if (id)
+ fprintf(fp, " neighbor %s\n", id);
+ }
+ }
+ }
+}
+
int parse_ospf_areas(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
{
int areas_configured = 0;
@@ -315,6 +348,7 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
fputs("router ospf\n", fp);
num_areas = parse_ospf_areas(session, areas, fp);
parse_ospf_redistribute(session, lydx_get_child(ospf, "redistribute"), fp);
+ parse_ospf_static_neighbors(areas, fp);
default_route = lydx_get_child(ospf, "default-route-advertise");
if (default_route) {
/* enable is obsolete in favor for enabled. */
diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc
index 8efe9e3e1..b082bd821 100644
--- a/src/confd/yang/confd.inc
+++ b/src/confd/yang/confd.inc
@@ -14,7 +14,7 @@ MODULES=(
"ietf-routing@2018-03-13.yang"
"ietf-ipv6-unicast-routing@2018-03-13.yang"
"ietf-ipv4-unicast-routing@2018-03-13.yang"
- "ietf-ospf@2022-10-19.yang -e bfd -e explicit-router-id"
+ "ietf-ospf@2022-10-19.yang -e bfd -e explicit-router-id -e hybrid-interface"
"ietf-rip@2020-02-20.yang"
"iana-bfd-types@2021-10-21.yang"
"ietf-bfd-types@2022-09-22.yang"
diff --git a/src/confd/yang/confd/infix-routing.yang b/src/confd/yang/confd/infix-routing.yang
index 9daa8b50c..e11fe0806 100644
--- a/src/confd/yang/confd/infix-routing.yang
+++ b/src/confd/yang/confd/infix-routing.yang
@@ -26,6 +26,12 @@ module infix-routing {
contact "kernelkit@googlegroups.com";
description "Deviations and augments for ietf-routing, ietf-ospf, and ietf-rip.";
+ revision 2026-03-04 {
+ description "Remove interface-type deviation to expose standard ietf-ospf
+ interface types including point-to-multipoint and hybrid.
+ Un-deviate static-neighbors for non-broadcast P2MP support.";
+ }
+
revision 2025-12-02 {
description "Add configurable OSPF debug logging container.
Add RIP (Routing Information Protocol) support.";
@@ -247,18 +253,6 @@ module infix-routing {
}
/* OSPF */
- typedef infix-ospf-interface-type {
- type enumeration {
- enum broadcast {
- description
- "Specifies an OSPF broadcast multi-access network.";
- }
- enum point-to-point {
- description
- "Specifies an OSPF point-to-point network.";
- }
- }
- }
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol" {
@@ -367,11 +361,6 @@ module infix-routing {
}
}
- deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/ospf:interface-type" {
- deviate replace {
- type infix-ospf-interface-type;
- }
- }
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:auto-cost" {
deviate not-supported;
@@ -463,10 +452,6 @@ module infix-routing {
}
/* OSPF Area Interface */
- deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/ospf:static-neighbors"
- {
- deviate not-supported;
- }
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/ospf:multi-areas" {
deviate not-supported;
}
diff --git a/src/confd/yang/confd/infix-routing@2025-12-02.yang b/src/confd/yang/confd/infix-routing@2026-03-04.yang
similarity index 100%
rename from src/confd/yang/confd/infix-routing@2025-12-02.yang
rename to src/confd/yang/confd/infix-routing@2026-03-04.yang
diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py
index e4a19c9d4..dfb31337a 100755
--- a/src/statd/python/cli_pretty/cli_pretty.py
+++ b/src/statd/python/cli_pretty/cli_pretty.py
@@ -4838,7 +4838,7 @@ def show_ospf_interfaces(json_data):
state = target_iface.get('state', 'down')
cost = target_iface.get('cost', 0)
priority = target_iface.get('priority', 1)
- iface_type = target_iface.get('interface-type', 'unknown')
+ iface_type = target_iface.get('interface-type', '')
hello_interval = target_iface.get('hello-interval', 10)
dead_interval = target_iface.get('dead-interval', 40)
retransmit_interval = target_iface.get('retransmit-interval', 5)
@@ -4914,9 +4914,11 @@ def show_ospf_interfaces(json_data):
network_type_map = {
'point-to-point': 'POINTOPOINT',
'broadcast': 'BROADCAST',
- 'non-broadcast': 'NBMA'
+ 'non-broadcast': 'NBMA',
+ 'point-to-multipoint': 'POINTOMULTIPOINT',
+ 'hybrid': 'POINTOMULTIPOINT'
}
- network_type = network_type_map.get(iface_type, iface_type.upper())
+ network_type = network_type_map.get(iface_type, iface_type.upper() if iface_type else 'LOOPBACK')
print(f"{name} is up")
if ip_address:
@@ -4956,30 +4958,50 @@ def show_ospf_interfaces(json_data):
return
# Display table view (no specific interface)
- hdr = f"{'INTERFACE':<12} {'AREA':<12} {'STATE':<10} {'COST':<6} {'PRI':<4} {'DR':<15} {'BDR':<15} {'NBRS':<5}"
- print(Decore.invert(hdr))
+ type_display_map = {
+ 'point-to-point': 'P2P',
+ 'broadcast': 'Broadcast',
+ 'non-broadcast': 'NBMA',
+ 'point-to-multipoint': 'P2MP',
+ 'hybrid': 'Hybrid'
+ }
+
+ def fmt_state(state):
+ if state in ('dr', 'bdr'):
+ return state.upper()
+ if state == 'dr-other':
+ return 'DROther'
+ return state.capitalize()
+
+ table = SimpleTable([
+ Column('INTERFACE'),
+ Column('AREA'),
+ Column('TYPE'),
+ Column('STATE'),
+ Column('COST', 'right'),
+ Column('PRI', 'right'),
+ Column('DR'),
+ Column('BDR'),
+ Column('NBRS', 'right')
+ ])
for iface in all_interfaces:
name = iface.get('name', 'unknown')
area_id = iface.get('_area_id', '0.0.0.0')
state = iface.get('state', 'down')
+ iface_type = iface.get('interface-type', '')
cost = iface.get('cost', 0)
priority = iface.get('priority', 1)
dr_id = iface.get('dr-router-id', '-')
bdr_id = iface.get('bdr-router-id', '-')
neighbors = iface.get('neighbors', {}).get('neighbor', [])
- nbr_count = len(neighbors)
- # Capitalize state nicely
- state_display = state.upper() if state in ['dr', 'bdr'] else state.capitalize()
- if state == 'dr-other':
- state_display = 'DROther'
-
- # Shorten router IDs for display
- dr_display = dr_id if dr_id != '-' else '-'
- bdr_display = bdr_id if bdr_id != '-' else '-'
+ table.row(name, area_id,
+ type_display_map.get(iface_type, iface_type.capitalize() if iface_type else '-'),
+ fmt_state(state),
+ cost, priority, dr_id, bdr_id, len(neighbors))
- print(f"{name:<12} {area_id:<12} {state_display:<10} {cost:<6} {priority:<4} {dr_display:<15} {bdr_display:<15} {nbr_count:<5}")
+ table.print()
def show_ospf_neighbor(json_data):
diff --git a/src/statd/python/yanger/ietf_ospf.py b/src/statd/python/yanger/ietf_ospf.py
index d7e090150..bd6335d37 100644
--- a/src/statd/python/yanger/ietf_ospf.py
+++ b/src/statd/python/yanger/ietf_ospf.py
@@ -124,8 +124,15 @@ def add_areas(control_protocols):
interface["enabled"] = iface["ospfEnabled"]
if iface["networkType"] == "POINTOPOINT":
interface["interface-type"] = "point-to-point"
- if iface["networkType"] == "BROADCAST":
+ elif iface["networkType"] == "BROADCAST":
interface["interface-type"] = "broadcast"
+ elif iface["networkType"] == "POINTOMULTIPOINT":
+ if iface.get("p2mpNonBroadcast", False):
+ interface["interface-type"] = "point-to-multipoint"
+ else:
+ interface["interface-type"] = "hybrid"
+ elif iface["networkType"] == "NBMA":
+ interface["interface-type"] = "non-broadcast"
if iface.get("state"):
# Wev've never seen "DependUpon", and has no entry in
diff --git a/test/case/routing/Readme.adoc b/test/case/routing/Readme.adoc
index 68bfc2d7c..6152bc0ad 100644
--- a/test/case/routing/Readme.adoc
+++ b/test/case/routing/Readme.adoc
@@ -10,6 +10,8 @@ Tests verifying standard routing protocols and configuration:
- OSPF with BFD (Bidirectional Forwarding Detection)
- OSPF default route advertisement and propagation
- OSPF debug logging configuration and verification
+ - OSPF point-to-multipoint hybrid (broadcast) interface type
+ - OSPF point-to-multipoint (non-broadcast) interface type with static neighbors
- RIP basic neighbor discovery and route exchange
- RIP passive interface configuration
- RIP route redistribution (connected, static, and OSPF)
@@ -46,6 +48,14 @@ include::ospf_debug/Readme.adoc[]
<<<
+include::ospf_point_to_multipoint_hybrid/Readme.adoc[]
+
+<<<
+
+include::ospf_point_to_multipoint/Readme.adoc[]
+
+<<<
+
include::rip_basic/Readme.adoc[]
<<<
diff --git a/test/case/routing/all.yaml b/test/case/routing/all.yaml
index 8086180c5..83d7637e2 100644
--- a/test/case/routing/all.yaml
+++ b/test/case/routing/all.yaml
@@ -29,6 +29,12 @@
- name: OSPF Debug Logging
case: ospf_debug/test.py
+- name: OSPF Point-to-Multipoint Hybrid
+ case: ospf_point_to_multipoint_hybrid/test.py
+
+- name: OSPF Point-to-Multipoint
+ case: ospf_point_to_multipoint/test.py
+
- name: RIP Basic
case: rip_basic/test.py
diff --git a/test/case/routing/ospf_point_to_multipoint/__pycache__/test.cpython-312.pyc b/test/case/routing/ospf_point_to_multipoint/__pycache__/test.cpython-312.pyc
new file mode 100644
index 000000000..42f7f3331
Binary files /dev/null and b/test/case/routing/ospf_point_to_multipoint/__pycache__/test.cpython-312.pyc differ
diff --git a/test/case/routing/ospf_point_to_multipoint/test.py b/test/case/routing/ospf_point_to_multipoint/test.py
new file mode 100755
index 000000000..48c2ab630
--- /dev/null
+++ b/test/case/routing/ospf_point_to_multipoint/test.py
@@ -0,0 +1,341 @@
+#!/usr/bin/env python3
+"""OSPF Point-to-Multipoint
+
+Verify OSPF point-to-multipoint (non-broadcast) interface type by
+configuring three routers on a shared multi-access network with the
+ietf-ospf 'point-to-multipoint' interface type and static neighbors.
+This maps to FRR's 'point-to-multipoint non-broadcast' network type,
+which requires manual neighbor configuration since there is no
+multicast neighbor discovery.
+
+R2 acts as the hub, bridging two physical links (link1, link2) into a
+single broadcast domain (br0). R1 and R3 each connect to one of R2's
+ports. The test verifies that all routers form OSPF adjacencies via
+unicast, exchange routes, and that the interface type is correctly
+reported as point-to-multipoint.
+
+....
+ +------------------+ +------------------+
+ | R1 | | R3 |
+ | 10.0.1.1/32 | | 10.0.3.1/32 |
+ | (lo) | | (lo) |
+ +--------+---------+ +--------+---------+
+ | .1 | .3
+ | +------------------+ |
+ +----link1------+ R2 +------link2--------+
+ | 10.0.2.1/32 |
+ | (lo) |
+ | br0: 10.0.123.2 |
+ +------------------+
+ 10.0.123.0/24
+ (P2MP non-broadcast / shared segment)
+....
+"""
+
+import infamy
+import infamy.route as route
+from infamy.util import until, parallel
+
+
+def config_target1(target, link, data):
+ target.put_config_dicts({
+ "ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": link,
+ "enabled": True,
+ "ipv4": {
+ "forwarding": True,
+ "address": [{
+ "ip": "10.0.123.1",
+ "prefix-length": 24
+ }]
+ }
+ },
+ {
+ "name": data,
+ "enabled": True,
+ "ipv4": {
+ "forwarding": True,
+ "address": [{
+ "ip": "10.0.10.1",
+ "prefix-length": 24
+ }]
+ }
+ },
+ {
+ "name": "lo",
+ "enabled": True,
+ "ipv4": {
+ "address": [{
+ "ip": "10.0.1.1",
+ "prefix-length": 32
+ }]
+ }
+ }
+ ]
+ }
+ },
+ "ietf-routing": {
+ "routing": {
+ "control-plane-protocols": {
+ "control-plane-protocol": [{
+ "type": "infix-routing:ospfv2",
+ "name": "default",
+ "ospf": {
+ "redistribute": {
+ "redistribute": [{
+ "protocol": "connected"
+ }]
+ },
+ "areas": {
+ "area": [{
+ "area-id": "0.0.0.0",
+ "interfaces": {
+ "interface": [{
+ "name": link,
+ "enabled": True,
+ "hello-interval": 1,
+ "dead-interval": 3,
+ "interface-type": "point-to-multipoint",
+ "static-neighbors": {
+ "neighbor": [
+ {"identifier": "10.0.123.2"},
+ {"identifier": "10.0.123.3"}
+ ]
+ }
+ }, {
+ "name": "lo",
+ "enabled": True
+ }]
+ }
+ }]
+ }
+ }
+ }]
+ }
+ }
+ }
+ })
+
+
+def config_target2(target, link1, link2):
+ target.put_config_dicts({
+ "ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": "br0",
+ "type": "infix-if-type:bridge",
+ "enabled": True,
+ "ipv4": {
+ "forwarding": True,
+ "address": [{
+ "ip": "10.0.123.2",
+ "prefix-length": 24
+ }]
+ }
+ },
+ {
+ "name": link1,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {
+ "bridge": "br0"
+ }
+ },
+ {
+ "name": link2,
+ "enabled": True,
+ "infix-interfaces:bridge-port": {
+ "bridge": "br0"
+ }
+ },
+ {
+ "name": "lo",
+ "enabled": True,
+ "ipv4": {
+ "address": [{
+ "ip": "10.0.2.1",
+ "prefix-length": 32
+ }]
+ }
+ }
+ ]
+ }
+ },
+ "ietf-routing": {
+ "routing": {
+ "control-plane-protocols": {
+ "control-plane-protocol": [{
+ "type": "infix-routing:ospfv2",
+ "name": "default",
+ "ospf": {
+ "redistribute": {
+ "redistribute": [{
+ "protocol": "connected"
+ }]
+ },
+ "areas": {
+ "area": [{
+ "area-id": "0.0.0.0",
+ "interfaces": {
+ "interface": [{
+ "name": "br0",
+ "enabled": True,
+ "hello-interval": 1,
+ "dead-interval": 3,
+ "interface-type": "point-to-multipoint",
+ "static-neighbors": {
+ "neighbor": [
+ {"identifier": "10.0.123.1"},
+ {"identifier": "10.0.123.3"}
+ ]
+ }
+ }, {
+ "name": "lo",
+ "enabled": True
+ }]
+ }
+ }]
+ }
+ }
+ }]
+ }
+ }
+ }
+ })
+
+
+def config_target3(target, link, data):
+ target.put_config_dicts({
+ "ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": link,
+ "enabled": True,
+ "ipv4": {
+ "forwarding": True,
+ "address": [{
+ "ip": "10.0.123.3",
+ "prefix-length": 24
+ }]
+ }
+ },
+ {
+ "name": data,
+ "enabled": True,
+ "ipv4": {
+ "forwarding": True,
+ "address": [{
+ "ip": "10.0.30.1",
+ "prefix-length": 24
+ }]
+ }
+ },
+ {
+ "name": "lo",
+ "enabled": True,
+ "ipv4": {
+ "address": [{
+ "ip": "10.0.3.1",
+ "prefix-length": 32
+ }]
+ }
+ }
+ ]
+ }
+ },
+ "ietf-routing": {
+ "routing": {
+ "control-plane-protocols": {
+ "control-plane-protocol": [{
+ "type": "infix-routing:ospfv2",
+ "name": "default",
+ "ospf": {
+ "redistribute": {
+ "redistribute": [{
+ "protocol": "connected"
+ }]
+ },
+ "areas": {
+ "area": [{
+ "area-id": "0.0.0.0",
+ "interfaces": {
+ "interface": [{
+ "name": link,
+ "enabled": True,
+ "hello-interval": 1,
+ "dead-interval": 3,
+ "interface-type": "point-to-multipoint",
+ "static-neighbors": {
+ "neighbor": [
+ {"identifier": "10.0.123.1"},
+ {"identifier": "10.0.123.2"}
+ ]
+ }
+ }, {
+ "name": "lo",
+ "enabled": True
+ }]
+ }
+ }]
+ }
+ }
+ }]
+ }
+ }
+ }
+ })
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUTs"):
+ env = infamy.Env()
+ R1 = env.attach("R1", "mgmt")
+ R2 = env.attach("R2", "mgmt")
+ R3 = env.attach("R3", "mgmt")
+
+
+ with test.step("Configure targets"):
+ _, R1link = env.ltop.xlate("R1", "link")
+ _, R1data = env.ltop.xlate("R1", "data")
+ _, R2link1 = env.ltop.xlate("R2", "link1")
+ _, R2link2 = env.ltop.xlate("R2", "link2")
+ _, R3link = env.ltop.xlate("R3", "link")
+ _, R3data = env.ltop.xlate("R3", "data")
+
+ parallel(config_target1(R1, R1link, R1data),
+ config_target2(R2, R2link1, R2link2),
+ config_target3(R3, R3link, R3data))
+
+ with test.step("Wait for OSPF routes"):
+ print("Waiting for OSPF routes from all routers")
+ until(lambda: route.ipv4_route_exist(R1, "10.0.2.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
+ until(lambda: route.ipv4_route_exist(R1, "10.0.3.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
+ until(lambda: route.ipv4_route_exist(R2, "10.0.1.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
+ until(lambda: route.ipv4_route_exist(R2, "10.0.3.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
+ until(lambda: route.ipv4_route_exist(R3, "10.0.1.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
+ until(lambda: route.ipv4_route_exist(R3, "10.0.2.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
+
+ with test.step("Verify interface type is point-to-multipoint"):
+ print("Checking OSPF interface type on all routers")
+ assert route.ospf_get_interface_type(R1, "0.0.0.0", R1link) == "point-to-multipoint"
+ assert route.ospf_get_interface_type(R2, "0.0.0.0", "br0") == "point-to-multipoint"
+ assert route.ospf_get_interface_type(R3, "0.0.0.0", R3link) == "point-to-multipoint"
+
+ with test.step("Verify connectivity between all DUTs"):
+ _, hport1 = env.ltop.xlate("PC", "data1")
+ _, hport2 = env.ltop.xlate("PC", "data2")
+ with infamy.IsolatedMacVlan(hport1) as ns1, \
+ infamy.IsolatedMacVlan(hport2) as ns2:
+ ns1.addip("10.0.10.2")
+ ns2.addip("10.0.30.2")
+ ns1.addroute("10.0.3.1/32", "10.0.10.1")
+ ns2.addroute("10.0.1.1/32", "10.0.30.1")
+ parallel(
+ lambda: ns1.must_reach("10.0.3.1"),
+ lambda: ns2.must_reach("10.0.1.1"),
+ )
+ test.succeed()
diff --git a/test/case/routing/ospf_point_to_multipoint/topology.dot b/test/case/routing/ospf_point_to_multipoint/topology.dot
new file mode 100644
index 000000000..4d9fc970f
--- /dev/null
+++ b/test/case/routing/ospf_point_to_multipoint/topology.dot
@@ -0,0 +1,39 @@
+graph "3x1" {
+ layout="neato";
+ overlap="false";
+ esep="+20";
+ size=10
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ PC [
+ label="PC | {