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 | { mgmt1 | data1 | \n\n\n\n | mgmt2 | mgmt3 | data2 }", + pos="20,30!", + requires="controller", + ]; + + R1 [ + label="{ mgmt | data | link } | R1 \n 10.0.1.1/32 \n(lo)", + pos="160,60!", + requires="infix", + ]; + R2 [ + label="{ link1 | mgmt | link2 } | R2 \n 10.0.2.1/32 \n(lo) \n(br0: 10.0.123.2/24)", + pos="160,30!", + requires="infix", + ]; + R3 [ + label="{ link | mgmt | data } | R3 \n 10.0.3.1/32 \n(lo)", + pos="160,0!", + requires="infix", + ]; + + PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt3 -- R3:mgmt [requires="mgmt", color="lightgray"] + PC:data1 -- R1:data [taillabel="10.0.10.2/24", headlabel="10.0.10.1/24"] + R3:data -- PC:data2 [taillabel="10.0.30.1/24", headlabel="10.0.30.2/24"] + R1:link -- R2:link1 [taillabel="10.0.123.1/24"] + R3:link -- R2:link2 [taillabel="10.0.123.3/24"] +} diff --git a/test/case/routing/ospf_point_to_multipoint_hybrid/__pycache__/test.cpython-312.pyc b/test/case/routing/ospf_point_to_multipoint_hybrid/__pycache__/test.cpython-312.pyc new file mode 100644 index 000000000..0729f22e0 Binary files /dev/null and b/test/case/routing/ospf_point_to_multipoint_hybrid/__pycache__/test.cpython-312.pyc differ diff --git a/test/case/routing/ospf_point_to_multipoint_hybrid/test.py b/test/case/routing/ospf_point_to_multipoint_hybrid/test.py new file mode 100755 index 000000000..f067ad832 --- /dev/null +++ b/test/case/routing/ospf_point_to_multipoint_hybrid/test.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +"""OSPF Point-to-Multipoint Hybrid + +Verify OSPF point-to-multipoint hybrid (broadcast) interface type by +configuring three routers on a shared multi-access network with the +ietf-ospf 'hybrid' interface type. This maps to FRR's 'point-to-multipoint' +network type, which uses multicast for 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, exchange +routes, and that the interface type is correctly reported as hybrid. + +.... + +------------------+ +------------------+ + | 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 hybrid / 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-system": { + "system": { + "hostname": "R1" + } + }, + "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, + "interface-type": "hybrid", + "hello-interval": 1, + "dead-interval": 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-system": { + "system": { + "hostname": "R2" + } + }, + "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, + "interface-type": "hybrid", + "hello-interval": 1, + "dead-interval": 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-system": { + "system": { + "hostname": "R3" + } + }, + "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, + "interface-type": "hybrid", + "hello-interval": 1, + "dead-interval": 3 + }, { + "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 to converge") + 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 hybrid"): + print("Checking OSPF interface type on all routers") + assert route.ospf_get_interface_type(R1, "0.0.0.0", R1link) == "hybrid" + assert route.ospf_get_interface_type(R2, "0.0.0.0", "br0") == "hybrid" + assert route.ospf_get_interface_type(R3, "0.0.0.0", R3link) == "hybrid" + + 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_hybrid/topology.dot b/test/case/routing/ospf_point_to_multipoint_hybrid/topology.dot new file mode 100644 index 000000000..6a345571c --- /dev/null +++ b/test/case/routing/ospf_point_to_multipoint_hybrid/topology.dot @@ -0,0 +1,39 @@ +graph "3r-p2mp" { + 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 | { mgmt1 | data1 | \n\n\n\n | mgmt2 | mgmt3 | data2 }", + pos="20,30!", + requires="controller", + ]; + + R1 [ + label="{ mgmt | data | link } | R1 \n 10.0.1.1/32 \n(lo)", + pos="160,60!", + requires="infix", + ]; + R2 [ + label="{ link1 | mgmt | link2 } | R2 \n 10.0.2.1/32 \n(lo) \n(br0: 10.0.123.2/24)", + pos="160,30!", + requires="infix", + ]; + R3 [ + label="{ link | mgmt | data } | R3 \n 10.0.3.1/32 \n(lo)", + pos="160,0!", + requires="infix", + ]; + + PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt3 -- R3:mgmt [requires="mgmt", color="lightgray"] + PC:data1 -- R1:data [taillabel="10.0.10.2/24", headlabel="10.0.10.1/24"] + R3:data -- PC:data2 [taillabel="10.0.30.1/24", headlabel="10.0.30.2/24"] + R1:link -- R2:link1 [taillabel="10.0.123.1/24"] + R3:link -- R2:link2 [taillabel="10.0.123.3/24"] +}