From 271b17c23b0d0ac5c1f57bebc29a66ebe9365599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Sat, 7 Mar 2026 18:00:45 +0100 Subject: [PATCH] ospf: add point-to-multipoint and hybrid interface type support --- doc/ChangeLog.md | 2 + doc/routing.md | 60 +++ src/confd/src/routing.c | 36 +- src/confd/yang/confd.inc | 2 +- src/confd/yang/confd/infix-routing.yang | 27 +- ...-02.yang => infix-routing@2026-03-04.yang} | 0 src/statd/python/cli_pretty/cli_pretty.py | 52 ++- src/statd/python/yanger/ietf_ospf.py | 9 +- test/case/routing/Readme.adoc | 10 + test/case/routing/all.yaml | 6 + .../ospf_point_to_multipoint/Readme.adoc | 1 + .../__pycache__/test.cpython-312.pyc | Bin 0 -> 9295 bytes .../ospf_point_to_multipoint/test.adoc | 49 +++ .../routing/ospf_point_to_multipoint/test.py | 341 ++++++++++++++++++ .../ospf_point_to_multipoint/topology.dot | 39 ++ .../ospf_point_to_multipoint/topology.svg | 113 ++++++ .../Readme.adoc | 1 + .../__pycache__/test.cpython-312.pyc | Bin 0 -> 9083 bytes .../ospf_point_to_multipoint_hybrid/test.adoc | 46 +++ .../ospf_point_to_multipoint_hybrid/test.py | 334 +++++++++++++++++ .../topology.dot | 39 ++ .../topology.svg | 113 ++++++ 22 files changed, 1241 insertions(+), 39 deletions(-) rename src/confd/yang/confd/{infix-routing@2025-12-02.yang => infix-routing@2026-03-04.yang} (100%) create mode 120000 test/case/routing/ospf_point_to_multipoint/Readme.adoc create mode 100644 test/case/routing/ospf_point_to_multipoint/__pycache__/test.cpython-312.pyc create mode 100644 test/case/routing/ospf_point_to_multipoint/test.adoc create mode 100755 test/case/routing/ospf_point_to_multipoint/test.py create mode 100644 test/case/routing/ospf_point_to_multipoint/topology.dot create mode 100644 test/case/routing/ospf_point_to_multipoint/topology.svg create mode 120000 test/case/routing/ospf_point_to_multipoint_hybrid/Readme.adoc create mode 100644 test/case/routing/ospf_point_to_multipoint_hybrid/__pycache__/test.cpython-312.pyc create mode 100644 test/case/routing/ospf_point_to_multipoint_hybrid/test.adoc create mode 100755 test/case/routing/ospf_point_to_multipoint_hybrid/test.py create mode 100644 test/case/routing/ospf_point_to_multipoint_hybrid/topology.dot create mode 100644 test/case/routing/ospf_point_to_multipoint_hybrid/topology.svg diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index a13000f0e..fa043c996 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -24,6 +24,8 @@ All notable changes to the project are documented in this file. - `set ssh known-hosts ` — pre-enroll a host key received out-of-band, e.g. after a factory reset changes the device host key - `no ssh known-hosts ` — remove a stale or changed host key entry +- Add OSPF point-to-multipoint (P2MP) and hybrid interface type support + ### Fixes diff --git a/doc/routing.md b/doc/routing.md index e6b0b97ba..f8b99e588 100644 --- a/doc/routing.md +++ b/doc/routing.md @@ -152,6 +152,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 f105e7e28..e2fcb892d 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 703468c53..1fee3f0f7 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -4923,7 +4923,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) @@ -4999,9 +4999,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: @@ -5041,30 +5043,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/Readme.adoc b/test/case/routing/ospf_point_to_multipoint/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/routing/ospf_point_to_multipoint/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file 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 0000000000000000000000000000000000000000..42f7f33319ce9476b68360572c6f4226ddbe7135 GIT binary patch literal 9295 zcmdrxTW}jka=QQ)FA@MjQG7p^r1$_ykOU}N56hBe*|dETA%;|Bo05;U#4L!VKrFJm zlte@yy2QDYF0RMES^4=$Yy1?&;~7?wKC_#^>`g@Xh@AzBF`%Vg3^v)>mUzwwGNDbB7_!JVRJw zOR@7TEp03Ic{__Zd&;rG&2uZxc_*|TpD^<-!U1#>CqR#l5!vMiOKhopU{)uY`5Lii zxuw)&)rji>Yt$zhmZ5T5t-exe$(i?y{^hO`-l`Ed@jS2@t@*k=XuM@K!98ed%4q6| z4gV~;EVIW9YRhK;a{DcRJUrNv(J)!33Q)7IN!K0Zhr-L(>}PpBv8TKyf5y$ z3htJDaMwIH@78^BSI)Z)c8wE$F0oE*6q_E{4GROtEe6GAv84>-5$nZPvCYC*CG6?% zx7NY(wT2PeNxg-kyO$#eK4D11mj^BTnApau*rux3=Bn71s@T>_?Dw2Uo29ksb?v0x z!cdLv%g3#KS}Mgl(jhilHY1&v2N_1S{eby%7A#SnBS<<(*SsHM!Yo> zoZCKjRyXOfR?YOs&V6zBR&aNLRoLY>OZKyBVrNOi`R+Zby2_|}#O~#HE&ftT4p}rX z)1ImnePtBg%kP(Xts3bk1D57gN;R565-QPv#!fN>J8$?Cri88Bc`s{2H2ot>hf--p z`sY1z&DP6A7?r(hW0{X&Wm)EP+o!+2X8(w}%0Ab_e)`)zdw#lSPpEp&r-?oA%X{F{ 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О`p^QIKM%6x30{SWR;;dc~t>mF_qXIhyXkOig#FnwZh;@1(Ejb`gNn zIHhO|HsS-i17wA8l;t)@58 z#1RtBh2q9rzzkM*ChYXq{5hRZmh^;H6@-@{u21Wpp5$0V3oX`W!Wu97wg!53;x^0|8GqDe=q~TbI!1= z>vsv;c`VX4Y~>R+#u>bE;U^b=bn!1f&NJiefzL_KBBXjNkIyWN*H#n=qdCzul>i*PbaN@gQ7u4Uq|G{Q((x@Bw-*Yz4x*NZ-t@eMtMaii*6UB)Cvt(2o2%ajMji)7 z?$3VJ@()v+fioNLUiv0*X~TVKBXFs*|DDaiTN_gszX@F2a9`XAT&(Qh#s4^P377C= z79Izz`5xQ!9N%JUIsfjpYT9gwR9nL`ys!7>g>QYmkA1!OrZ#=UdB)@M-&b>$Xgi>D zW6@k~;IsE{z5n^;C$&A5^|QCme$jEi=1J{g>?yUrt8MtME(-sC&)I$c9Un;A!BKWx zpfBWboaZNJe{}Yf*p9%$cBgxO#|B%J?)1WrA6Axb&%#bKTHaXJ!cIM!z_2?sKps`S zKt-EXIsjUwu=8$Fc0v6|@XNoikA>|2%${~n2f3elTA=Z>o--ZOo!q~i!p6^=PuENj zbHC`t#xG8sZkRsG{VIr!Uk#lO#yYrPzkVPVbp6JEx<1BvzUJM~_ + + + + + +3x1 + + + +PC + +PC + +mgmt1 + +data1 + + +mgmt2 + +mgmt3 + +data2 + + + +R1 + +mgmt + +data + +link + +R1 + 10.0.1.1/32 +(lo) + + + +PC:mgmt1--R1:mgmt + + + + +PC:data1--R1:data + +10.0.10.1/24 +10.0.10.2/24 + + + +R2 + +link1 + +mgmt + +link2 + +R2 + 10.0.2.1/32 +(lo) +(br0: 10.0.123.2/24) + + + +PC:mgmt2--R2:mgmt + + + + +R3 + +link + +mgmt + +data + +R3 + 10.0.3.1/32 +(lo) + + + +PC:mgmt3--R3:mgmt + + + + +R1:link--R2:link1 + +10.0.123.1/24 + + + +R3:data--PC:data2 + +10.0.30.2/24 +10.0.30.1/24 + + + +R3:link--R2:link2 + +10.0.123.3/24 + + + diff --git a/test/case/routing/ospf_point_to_multipoint_hybrid/Readme.adoc b/test/case/routing/ospf_point_to_multipoint_hybrid/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/routing/ospf_point_to_multipoint_hybrid/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file 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 0000000000000000000000000000000000000000..0729f22e077b267d01c288c5f2c0b7825da80db7 GIT binary patch literal 9083 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 qdK>GH1tAgeKlZvn&f~CYbNvsyz_YCY literal 0 HcmV?d00001 diff --git a/test/case/routing/ospf_point_to_multipoint_hybrid/test.adoc b/test/case/routing/ospf_point_to_multipoint_hybrid/test.adoc new file mode 100644 index 000000000..7bd202af0 --- /dev/null +++ b/test/case/routing/ospf_point_to_multipoint_hybrid/test.adoc @@ -0,0 +1,46 @@ +=== OSPF Point-to-Multipoint Hybrid + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_point_to_multipoint_hybrid] + +==== Description + +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) +.... + +==== Topology + +image::topology.svg[OSPF Point-to-Multipoint Hybrid topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure targets +. Wait for OSPF routes +. Verify interface type is hybrid +. Verify connectivity between all DUTs + + 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"] +} diff --git a/test/case/routing/ospf_point_to_multipoint_hybrid/topology.svg b/test/case/routing/ospf_point_to_multipoint_hybrid/topology.svg new file mode 100644 index 000000000..94d30fe36 --- /dev/null +++ b/test/case/routing/ospf_point_to_multipoint_hybrid/topology.svg @@ -0,0 +1,113 @@ + + + + + + +3r-p2mp + + + +PC + +PC + +mgmt1 + +data1 + + +mgmt2 + +mgmt3 + +data2 + + + +R1 + +mgmt + +data + +link + +R1 + 10.0.1.1/32 +(lo) + + + +PC:mgmt1--R1:mgmt + + + + +PC:data1--R1:data + +10.0.10.1/24 +10.0.10.2/24 + + + +R2 + +link1 + +mgmt + +link2 + +R2 + 10.0.2.1/32 +(lo) +(br0: 10.0.123.2/24) + + + +PC:mgmt2--R2:mgmt + + + + +R3 + +link + +mgmt + +data + +R3 + 10.0.3.1/32 +(lo) + + + +PC:mgmt3--R3:mgmt + + + + +R1:link--R2:link1 + +10.0.123.1/24 + + + +R3:data--PC:data2 + +10.0.30.2/24 +10.0.30.1/24 + + + +R3:link--R2:link2 + +10.0.123.3/24 + + +