ospf: add point-to-multipoint and hybrid interface type support

This commit is contained in:
Mattias Walström
2026-03-10 12:44:47 +01:00
parent 5bb21e5221
commit 312b856fba
22 changed files with 1242 additions and 40 deletions
+2
View File
@@ -25,6 +25,8 @@ All notable changes to the project are documented in this file.
- `set ssh known-hosts <host> <keytype> <pubkey>` — pre-enroll a host key
received out-of-band, e.g. after a factory reset changes the device host key
- `no ssh known-hosts <host>` — remove a stale or changed host key entry
- Add OSPF point-to-multipoint (P2MP) and hybrid interface type support
### Fixes
+60
View File
@@ -152,6 +152,66 @@ an Ethernet interface can be done as follows.
admin@example:/config/routing/…/ospf/area/0.0.0.0/interface/e0/>
</code></pre>
### 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.
<pre class="cli"><code>admin@example:/config/> <b>edit routing control-plane-protocol ospfv2 name default ospf</b>
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 interface-type hybrid</b>
admin@example:/config/routing/…/ospf/> <b>leave</b>
admin@example:/>
</code></pre>
#### 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).
<pre class="cli"><code>admin@example:/config/> <b>edit routing control-plane-protocol ospfv2 name default ospf</b>
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 interface-type point-to-multipoint</b>
admin@example:/config/routing/…/ospf/> <b>leave</b>
admin@example:/>
</code></pre>
### 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.
<pre class="cli"><code>admin@example:/config/> <b>edit routing control-plane-protocol ospfv2 name default ospf</b>
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 static-neighbors neighbor 10.0.123.2</b>
admin@example:/config/routing/…/ospf/> <b>set area 0.0.0.0 interface e0 static-neighbors neighbor 10.0.123.3</b>
admin@example:/config/routing/…/ospf/> <b>leave</b>
admin@example:/>
</code></pre>
> [!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
+35 -1
View File
@@ -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. */
+2 -2
View File
@@ -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"
@@ -31,7 +31,7 @@ MODULES=(
"ieee802-dot1q-types@2022-10-29.yang"
"infix-ip@2025-11-02.yang"
"infix-if-type@2026-01-07.yang"
"infix-routing@2025-12-02.yang"
"infix-routing@2026-03-04.yang "
"ieee802-dot1ab-lldp@2022-03-15.yang"
"infix-lldp@2025-05-05.yang"
"infix-dhcp-common@2025-12-21.yang"
+6 -21
View File
@@ -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;
}
+37 -15
View File
@@ -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'
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))
# Shorten router IDs for display
dr_display = dr_id if dr_id != '-' else '-'
bdr_display = bdr_id if bdr_id != '-' else '-'
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):
+8 -1
View File
@@ -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
+10
View File
@@ -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[]
<<<
+6
View File
@@ -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
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,49 @@
=== OSPF Point-to-Multipoint
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_point_to_multipoint]
==== Description
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)
....
==== Topology
image::topology.svg[OSPF Point-to-Multipoint topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUTs
. Configure targets
. Wait for OSPF routes
. Verify interface type is point-to-multipoint
. Verify connectivity between all DUTs
+341
View File
@@ -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()
@@ -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> mgmt1 | <data1> data1 | \n\n\n\n | <mgmt2> mgmt2 | <mgmt3> mgmt3 | <data2> data2 }",
pos="20,30!",
requires="controller",
];
R1 [
label="{ <mgmt> mgmt | <data> data | <link> link } | R1 \n 10.0.1.1/32 \n(lo)",
pos="160,60!",
requires="infix",
];
R2 [
label="{ <link1> link1 | <mgmt> mgmt | <link2> 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> link | <mgmt> mgmt | <data> 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"]
}
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 3x1 Pages: 1 -->
<svg width="720pt" height="312pt"
viewBox="0.00 0.00 720.00 312.15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(0.98 0.98) rotate(0) translate(4 314.02)">
<title>3x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-314.02 729.56,-314.02 729.56,4 -4,4"/>
<!-- PC -->
<g id="node1" class="node">
<title>PC</title>
<polygon fill="none" stroke="black" points="0,-61.51 0,-248.51 91,-248.51 91,-61.51 0,-61.51"/>
<text text-anchor="middle" x="16.5" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">PC</text>
<polyline fill="none" stroke="black" points="33,-61.51 33,-248.51 "/>
<text text-anchor="middle" x="62" y="-233.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="33,-225.51 91,-225.51 "/>
<text text-anchor="middle" x="62" y="-210.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="33,-202.51 91,-202.51 "/>
<polyline fill="none" stroke="black" points="33,-130.51 91,-130.51 "/>
<text text-anchor="middle" x="62" y="-115.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
<polyline fill="none" stroke="black" points="33,-107.51 91,-107.51 "/>
<text text-anchor="middle" x="62" y="-92.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt3</text>
<polyline fill="none" stroke="black" points="33,-84.51 91,-84.51 "/>
<text text-anchor="middle" x="62" y="-69.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
</g>
<!-- R1 -->
<g id="node2" class="node">
<title>R1</title>
<polygon fill="none" stroke="black" points="518.56,-240.52 518.56,-309.52 692.56,-309.52 692.56,-240.52 518.56,-240.52"/>
<text text-anchor="middle" x="543.56" y="-294.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="518.56,-286.52 568.56,-286.52 "/>
<text text-anchor="middle" x="543.56" y="-271.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="518.56,-263.52 568.56,-263.52 "/>
<text text-anchor="middle" x="543.56" y="-248.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="568.56,-240.52 568.56,-309.52 "/>
<text text-anchor="middle" x="630.56" y="-286.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1 </text>
<text text-anchor="middle" x="630.56" y="-271.32" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 10.0.1.1/32 </text>
<text text-anchor="middle" x="630.56" y="-256.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
</g>
<!-- PC&#45;&#45;R1 -->
<g id="edge1" class="edge">
<title>PC:mgmt1&#45;&#45;R1:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-237.01C91.5,-237.01 518.56,-298.02 518.56,-298.02"/>
</g>
<!-- PC&#45;&#45;R1 -->
<g id="edge4" class="edge">
<title>PC:data1&#45;&#45;R1:data</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-214.01C91.5,-214.01 518.56,-275.02 518.56,-275.02"/>
<text text-anchor="middle" x="473.06" y="-278.82" font-family="DejaVu Serif, Book" font-size="14.00">10.0.10.1/24</text>
<text text-anchor="middle" x="137" y="-217.81" font-family="DejaVu Serif, Book" font-size="14.00">10.0.10.2/24</text>
</g>
<!-- R2 -->
<g id="node3" class="node">
<title>R2</title>
<polygon fill="none" stroke="black" points="485.56,-120.51 485.56,-189.51 725.56,-189.51 725.56,-120.51 485.56,-120.51"/>
<text text-anchor="middle" x="514.56" y="-174.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">link1</text>
<polyline fill="none" stroke="black" points="485.56,-166.51 543.56,-166.51 "/>
<text text-anchor="middle" x="514.56" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="485.56,-143.51 543.56,-143.51 "/>
<text text-anchor="middle" x="514.56" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">link2</text>
<polyline fill="none" stroke="black" points="543.56,-120.51 543.56,-189.51 "/>
<text text-anchor="middle" x="634.56" y="-173.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2 </text>
<text text-anchor="middle" x="634.56" y="-158.81" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 10.0.2.1/32 </text>
<text text-anchor="middle" x="634.56" y="-143.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo) </text>
<text text-anchor="middle" x="634.56" y="-128.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">(br0: 10.0.123.2/24)</text>
</g>
<!-- PC&#45;&#45;R2 -->
<g id="edge2" class="edge">
<title>PC:mgmt2&#45;&#45;R2:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-119.01C91.5,-119.01 485.56,-155.01 485.56,-155.01"/>
</g>
<!-- R3 -->
<g id="node4" class="node">
<title>R3</title>
<polygon fill="none" stroke="black" points="518.56,-0.5 518.56,-69.5 692.56,-69.5 692.56,-0.5 518.56,-0.5"/>
<text text-anchor="middle" x="543.56" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="518.56,-46.5 568.56,-46.5 "/>
<text text-anchor="middle" x="543.56" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="518.56,-23.5 568.56,-23.5 "/>
<text text-anchor="middle" x="543.56" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="568.56,-0.5 568.56,-69.5 "/>
<text text-anchor="middle" x="630.56" y="-46.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R3 </text>
<text text-anchor="middle" x="630.56" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 10.0.3.1/32 </text>
<text text-anchor="middle" x="630.56" y="-16.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
</g>
<!-- PC&#45;&#45;R3 -->
<g id="edge3" class="edge">
<title>PC:mgmt3&#45;&#45;R3:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-96.01C91.5,-96.01 518.56,-35 518.56,-35"/>
</g>
<!-- R1&#45;&#45;R2 -->
<g id="edge6" class="edge">
<title>R1:link&#45;&#45;R2:link1</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M543.56,-240.02C543.56,-240.02 514.56,-190.01 514.56,-190.01"/>
<text text-anchor="middle" x="493.56" y="-228.82" font-family="DejaVu Serif, Book" font-size="14.00">10.0.123.1/24</text>
</g>
<!-- R3&#45;&#45;PC -->
<g id="edge5" class="edge">
<title>R3:data&#45;&#45;PC:data2</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M518.56,-12C518.56,-12 91.5,-73.01 91.5,-73.01"/>
<text text-anchor="middle" x="137" y="-76.81" font-family="DejaVu Serif, Book" font-size="14.00">10.0.30.2/24</text>
<text text-anchor="middle" x="473.06" y="-15.8" font-family="DejaVu Serif, Book" font-size="14.00">10.0.30.1/24</text>
</g>
<!-- R3&#45;&#45;R2 -->
<g id="edge7" class="edge">
<title>R3:link&#45;&#45;R2:link2</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M543.56,-70C543.56,-70 514.56,-120.01 514.56,-120.01"/>
<text text-anchor="middle" x="493.56" y="-73.8" font-family="DejaVu Serif, Book" font-size="14.00">10.0.123.3/24</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

@@ -0,0 +1 @@
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
+334
View File
@@ -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()
@@ -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> mgmt1 | <data1> data1 | \n\n\n\n | <mgmt2> mgmt2 | <mgmt3> mgmt3 | <data2> data2 }",
pos="20,30!",
requires="controller",
];
R1 [
label="{ <mgmt> mgmt | <data> data | <link> link } | R1 \n 10.0.1.1/32 \n(lo)",
pos="160,60!",
requires="infix",
];
R2 [
label="{ <link1> link1 | <mgmt> mgmt | <link2> 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> link | <mgmt> mgmt | <data> 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"]
}
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 3r&#45;p2mp Pages: 1 -->
<svg width="720pt" height="312pt"
viewBox="0.00 0.00 720.00 312.15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(0.98 0.98) rotate(0) translate(4 314.02)">
<title>3r&#45;p2mp</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-314.02 729.56,-314.02 729.56,4 -4,4"/>
<!-- PC -->
<g id="node1" class="node">
<title>PC</title>
<polygon fill="none" stroke="black" points="0,-61.51 0,-248.51 91,-248.51 91,-61.51 0,-61.51"/>
<text text-anchor="middle" x="16.5" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">PC</text>
<polyline fill="none" stroke="black" points="33,-61.51 33,-248.51 "/>
<text text-anchor="middle" x="62" y="-233.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="33,-225.51 91,-225.51 "/>
<text text-anchor="middle" x="62" y="-210.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="33,-202.51 91,-202.51 "/>
<polyline fill="none" stroke="black" points="33,-130.51 91,-130.51 "/>
<text text-anchor="middle" x="62" y="-115.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
<polyline fill="none" stroke="black" points="33,-107.51 91,-107.51 "/>
<text text-anchor="middle" x="62" y="-92.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt3</text>
<polyline fill="none" stroke="black" points="33,-84.51 91,-84.51 "/>
<text text-anchor="middle" x="62" y="-69.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
</g>
<!-- R1 -->
<g id="node2" class="node">
<title>R1</title>
<polygon fill="none" stroke="black" points="518.56,-240.52 518.56,-309.52 692.56,-309.52 692.56,-240.52 518.56,-240.52"/>
<text text-anchor="middle" x="543.56" y="-294.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="518.56,-286.52 568.56,-286.52 "/>
<text text-anchor="middle" x="543.56" y="-271.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="518.56,-263.52 568.56,-263.52 "/>
<text text-anchor="middle" x="543.56" y="-248.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="568.56,-240.52 568.56,-309.52 "/>
<text text-anchor="middle" x="630.56" y="-286.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1 </text>
<text text-anchor="middle" x="630.56" y="-271.32" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 10.0.1.1/32 </text>
<text text-anchor="middle" x="630.56" y="-256.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
</g>
<!-- PC&#45;&#45;R1 -->
<g id="edge1" class="edge">
<title>PC:mgmt1&#45;&#45;R1:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-237.01C91.5,-237.01 518.56,-298.02 518.56,-298.02"/>
</g>
<!-- PC&#45;&#45;R1 -->
<g id="edge4" class="edge">
<title>PC:data1&#45;&#45;R1:data</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-214.01C91.5,-214.01 518.56,-275.02 518.56,-275.02"/>
<text text-anchor="middle" x="473.06" y="-278.82" font-family="DejaVu Serif, Book" font-size="14.00">10.0.10.1/24</text>
<text text-anchor="middle" x="137" y="-217.81" font-family="DejaVu Serif, Book" font-size="14.00">10.0.10.2/24</text>
</g>
<!-- R2 -->
<g id="node3" class="node">
<title>R2</title>
<polygon fill="none" stroke="black" points="485.56,-120.51 485.56,-189.51 725.56,-189.51 725.56,-120.51 485.56,-120.51"/>
<text text-anchor="middle" x="514.56" y="-174.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">link1</text>
<polyline fill="none" stroke="black" points="485.56,-166.51 543.56,-166.51 "/>
<text text-anchor="middle" x="514.56" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="485.56,-143.51 543.56,-143.51 "/>
<text text-anchor="middle" x="514.56" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">link2</text>
<polyline fill="none" stroke="black" points="543.56,-120.51 543.56,-189.51 "/>
<text text-anchor="middle" x="634.56" y="-173.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2 </text>
<text text-anchor="middle" x="634.56" y="-158.81" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 10.0.2.1/32 </text>
<text text-anchor="middle" x="634.56" y="-143.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo) </text>
<text text-anchor="middle" x="634.56" y="-128.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">(br0: 10.0.123.2/24)</text>
</g>
<!-- PC&#45;&#45;R2 -->
<g id="edge2" class="edge">
<title>PC:mgmt2&#45;&#45;R2:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-119.01C91.5,-119.01 485.56,-155.01 485.56,-155.01"/>
</g>
<!-- R3 -->
<g id="node4" class="node">
<title>R3</title>
<polygon fill="none" stroke="black" points="518.56,-0.5 518.56,-69.5 692.56,-69.5 692.56,-0.5 518.56,-0.5"/>
<text text-anchor="middle" x="543.56" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="518.56,-46.5 568.56,-46.5 "/>
<text text-anchor="middle" x="543.56" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="518.56,-23.5 568.56,-23.5 "/>
<text text-anchor="middle" x="543.56" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="568.56,-0.5 568.56,-69.5 "/>
<text text-anchor="middle" x="630.56" y="-46.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R3 </text>
<text text-anchor="middle" x="630.56" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 10.0.3.1/32 </text>
<text text-anchor="middle" x="630.56" y="-16.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
</g>
<!-- PC&#45;&#45;R3 -->
<g id="edge3" class="edge">
<title>PC:mgmt3&#45;&#45;R3:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-96.01C91.5,-96.01 518.56,-35 518.56,-35"/>
</g>
<!-- R1&#45;&#45;R2 -->
<g id="edge6" class="edge">
<title>R1:link&#45;&#45;R2:link1</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M543.56,-240.02C543.56,-240.02 514.56,-190.01 514.56,-190.01"/>
<text text-anchor="middle" x="493.56" y="-228.82" font-family="DejaVu Serif, Book" font-size="14.00">10.0.123.1/24</text>
</g>
<!-- R3&#45;&#45;PC -->
<g id="edge5" class="edge">
<title>R3:data&#45;&#45;PC:data2</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M518.56,-12C518.56,-12 91.5,-73.01 91.5,-73.01"/>
<text text-anchor="middle" x="137" y="-76.81" font-family="DejaVu Serif, Book" font-size="14.00">10.0.30.2/24</text>
<text text-anchor="middle" x="473.06" y="-15.8" font-family="DejaVu Serif, Book" font-size="14.00">10.0.30.1/24</text>
</g>
<!-- R3&#45;&#45;R2 -->
<g id="edge7" class="edge">
<title>R3:link&#45;&#45;R2:link2</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M543.56,-70C543.56,-70 514.56,-120.01 514.56,-120.01"/>
<text text-anchor="middle" x="493.56" y="-73.8" font-family="DejaVu Serif, Book" font-size="14.00">10.0.123.3/24</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB