diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index 6358c837..acbbdb84 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -12,6 +12,10 @@ All notable changes to the project are documented in this file.
- Upgrade netopeer2 (NETCONF) to 2.4.1
- Add documentation on Infix upgrading and downgrading, issue #1009
- Add HDMI and USB support for iMX8MP-evk
+- Enforced strict format for LLDP destination MAC address:
+ - Only accepts colon-separated format: `01:80:C2:00:00:0E`
+- Add `show lldp` command to show discovered neighbors per interface.
+- Add configuration support for per-interface LLDP administrative status
### Fixes
- Fix containers with multiple mounts
@@ -128,6 +132,7 @@ All notable changes to the project are documented in this file.
- Add support for GRE/GRETAP tunnels
- Add support for STP/RSTP on bridges
- Add support for VXLAN tunnels
+ - Add support for configuring global LLDP `message-tx-interval`
### Fixes
diff --git a/doc/discovery.md b/doc/discovery.md
index 4bfcbb28..83b16ab3 100644
--- a/doc/discovery.md
+++ b/doc/discovery.md
@@ -46,7 +46,7 @@ admin@infix-c0-ff-ee:~$
## LLDP
-Infix supports LLDP (IEEE 802.1AB). For a device with factory default
+Infix supports LLDP (IEEE 802.1AB). For a device with factory default
settings, the link-local IPv6 address can be read from the Management
Address TLV using *tcpdump* or other sniffing tools[^1]:
@@ -131,6 +131,10 @@ tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144
linux-pc:#
```
+The following capabilities are available via NETCONF/RESTCONF or the Infix CLI.
+
+### LLDP Enable/Disable
+
The LLDP service can be disabled using the following commands.
```
@@ -140,6 +144,59 @@ admin@infix-c0-ff-ee:/config/> leave
admin@infix-c0-ff-ee:/>
```
+To reenable it from the CLI config mode:
+
+```
+admin@test-00-01-00:/config/> set lldp enabled
+admin@test-00-01-00:/config/> leave
+```
+
+### LLDP Message Transmission Interval
+
+By default, LLDP uses a `message-tx-interval` of 30 seconds, as defined
+by the IEEE standard. Infix allows this value to be customized.
+To change it using the CLI:
+
+```
+admin@test-00-01-00:/config/> set lldp message-tx-interval 1
+admin@test-00-01-00:/config/> leave
+```
+
+### LLDP Administrative Status per Interface
+
+Infix supports configuring the LLDP administrative status on a per-port
+basis. The default mode is `tx-and-rx`, but the following options are
+also supported:
+
+- `rx-only` – Receive LLDP packets only
+- `tx-only` – Transmit LLDP packets only
+- `disabled` – Disable LLDP on the interface
+
+Example configuration:
+
+```
+admin@test-00-01-00:/config/> set lldp port e8 dest-mac-address 01:80:C2:00:00:0E admin-status disabled
+admin@test-00-01-00:/config/> set lldp port e5 dest-mac-address 01:80:C2:00:00:0E admin-status rx-only
+admin@test-00-01-00:/config/> set lldp port e6 dest-mac-address 01:80:C2:00:00:0E admin-status tx-only
+admin@test-00-01-00:/config/> leave
+```
+
+> [!NOTE]
+> The destination MAC address must be the standard LLDP multicast
+> address: `01:80:C2:00:00:0E`.
+
+### Displaying LLDP Neighbor Information
+
+In CLI mode, Infix also provides a convenient `show lldp` command to
+list LLDP neighbors detected on each interface:
+
+```
+admin@test-00-01-00:/> show lldp
+INTERFACE REM-IDX TIME CHASSIS-ID PORT-ID
+e5 1 902 00:a0:85:00:04:01 00:a0:85:00:04:07
+e6 3 897 00:a0:85:00:03:01 00:a0:85:00:03:07
+e8 2 901 00:a0:85:00:02:01 00:a0:85:00:02:05
+```
## mDNS-SD
diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc
index 9aab145f..f6074cfa 100644
--- a/src/confd/yang/confd.inc
+++ b/src/confd/yang/confd.inc
@@ -25,7 +25,7 @@ MODULES=(
"infix-if-type@2025-02-12.yang"
"infix-routing@2024-11-27.yang"
"ieee802-dot1ab-lldp@2022-03-15.yang"
- "infix-lldp@2025-01-08.yang"
+ "infix-lldp@2025-05-05.yang"
"infix-dhcp-common@2025-01-29.yang"
"infix-dhcp-client@2025-01-29.yang"
"infix-dhcp-server@2025-01-29.yang"
diff --git a/src/confd/yang/confd/infix-lldp.yang b/src/confd/yang/confd/infix-lldp.yang
index 375c3077..5a49f3d4 100644
--- a/src/confd/yang/confd/infix-lldp.yang
+++ b/src/confd/yang/confd/infix-lldp.yang
@@ -11,6 +11,13 @@ module infix-lldp {
contact "kernelkit@googlegroups.com";
description "Infix augments and deviations to ieee-dot1ab-lldp.";
+ revision 2025-05-05 {
+ description
+ "Restrict 'dest-mac-address' to only allow the LLDP multicast MAC
+ address (01:80:C2:00:00:0E).";
+ reference "internal";
+ }
+
revision 2025-01-08 {
description "Enable std. /lldp:lldp/message-tx-interval";
}
@@ -32,6 +39,22 @@ module infix-lldp {
}
}
+ deviation "/lldp:lldp/lldp:port/lldp:dest-mac-address" {
+ deviate replace {
+ type string {
+ pattern "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}";
+ }
+ }
+ }
+
+ deviation "/lldp:lldp/lldp:port/lldp:dest-mac-address" {
+ deviate add {
+ must "translate(., 'abcdef', 'ABCDEF') = '01:80:C2:00:00:0E'" {
+ error-message "Only the LLDP Multicast MAC (01:80:C2:00:00:0E) is allowed.";
+ }
+ }
+ }
+
deviation "/lldp:lldp/lldp:message-fast-tx" {
deviate not-supported;
}
diff --git a/src/confd/yang/confd/infix-lldp@2025-01-08.yang b/src/confd/yang/confd/infix-lldp@2025-05-05.yang
similarity index 100%
rename from src/confd/yang/confd/infix-lldp@2025-01-08.yang
rename to src/confd/yang/confd/infix-lldp@2025-05-05.yang
diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml
index f80f41ec..c10e1b27 100644
--- a/src/klish-plugin-infix/xml/infix.xml
+++ b/src/klish-plugin-infix/xml/infix.xml
@@ -373,6 +373,12 @@
+
+
+ show lldp
+
+
+
show hardware |pager
@@ -471,22 +477,22 @@
sysinfo
-->
-
- cat /etc/version
-
-
-
- jq -C . /etc/factory-config.cfg |pager
-
-
-
- sysrepocfg -X -f json | jq -C . |pager
-
-
-
- jq -C . /cfg/startup-config.cfg |pager
-
+
+ cat /etc/version
+
+
+ jq -C . /etc/factory-config.cfg |pager
+
+
+
+ sysrepocfg -X -f json | jq -C . |pager
+
+
+
+ jq -C . /cfg/startup-config.cfg |pager
+
+
/bin/yorn "This will restore the device to factory defaults"
@@ -581,6 +587,7 @@
+
diff --git a/src/show/show.py b/src/show/show.py
index 6416a7ef..49d2a3e3 100755
--- a/src/show/show.py
+++ b/src/show/show.py
@@ -154,6 +154,15 @@ def routes(args: List[str]):
return
cli_pretty(data, "show-routing-table", "-i", ip_version)
+def lldp(args: List[str]):
+ data = run_sysrepocfg("/ieee802-dot1ab-lldp:lldp")
+ if not data:
+ print("No lldp data retrieved.")
+ return
+ if RAW_OUTPUT:
+ print(json.dumps(data, indent=2))
+ return
+ cli_pretty(data, "show-lldp")
def execute_command(command: str, args: List[str]):
command_mapping = {
@@ -162,6 +171,7 @@ def execute_command(command: str, args: List[str]):
'interface': interface,
'ntp': ntp,
'routes': routes,
+ 'lldp': lldp,
'software' : software,
'stp': stp,
}
diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py
index 85f04961..19e64c54 100755
--- a/src/statd/python/cli_pretty/cli_pretty.py
+++ b/src/statd/python/cli_pretty/cli_pretty.py
@@ -20,6 +20,7 @@ class PadMdb:
group = 20
ports = 45
+
class PadStpPort:
port = 12
id = 7
@@ -30,6 +31,7 @@ class PadStpPort:
total = 12 + 7 + 12 + 12 + 6 + 31
+
class PadRoute:
dest = 30
pref = 8
@@ -57,6 +59,7 @@ class PadSoftware:
state = 10
version = 23
+
class PadDhcpServer:
ip = 17
mac = 19
@@ -64,11 +67,13 @@ class PadDhcpServer:
cid = 19
exp = 10
+
class PadUsbPort:
title = 30
name = 20
state = 10
+
class PadNtpSource:
address = 16
mode = 13
@@ -76,6 +81,7 @@ class PadNtpSource:
stratum = 11
poll = 14
+
class Decore():
@staticmethod
def decorate(sgr, txt, restore="0"):
@@ -109,11 +115,21 @@ class Decore():
def gray_bg(txt):
return Decore.decorate("100", txt)
+
+class PadLldp:
+ interface = 16
+ rem_idx = 10
+ time = 12
+ chassis_id = 20
+ port_id = 20
+
+
def datetime_now():
if UNIT_TEST:
return datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
return datetime.now(timezone.utc)
+
def get_json_data(default, indata, *args):
data = indata
for arg in args:
@@ -124,12 +140,14 @@ def get_json_data(default, indata, *args):
return data
+
def remove_yang_prefix(key):
parts = key.split(":", 1)
if len(parts) > 1:
return parts[1]
return key
+
class Date(datetime):
def _pretty_delta(delta):
assert(delta.total_seconds() > 0)
@@ -179,7 +197,6 @@ class Date(datetime):
return cls.strptime(f"{date}+{tz}", "%Y-%m-%dT%H:%M:%S%z")
-
class Route:
def __init__(self, data, ip):
self.data = data
@@ -321,6 +338,7 @@ class Software:
print(f"SHA-256 : {self.hash}")
print(f"Installed : {self.date}")
+
class USBport:
def __init__(self, data):
self.data = data
@@ -333,6 +351,7 @@ class USBport:
row += f"{self.state:<{PadUsbPort.state}}"
print(row)
+
class STPBridgeID:
def __init__(self, id):
self.id = id
@@ -345,6 +364,7 @@ class STPBridgeID:
)
return f"{prio:1x}.{sysid:03x}.{addr}"
+
class STPPortID:
def __init__(self, id):
self.id = id
@@ -356,6 +376,7 @@ class STPPortID:
)
return f"{prio:1x}.{pid:03x}"
+
class DhcpServer:
def __init__(self, data):
self.data = data
@@ -533,6 +554,7 @@ class Iface:
def is_gretap(self):
return self.data['type'] == "infix-if-type:gretap"
+
def oper(self, detail=False):
"""Remap in brief overview to fit column widths."""
if not detail and self.oper_status == "lower-layer-down":
@@ -719,7 +741,7 @@ class Iface:
lower.pr_name(pipe)
lower.pr_proto_lag()
- def pr_veth(self, _ifaces):
+ def pr_veth(self):
self.pr_name(pipe="")
self.pr_proto_veth()
self.pr_proto_ipv4()
@@ -930,6 +952,25 @@ class Iface:
print(f"{' port':<{20}}: {tc.get('port', 'UNKNOWN')}")
+class LldpNeighbor:
+ def __init__(self, iface, data):
+ self.interface = iface
+ self.remote_index = data.get("remote-index", 0)
+ self.time_mark = data.get("time-mark", 0)
+ self.chassis_id = data.get("chassis-id", "unknown")
+ self.port_id = data.get("port-id", "unknown")
+
+ def print(self):
+ row = (
+ f"{self.interface:<{PadLldp.interface}}"
+ f"{self.remote_index:<{PadLldp.rem_idx}}"
+ f"{self.time_mark:<{PadLldp.time}}"
+ f"{self.chassis_id:<{PadLldp.chassis_id}}"
+ f"{self.port_id:<{PadLldp.port_id}}"
+ )
+ print(row)
+
+
def find_iface(_ifaces, name):
for _iface in [Iface(data) for data in _ifaces]:
if _iface.name == name:
@@ -941,9 +982,11 @@ def find_iface(_ifaces, name):
def version_sort(s):
return [int(x) if x.isdigit() else x for x in re.split(r'(\d+)', s)]
+
def ifname_sort(iface):
return version_sort(iface["name"])
+
def brport_sort(iface):
brname = iface.get("infix-interfaces:bridge-port", {}).get("bridge", "")
return version_sort(brname) + version_sort(iface["name"])
@@ -987,7 +1030,7 @@ def pr_interface_list(json):
continue
if iface.is_veth():
- iface.pr_veth(ifaces)
+ iface.pr_veth()
continue
if iface.is_gre():
@@ -1057,7 +1100,6 @@ def show_bridge_mdb(json):
iface.pr_vlans_mdb(iface.name)
-
def show_bridge_stp_port(ifname, brport):
stp = brport["stp"]
@@ -1091,6 +1133,7 @@ def show_bridge_stp_port(ifname, brport):
)
print(row)
+
def show_bridge_stp(json):
if not json.get("ietf-interfaces:interfaces"):
print("Error, top level \"ietf-interfaces:interface\" missing")
@@ -1198,6 +1241,7 @@ def show_software(json, name):
if slot.is_rootfs():
slot.print()
+
def show_hardware(json):
if not json.get("ietf-hardware:hardware"):
print(f"Error, top level \"ietf-hardware:component\" missing")
@@ -1216,6 +1260,7 @@ def show_hardware(json):
port = USBport(component)
port.print()
+
def show_ntp(json):
if not json.get("ietf-system:system-state"):
print("NTP client not enabled.")
@@ -1236,6 +1281,7 @@ def show_ntp(json):
row += f"{source['poll']:>{PadNtpSource.poll}}"
print(row)
+
def show_dhcp_server(json, stats):
data = json.get("infix-dhcp-server:dhcp-server")
if not data:
@@ -1255,6 +1301,37 @@ def show_dhcp_server(json, stats):
print(Decore.invert(hdr))
server.print()
+
+def show_lldp(json):
+ if not json.get("ieee802-dot1ab-lldp:lldp"):
+ print("Error: No LLDP data available.")
+ sys.exit(1)
+
+ lldp_ports = json["ieee802-dot1ab-lldp:lldp"].get("port", [])
+
+ if not lldp_ports:
+ print("No LLDP neighbors found.")
+ return
+
+ header = (
+ f"{'INTERFACE':<{PadLldp.interface}}"
+ f"{'REM-IDX':<{PadLldp.rem_idx}}"
+ f"{'TIME':<{PadLldp.time}}"
+ f"{'CHASSIS-ID':<{PadLldp.chassis_id}}"
+ f"{'PORT-ID':<{PadLldp.port_id}}"
+ )
+
+ print(Decore.invert(header))
+
+ for port_data in lldp_ports:
+ port_name = port_data["name"]
+ neighbors = port_data.get("remote-systems-data", [])
+
+ for neighbor in neighbors:
+ entry = LldpNeighbor(port_name, neighbor)
+ entry.print()
+
+
def main():
global UNIT_TEST
@@ -1272,47 +1349,51 @@ def main():
parser.add_argument('-t', '--test', action='store_true', help='Enable unit test mode')
- parser_show_routing_table = subparsers.add_parser('show-routing-table', help='Show the routing table')
- parser_show_routing_table.add_argument('-i', '--ip', required=True, help='IPv4 or IPv6 address')
+ subparsers.add_parser('show-boot-order', help='Show NTP sources')
- parser_show_interfaces = subparsers.add_parser('show-interfaces', help='Show interfaces')
- parser_show_interfaces.add_argument('-n', '--name', help='Interface name')
+ subparsers.add_parser('show-bridge-mdb', help='Show bridge MDB')
- parser_show_bridge_mdb = subparsers.add_parser('show-bridge-mdb', help='Show bridge MDB')
- parser_show_bridge_stp = subparsers.add_parser('show-bridge-stp',
- help='Show spanning tree state')
+ subparsers.add_parser('show-bridge-stp', help='Show spanning tree state')
- parser_show_software = subparsers.add_parser('show-software', help='Show software versions')
- parser_show_software.add_argument('-n', '--name', help='Slotname')
+ subparsers.add_parser('show-dhcp-server', help='Show DHCP server') \
+ .add_argument("-s", "--stats", action="store_true", help="Show server statistics")
- parser_show_hardware = subparsers.add_parser('show-hardware', help='Show USB ports')
+ subparsers.add_parser('show-hardware', help='Show USB ports')
- parser_show_ntp_sources = subparsers.add_parser('show-ntp', help='Show NTP sources')
+ subparsers.add_parser('show-interfaces', help='Show interfaces') \
+ .add_argument('-n', '--name', help='Interface name')
- parser_show_boot_order = subparsers.add_parser('show-boot-order', help='Show NTP sources')
+ subparsers.add_parser('show-lldp', help='Show LLDP neighbors')
- parser_dhcp_srv = subparsers.add_parser('show-dhcp-server', help='Show DHCP server')
- parser_dhcp_srv.add_argument("-s", "--stats", action="store_true", help="Show server statistics")
+ subparsers.add_parser('show-ntp', help='Show NTP sources')
+
+ subparsers.add_parser('show-routing-table', help='Show the routing table') \
+ .add_argument('-i', '--ip', required=True, help='IPv4 or IPv6 address')
+
+ subparsers.add_parser('show-software', help='Show software versions') \
+ .add_argument('-n', '--name', help='Slotname')
args = parser.parse_args()
UNIT_TEST = args.test
- if args.command == "show-interfaces":
+ if args.command == "show-bridge-mdb":
+ show_bridge_mdb(json_data)
+ elif args.command == "show-bridge-stp":
+ show_bridge_stp(json_data)
+ elif args.command == "show-dhcp-server":
+ show_dhcp_server(json_data, args.stats)
+ elif args.command == "show-hardware":
+ show_hardware(json_data)
+ elif args.command == "show-interfaces":
show_interfaces(json_data, args.name)
+ elif args.command == "show-lldp":
+ show_lldp(json_data)
+ elif args.command == "show-ntp":
+ show_ntp(json_data)
elif args.command == "show-routing-table":
show_routing_table(json_data, args.ip)
elif args.command == "show-software":
show_software(json_data, args.name)
- elif args.command == "show-bridge-mdb":
- show_bridge_mdb(json_data)
- elif args.command == "show-bridge-stp":
- show_bridge_stp(json_data)
- elif args.command == "show-hardware":
- show_hardware(json_data)
- elif args.command == "show-ntp":
- show_ntp(json_data)
- elif args.command == "show-dhcp-server":
- show_dhcp_server(json_data, args.stats)
else:
print(f"Error, unknown command '{args.command}'")
sys.exit(1)
diff --git a/src/statd/python/yanger/infix_lldp.py b/src/statd/python/yanger/infix_lldp.py
index fcd5f5f5..2312c737 100644
--- a/src/statd/python/yanger/infix_lldp.py
+++ b/src/statd/python/yanger/infix_lldp.py
@@ -4,20 +4,35 @@ from collections import defaultdict
def operational():
"""Retrieve LLDP neighbor information and store in remote-systems-data under the correct port."""
- # Reference: https://www.ieee802.org/1/files/public/YANGs/ieee802-types.yang
- subtype_mapping = {
- "component": "chassis-component",
+ # https://www.ieee802.org/1/files/public/YANGs/ieee802-types.yang
+ # and subtypes are not supported in
+ # https://github.com/lldpd/lldpd until v1.0.19
+ chassis_id_subtype_mapping = {
+ #"unhandled": "chassis-component",
"ifalias": "interface-alias",
- "port": "port-component",
+ #"unhandled": "port-component",
"mac": "mac-address",
"ip": "network-address",
"ifname": "interface-name",
"local": "local"
}
- DEFAULT_MAC = "00-00-00-00-00-00"
+ # https://www.ieee802.org/1/files/public/YANGs/ieee802-types.yang
+ # and subtypes are not supported in
+ # https://github.com/lldpd/lldpd until v1.0.19
+ port_id_subtype_mapping = {
+ "ifalias": "interface-alias",
+ #"unhandled": "port-component",
+ "mac": "mac-address",
+ "ip": "network-address",
+ "ifname": "interface-name",
+ #"unhandled": "agent-circuit-id"
+ "local": "local"
+ }
- port_data = defaultdict(lambda: {"remote-systems-data": [], "dest-mac-address": None})
+ LLDP_MULTICAST_MAC = "01:80:C2:00:00:0E"
+
+ port_data = defaultdict(lambda: {"remote-systems-data": [], "dest-mac-address": LLDP_MULTICAST_MAC})
data = HOST.run_json(["lldpcli", "show", "neighbors", "-f", "json"])
@@ -32,18 +47,12 @@ def operational():
time_mark = parse_time(iface_data.get("age"))
chassis = iface_data.get("chassis", {})
- chassis_id_type, chassis_id_value = extract_chassis_id(chassis, subtype_mapping)
+ chassis_id_type, chassis_id_value = extract_chassis_id(chassis, chassis_id_subtype_mapping)
port_info = iface_data.get("port", {})
- port_id_type = subtype_mapping.get(port_info.get("id", {}).get("type"), "unknown")
+ port_id_type = port_id_subtype_mapping.get(port_info.get("id", {}).get("type"), "unknown")
port_id_value = port_info.get("id", {}).get("value", "")
- dest_mac_address = (
- chassis_id_value.replace(":", "-") if chassis_id_type == "mac-address" else
- port_id_value.replace(":", "-") if port_id_type == "mac-address" else
- DEFAULT_MAC
- )
-
remote_entry = {
"time-mark": time_mark,
"remote-index": remote_index,
@@ -55,9 +64,6 @@ def operational():
port_data[iface_name]["remote-systems-data"].append(remote_entry)
- if port_data[iface_name]["dest-mac-address"] is None:
- port_data[iface_name]["dest-mac-address"] = dest_mac_address
-
formatted_output = {
"ieee802-dot1ab-lldp:lldp": {
"port": [
diff --git a/test/case/ietf_interfaces/veth_delete/topology.dot b/test/case/ietf_interfaces/veth_delete/topology.dot
deleted file mode 100644
index 10fb0dee..00000000
--- a/test/case/ietf_interfaces/veth_delete/topology.dot
+++ /dev/null
@@ -1,25 +0,0 @@
-graph "1x3" {
- layout="neato";
- overlap="false";
- esep="+80";
-
- node [shape=record, fontname="DejaVu Sans Mono, Book"];
- edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
-
- host [
- label="host | { mgmt | data1 | data2 }",
- pos="0,12!",
- requires="controller",
- ];
-
- target [
- label="{ mgmt | data1 | data2 } | target",
- pos="10,12!",
-
- requires="infix",
- ];
-
- host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey]
- host:data1 -- target:data1 [color=black]
- host:data2 -- target:data2 [color=black]
-}
diff --git a/test/case/ietf_interfaces/veth_delete/topology.dot b/test/case/ietf_interfaces/veth_delete/topology.dot
new file mode 120000
index 00000000..7689a677
--- /dev/null
+++ b/test/case/ietf_interfaces/veth_delete/topology.dot
@@ -0,0 +1 @@
+../../../infamy/topologies/1x3.dot
\ No newline at end of file
diff --git a/test/case/ietf_interfaces/veth_delete/topology.svg b/test/case/ietf_interfaces/veth_delete/topology.svg
index e7d561fa..9517e639 100644
--- a/test/case/ietf_interfaces/veth_delete/topology.svg
+++ b/test/case/ietf_interfaces/veth_delete/topology.svg
@@ -35,7 +35,7 @@
host:mgmt--target:mgmt
-
+
diff --git a/test/case/ietf_interfaces/vlan_iface_termination/test.py b/test/case/ietf_interfaces/vlan_iface_termination/test.py
index e2c32c45..c26ae1ec 100755
--- a/test/case/ietf_interfaces/vlan_iface_termination/test.py
+++ b/test/case/ietf_interfaces/vlan_iface_termination/test.py
@@ -8,14 +8,14 @@ attached to a VLAN filtering bridge are always locally terminated.
.---------------------------.
| target |
| |
-| data0.10 br0 data1.10 |
+| data1.10 br0 data2.10 |
| \ / \ / |
-'------data0-----data1------'
+'------data1-----data2------'
| |
| |
-.------data0-----data1------.
+.------data1-----data2------.
| / : \ |
-| data0.10 : data1.10 |
+| data1.10 : data2.10 |
| : |
| host |
| : |
@@ -23,7 +23,7 @@ attached to a VLAN filtering bridge are always locally terminated.
....
In this setup, even though VLAN 10 is allowed to ingress and egress on
-both `data0` and `data1`, _bridging_ of packets from one to the other
+both `data1` and `data2`, _bridging_ of packets from one to the other
must _not_ be allowed.
"""
@@ -35,10 +35,10 @@ with infamy.Test() as test:
tgt = env.attach("target", "mgmt")
with test.step("Configure bridge and VLAN interfaces on target"):
- _, hdata0 = env.ltop.xlate( "host", "data0")
_, hdata1 = env.ltop.xlate( "host", "data1")
- _, ddata0 = env.ltop.xlate("target", "data0")
+ _, hdata2 = env.ltop.xlate( "host", "data2")
_, ddata1 = env.ltop.xlate("target", "data1")
+ _, ddata2 = env.ltop.xlate("target", "data2")
tgt.put_config_dicts({
"ietf-interfaces": {
@@ -53,35 +53,12 @@ with infamy.Test() as test:
"vlan": [
{
"vid": 1,
- "untagged": [ddata0, ddata1]
+ "untagged": [ddata1, ddata2]
},
]
}
}
},
- {
- "name": ddata0,
- "infix-interfaces:bridge-port": {
- "pvid": 1,
- "bridge": "br0"
- }
- },
- {
- "name": f"{ddata0}.10",
- "type": "infix-if-type:vlan",
- "vlan": {
- "id": 10,
- "lower-layer-if": ddata0,
- },
- "ipv4": {
- "address": [
- {
- "ip": "10.10.1.2",
- "prefix-length": 24,
- }
- ]
- }
- },
{
"name": ddata1,
"infix-interfaces:bridge-port": {
@@ -96,6 +73,29 @@ with infamy.Test() as test:
"id": 10,
"lower-layer-if": ddata1,
},
+ "ipv4": {
+ "address": [
+ {
+ "ip": "10.10.1.2",
+ "prefix-length": 24,
+ }
+ ]
+ }
+ },
+ {
+ "name": ddata2,
+ "infix-interfaces:bridge-port": {
+ "pvid": 1,
+ "bridge": "br0"
+ }
+ },
+ {
+ "name": f"{ddata2}.10",
+ "type": "infix-if-type:vlan",
+ "vlan": {
+ "id": 10,
+ "lower-layer-if": ddata2,
+ },
"ipv4": {
"address": [
{
@@ -110,8 +110,8 @@ with infamy.Test() as test:
}
})
- with infamy.IsolatedMacVlan(hdata0) as ns0, \
- infamy.IsolatedMacVlan(hdata1) as ns1:
+ with infamy.IsolatedMacVlan(hdata1) as ns0, \
+ infamy.IsolatedMacVlan(hdata2) as ns1:
with test.step("Configure IP addresses and VLAN interfaces on host"):
ns0.addip("10.0.1.1")
@@ -128,17 +128,17 @@ with infamy.Test() as test:
ip addr add 10.10.2.1/24 dev vlan10
""")
- with test.step("Verify that host:data0 reaches host:data1 with untagged packets"):
+ with test.step("Verify that host:data1 reaches host:data2 with untagged packets"):
ns0.must_reach("10.0.1.2")
- with test.step("Verify that traffic on VLAN 10 from host:data0 does not reach host:data1"):
+ with test.step("Verify that traffic on VLAN 10 from host:data1 does not reach host:data2"):
infamy.parallel(lambda: ns0.runsh("timeout -s INT 5 ping -i 0.2 -b 10.10.1.255 || true"),
lambda: ns1.must_not_receive("ip src 10.10.1.1"))
- with test.step("Verify that host:data0 can reach target on VLAN 10"):
+ with test.step("Verify that host:data1 can reach target on VLAN 10"):
ns0.must_reach("10.10.1.2")
- with test.step("Verify that host:data1 can reach target on VLAN 10"):
+ with test.step("Verify that host:data2 can reach target on VLAN 10"):
ns1.must_reach("10.10.2.2")
test.succeed()
diff --git a/test/case/ietf_interfaces/vlan_iface_termination/topology.svg b/test/case/ietf_interfaces/vlan_iface_termination/topology.svg
index d2fdf4fa..9517e639 100644
--- a/test/case/ietf_interfaces/vlan_iface_termination/topology.svg
+++ b/test/case/ietf_interfaces/vlan_iface_termination/topology.svg
@@ -14,11 +14,11 @@
host
-tgt
+mgmt
-data0
+data1
-data1
+data2
@@ -26,25 +26,25 @@
mgmt
-data0
+data1
-data1
+data2
target
-host:tgt--target:mgmt
-
+host:mgmt--target:mgmt
+
-host:data0--target:data0
+host:data1--target:data1
-host:data1--target:data1
+host:data2--target:data2
diff --git a/test/case/ietf_interfaces/vlan_iface_termination/vlan_iface_termination.adoc b/test/case/ietf_interfaces/vlan_iface_termination/vlan_iface_termination.adoc
index 8fd73631..cc955c6b 100644
--- a/test/case/ietf_interfaces/vlan_iface_termination/vlan_iface_termination.adoc
+++ b/test/case/ietf_interfaces/vlan_iface_termination/vlan_iface_termination.adoc
@@ -7,14 +7,14 @@ attached to a VLAN filtering bridge are always locally terminated.
.---------------------------.
| target |
| |
-| data0.10 br0 data1.10 |
+| data1.10 br0 data2.10 |
| \ / \ / |
-'------data0-----data1------'
+'------data1-----data2------'
| |
| |
-.------data0-----data1------.
+.------data1-----data2------.
| / : \ |
-| data0.10 : data1.10 |
+| data1.10 : data2.10 |
| : |
| host |
| : |
@@ -22,7 +22,7 @@ attached to a VLAN filtering bridge are always locally terminated.
....
In this setup, even though VLAN 10 is allowed to ingress and egress on
-both `data0` and `data1`, _bridging_ of packets from one to the other
+both `data1` and `data2`, _bridging_ of packets from one to the other
must _not_ be allowed.
==== Topology
@@ -41,10 +41,10 @@ endif::topdoc[]
. Set up topology and attach to target
. Configure bridge and VLAN interfaces on target
. Configure IP addresses and VLAN interfaces on host
-. Verify that host:data0 reaches host:data1 with untagged packets
-. Verify that traffic on VLAN 10 from host:data0 does not reach host:data1
-. Verify that host:data0 can reach target on VLAN 10
+. Verify that host:data1 reaches host:data2 with untagged packets
+. Verify that traffic on VLAN 10 from host:data1 does not reach host:data2
. Verify that host:data1 can reach target on VLAN 10
+. Verify that host:data2 can reach target on VLAN 10
<<<
diff --git a/test/case/infix_services/Readme.adoc b/test/case/infix_services/Readme.adoc
index a0bdab82..67c61268 100644
--- a/test/case/infix_services/Readme.adoc
+++ b/test/case/infix_services/Readme.adoc
@@ -3,14 +3,16 @@
<<<
-include::mdns_enable_disable/Readme.adoc[]
+include::mdns/mdns_enable_disable/Readme.adoc[]
-include::mdns_allow_deny/Readme.adoc[]
+include::mdns/mdns_allow_deny/Readme.adoc[]
-include::lldp_enable_disable/Readme.adoc[]
+include::lldp/lldp_enable_disable/Readme.adoc[]
-include::lldp_admin_status/Readme.adoc[]
+include::lldp/lldp_admin_status/Readme.adoc[]
-include::ssh_server_config/Readme.adoc[]
+include::lldp/lldp_ieee_group_forward/Readme.adoc[]
-include::ssh_key_authentication/Readme.adoc[]
+include::ssh/ssh_server_config/Readme.adoc[]
+
+include::ssh/ssh_key_authentication/Readme.adoc[]
diff --git a/test/case/infix_services/infix_services.yaml b/test/case/infix_services/infix_services.yaml
index eb5568f6..43ab99f5 100644
--- a/test/case/infix_services/infix_services.yaml
+++ b/test/case/infix_services/infix_services.yaml
@@ -1,9 +1,9 @@
---
- name: lldp
- suite: lldp.yaml
+ suite: lldp/lldp.yaml
- name: mdns
- suite: mdns.yaml
+ suite: mdns/mdns.yaml
- name: ssh
- suite: ssh.yaml
+ suite: ssh/ssh.yaml
diff --git a/test/case/infix_services/lldp.yaml b/test/case/infix_services/lldp/lldp.yaml
similarity index 63%
rename from test/case/infix_services/lldp.yaml
rename to test/case/infix_services/lldp/lldp.yaml
index 4f0c30e6..af3ce73b 100644
--- a/test/case/infix_services/lldp.yaml
+++ b/test/case/infix_services/lldp/lldp.yaml
@@ -4,3 +4,6 @@
- name: lldp_admin_status
case: lldp_admin_status/test.py
+
+- name: lldp_ieee_group_forward
+ case: lldp_ieee_group_forward/test.py
diff --git a/test/case/infix_services/lldp_admin_status/Readme.adoc b/test/case/infix_services/lldp/lldp_admin_status/Readme.adoc
similarity index 100%
rename from test/case/infix_services/lldp_admin_status/Readme.adoc
rename to test/case/infix_services/lldp/lldp_admin_status/Readme.adoc
diff --git a/test/case/infix_services/lldp_admin_status/lldp_admin_status.adoc b/test/case/infix_services/lldp/lldp_admin_status/lldp_admin_status.adoc
similarity index 75%
rename from test/case/infix_services/lldp_admin_status/lldp_admin_status.adoc
rename to test/case/infix_services/lldp/lldp_admin_status/lldp_admin_status.adoc
index 9a865876..24d351fe 100644
--- a/test/case/infix_services/lldp_admin_status/lldp_admin_status.adoc
+++ b/test/case/infix_services/lldp/lldp_admin_status/lldp_admin_status.adoc
@@ -4,11 +4,11 @@ Verify that LLDP admin status is set properly by lldpd
==== Topology
ifdef::topdoc[]
-image::{topdoc}../../test/case/infix_services/lldp_admin_status/topology.svg[LLDP admin status topology]
+image::{topdoc}../../test/case/infix_services/lldp/lldp_admin_status/topology.svg[LLDP admin status topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
-image::lldp_admin_status/topology.svg[LLDP admin status topology]
+image::lldp/lldp_admin_status/topology.svg[LLDP admin status topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[LLDP admin status topology]
diff --git a/test/case/infix_services/lldp_admin_status/test.py b/test/case/infix_services/lldp/lldp_admin_status/test.py
similarity index 95%
rename from test/case/infix_services/lldp_admin_status/test.py
rename to test/case/infix_services/lldp/lldp_admin_status/test.py
index 2bb7e275..5f9befbb 100755
--- a/test/case/infix_services/lldp_admin_status/test.py
+++ b/test/case/infix_services/lldp/lldp_admin_status/test.py
@@ -22,7 +22,7 @@ def capture_traffic(iface, sec):
return sniffer.output()
def send_lldp_packet(iface, chassis_id, chassis_id_subtype, ttl=3):
- eth = Ether(dst="01:80:c2:00:00:0e", type=0x88cc)
+ eth = Ether(src="02:01:02:03:04:05", dst="01:80:C2:00:00:0E", type=0x88cc)
lldpdu = eth / LLDPDU()
lldpdu /= LLDPDUChassisID(subtype=chassis_id_subtype, id=chassis_id)
lldpdu /= LLDPDUPortID(subtype=5, id=iface)
@@ -45,7 +45,7 @@ def verify_admin_status(test, target, port, admin_status, local_capture, remote_
"lldp": {
"port": [{
"name": target["data"],
- "dest-mac-address": "00-00-00-00-00-00",
+ "dest-mac-address": "01:80:C2:00:00:0E",
"admin-status": admin_status
}]
}
diff --git a/test/case/infix_services/lldp_admin_status/topology.dot b/test/case/infix_services/lldp/lldp_admin_status/topology.dot
similarity index 100%
rename from test/case/infix_services/lldp_admin_status/topology.dot
rename to test/case/infix_services/lldp/lldp_admin_status/topology.dot
diff --git a/test/case/infix_services/lldp_admin_status/topology.svg b/test/case/infix_services/lldp/lldp_admin_status/topology.svg
similarity index 100%
rename from test/case/infix_services/lldp_admin_status/topology.svg
rename to test/case/infix_services/lldp/lldp_admin_status/topology.svg
diff --git a/test/case/infix_services/lldp_enable_disable/Readme.adoc b/test/case/infix_services/lldp/lldp_enable_disable/Readme.adoc
similarity index 100%
rename from test/case/infix_services/lldp_enable_disable/Readme.adoc
rename to test/case/infix_services/lldp/lldp_enable_disable/Readme.adoc
diff --git a/test/case/infix_services/lldp_enable_disable/lldp_enable_disable.adoc b/test/case/infix_services/lldp/lldp_enable_disable/lldp_enable_disable.adoc
similarity index 75%
rename from test/case/infix_services/lldp_enable_disable/lldp_enable_disable.adoc
rename to test/case/infix_services/lldp/lldp_enable_disable/lldp_enable_disable.adoc
index 7d333f04..1884dedc 100644
--- a/test/case/infix_services/lldp_enable_disable/lldp_enable_disable.adoc
+++ b/test/case/infix_services/lldp/lldp_enable_disable/lldp_enable_disable.adoc
@@ -5,11 +5,11 @@ Operation and non-operation are confirmed using tcpdump.
==== Topology
ifdef::topdoc[]
-image::{topdoc}../../test/case/infix_services/lldp_enable_disable/topology.svg[LLDP enable disable topology]
+image::{topdoc}../../test/case/infix_services/lldp/lldp_enable_disable/topology.svg[LLDP enable disable topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
-image::lldp_enable_disable/topology.svg[LLDP enable disable topology]
+image::lldp/lldp_enable_disable/topology.svg[LLDP enable disable topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[LLDP enable disable topology]
diff --git a/test/case/infix_services/lldp_enable_disable/test.py b/test/case/infix_services/lldp/lldp_enable_disable/test.py
similarity index 100%
rename from test/case/infix_services/lldp_enable_disable/test.py
rename to test/case/infix_services/lldp/lldp_enable_disable/test.py
diff --git a/test/case/infix_services/lldp_enable_disable/topology.dot b/test/case/infix_services/lldp/lldp_enable_disable/topology.dot
similarity index 100%
rename from test/case/infix_services/lldp_enable_disable/topology.dot
rename to test/case/infix_services/lldp/lldp_enable_disable/topology.dot
diff --git a/test/case/infix_services/lldp_enable_disable/topology.svg b/test/case/infix_services/lldp/lldp_enable_disable/topology.svg
similarity index 100%
rename from test/case/infix_services/lldp_enable_disable/topology.svg
rename to test/case/infix_services/lldp/lldp_enable_disable/topology.svg
diff --git a/test/case/infix_services/lldp/lldp_ieee_group_forward/Readme.adoc b/test/case/infix_services/lldp/lldp_ieee_group_forward/Readme.adoc
new file mode 120000
index 00000000..f718858e
--- /dev/null
+++ b/test/case/infix_services/lldp/lldp_ieee_group_forward/Readme.adoc
@@ -0,0 +1 @@
+lldp_ieee_group_forward.adoc
\ No newline at end of file
diff --git a/test/case/infix_services/lldp/lldp_ieee_group_forward/lldp_ieee_group_forward.adoc b/test/case/infix_services/lldp/lldp_ieee_group_forward/lldp_ieee_group_forward.adoc
new file mode 100644
index 00000000..7d9df613
--- /dev/null
+++ b/test/case/infix_services/lldp/lldp_ieee_group_forward/lldp_ieee_group_forward.adoc
@@ -0,0 +1,27 @@
+=== LLDP IEEE Group Forward
+==== Description
+Verify that LLDP packets can be flooded to all ports on a bridge.
+Operation and non-operation are confirmed using tcpdump.
+
+==== Topology
+ifdef::topdoc[]
+image::{topdoc}../../test/case/infix_services/lldp/lldp_ieee_group_forward/topology.svg[LLDP IEEE Group Forward topology]
+endif::topdoc[]
+ifndef::topdoc[]
+ifdef::testgroup[]
+image::lldp/lldp_ieee_group_forward/topology.svg[LLDP IEEE Group Forward topology]
+endif::testgroup[]
+ifndef::testgroup[]
+image::topology.svg[LLDP IEEE Group Forward topology]
+endif::testgroup[]
+endif::topdoc[]
+==== Test sequence
+. Set up topology and attach to target DUT
+. Configure interfaces and disable LLDP daemon
+. Verify LLDP absence on host:data2
+. Enable LLDP flooding by setting group forward
+. Verify LLDP arrival on host:data2
+
+
+<<<
+
diff --git a/test/case/infix_services/lldp/lldp_ieee_group_forward/test.py b/test/case/infix_services/lldp/lldp_ieee_group_forward/test.py
new file mode 100755
index 00000000..3e20017b
--- /dev/null
+++ b/test/case/infix_services/lldp/lldp_ieee_group_forward/test.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+
+"""LLDP IEEE Group Forward
+
+Verify that LLDP packets can be flooded to all ports on a bridge.
+Operation and non-operation are confirmed using tcpdump.
+"""
+
+import time
+import threading
+import infamy
+
+from scapy.all import Ether, sendp
+from scapy.contrib.lldp import (
+ LLDPDU, LLDPDUChassisID, LLDPDUPortID,
+ LLDPDUTimeToLive, LLDPDUEndOfLLDPDU
+)
+
+SNIFFING_DURATION = 3
+
+LLDP_INTERVAL = 1
+LLDP_TTL = 3
+LLDP_MULTICAST_MAC = "01:80:C2:00:00:0E"
+LLDP_ETHERTYPE = 0x88cc
+
+CHASSIS_ID = "Chassis-Test"
+SRC_MAC = "02:01:02:03:04:05"
+
+BRIDGE = "br0"
+
+def send_lldp_packet(iface):
+ eth = Ether(src=SRC_MAC, dst=LLDP_MULTICAST_MAC, type=LLDP_ETHERTYPE)
+ lldpdu = eth / LLDPDU()
+ lldpdu /= LLDPDUChassisID(subtype=7, id=CHASSIS_ID)
+ lldpdu /= LLDPDUPortID(subtype=5, id=iface)
+ lldpdu /= LLDPDUTimeToLive(ttl=LLDP_TTL)
+ lldpdu /= LLDPDUEndOfLLDPDU()
+ sendp(lldpdu, iface=iface, verbose=False)
+
+def send_lldp_packets_continuously(iface, stop_event):
+ time.sleep(LLDP_INTERVAL/2)
+ while not stop_event.is_set():
+ send_lldp_packet(iface)
+ time.sleep(LLDP_INTERVAL)
+
+def capture_traffic(iface):
+ with infamy.IsolatedMacVlan(iface) as netns:
+ sniffer = infamy.Sniffer(netns, f"ether proto {LLDP_ETHERTYPE}")
+ with sniffer:
+ time.sleep(SNIFFING_DURATION)
+ return sniffer.output()
+
+def start_lldp_sender_thread(iface):
+ stop = threading.Event()
+ thread = threading.Thread(
+ target=send_lldp_packets_continuously,
+ args=(iface, stop)
+ )
+ thread.start()
+ return stop, thread
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+ _, hdata1 = env.ltop.xlate("host", "data1")
+ _, hdata2 = env.ltop.xlate("host", "data2")
+
+ with test.step("Configure interfaces and disable LLDP daemon"):
+ target.put_config_dicts({
+ "ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": BRIDGE,
+ "type": "infix-if-type:bridge",
+ "enabled": True,
+ },
+ {
+ "name": target["data1"],
+ "enabled": True,
+ "infix-interfaces:bridge-port": {
+ "bridge": "br0"
+ }
+ },
+ {
+ "name": target["data2"],
+ "enabled": True,
+ "infix-interfaces:bridge-port": {
+ "bridge": "br0"
+ }
+ }
+ ]
+ }
+ },
+ "ieee802-dot1ab-lldp": {
+ "lldp": {
+ "enabled": False
+ }
+ }
+ })
+
+ stop_event, sender_thread = start_lldp_sender_thread(hdata1)
+
+ try:
+ with test.step("Verify LLDP absence on host:data2"):
+ captured_traffic = capture_traffic(hdata2)
+ if "LLDP" in captured_traffic.stdout:
+ test.fail()
+
+ finally:
+ stop_event.set()
+ sender_thread.join()
+
+ with test.step("Enable LLDP flooding by setting group forward"):
+ target.put_config_dicts({
+ "ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": BRIDGE,
+ "infix-interfaces:bridge": {
+ "ieee-group-forward": [
+ "lldp"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ })
+
+ stop_event, sender_thread = start_lldp_sender_thread(hdata1)
+
+ try:
+ with test.step("Verify LLDP arrival on host:data2"):
+ captured_traffic = capture_traffic(hdata2)
+ if "LLDP" not in captured_traffic.stdout:
+ test.fail()
+
+ finally:
+ stop_event.set()
+ sender_thread.join()
+
+ test.succeed()
diff --git a/test/case/infix_services/lldp/lldp_ieee_group_forward/topology.dot b/test/case/infix_services/lldp/lldp_ieee_group_forward/topology.dot
new file mode 100644
index 00000000..3db4cf17
--- /dev/null
+++ b/test/case/infix_services/lldp/lldp_ieee_group_forward/topology.dot
@@ -0,0 +1,24 @@
+graph "1x3" {
+ layout="neato";
+ overlap="false";
+ esep="+80";
+
+ node [shape=record, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
+ ];
+
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
+ ];
+
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black", requires="ieee-mc"]
+ host:data2 -- target:data2 [color="black", requires="ieee-mc"]
+}
diff --git a/test/case/infix_services/ssh_server_config/topology.svg b/test/case/infix_services/lldp/lldp_ieee_group_forward/topology.svg
similarity index 100%
rename from test/case/infix_services/ssh_server_config/topology.svg
rename to test/case/infix_services/lldp/lldp_ieee_group_forward/topology.svg
diff --git a/test/case/infix_services/mdns.yaml b/test/case/infix_services/mdns/mdns.yaml
similarity index 100%
rename from test/case/infix_services/mdns.yaml
rename to test/case/infix_services/mdns/mdns.yaml
diff --git a/test/case/infix_services/mdns_allow_deny/Readme.adoc b/test/case/infix_services/mdns/mdns_allow_deny/Readme.adoc
similarity index 100%
rename from test/case/infix_services/mdns_allow_deny/Readme.adoc
rename to test/case/infix_services/mdns/mdns_allow_deny/Readme.adoc
diff --git a/test/case/infix_services/mdns_allow_deny/mdns_allow_deny.adoc b/test/case/infix_services/mdns/mdns_allow_deny/mdns_allow_deny.adoc
similarity index 80%
rename from test/case/infix_services/mdns_allow_deny/mdns_allow_deny.adoc
rename to test/case/infix_services/mdns/mdns_allow_deny/mdns_allow_deny.adoc
index ee1cadb9..6fcb2732 100644
--- a/test/case/infix_services/mdns_allow_deny/mdns_allow_deny.adoc
+++ b/test/case/infix_services/mdns/mdns_allow_deny/mdns_allow_deny.adoc
@@ -10,11 +10,11 @@ with three scenarios:
==== Topology
ifdef::topdoc[]
-image::{topdoc}../../test/case/infix_services/mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
+image::{topdoc}../../test/case/infix_services/mdns/mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
-image::mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
+image::mdns/mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[mDNS allow/deny interfaces topology]
diff --git a/test/case/infix_services/mdns_allow_deny/test.py b/test/case/infix_services/mdns/mdns_allow_deny/test.py
similarity index 100%
rename from test/case/infix_services/mdns_allow_deny/test.py
rename to test/case/infix_services/mdns/mdns_allow_deny/test.py
diff --git a/test/case/infix_services/mdns_allow_deny/topology.dot b/test/case/infix_services/mdns/mdns_allow_deny/topology.dot
similarity index 100%
rename from test/case/infix_services/mdns_allow_deny/topology.dot
rename to test/case/infix_services/mdns/mdns_allow_deny/topology.dot
diff --git a/test/case/infix_services/mdns_allow_deny/topology.svg b/test/case/infix_services/mdns/mdns_allow_deny/topology.svg
similarity index 100%
rename from test/case/infix_services/mdns_allow_deny/topology.svg
rename to test/case/infix_services/mdns/mdns_allow_deny/topology.svg
diff --git a/test/case/infix_services/mdns_enable_disable/Readme.adoc b/test/case/infix_services/mdns/mdns_enable_disable/Readme.adoc
similarity index 100%
rename from test/case/infix_services/mdns_enable_disable/Readme.adoc
rename to test/case/infix_services/mdns/mdns_enable_disable/Readme.adoc
diff --git a/test/case/infix_services/mdns_enable_disable/mdns_enable_disable.adoc b/test/case/infix_services/mdns/mdns_enable_disable/mdns_enable_disable.adoc
similarity index 77%
rename from test/case/infix_services/mdns_enable_disable/mdns_enable_disable.adoc
rename to test/case/infix_services/mdns/mdns_enable_disable/mdns_enable_disable.adoc
index 5816200f..9a7b7ae0 100644
--- a/test/case/infix_services/mdns_enable_disable/mdns_enable_disable.adoc
+++ b/test/case/infix_services/mdns/mdns_enable_disable/mdns_enable_disable.adoc
@@ -5,11 +5,11 @@ Operation and non-operation are confirmed using tcpdump.
==== Topology
ifdef::topdoc[]
-image::{topdoc}../../test/case/infix_services/mdns_enable_disable/topology.svg[mDNS enable disable topology]
+image::{topdoc}../../test/case/infix_services/mdns/mdns_enable_disable/topology.svg[mDNS enable disable topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
-image::mdns_enable_disable/topology.svg[mDNS enable disable topology]
+image::mdns/mdns_enable_disable/topology.svg[mDNS enable disable topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[mDNS enable disable topology]
diff --git a/test/case/infix_services/mdns_enable_disable/test.py b/test/case/infix_services/mdns/mdns_enable_disable/test.py
similarity index 100%
rename from test/case/infix_services/mdns_enable_disable/test.py
rename to test/case/infix_services/mdns/mdns_enable_disable/test.py
diff --git a/test/case/infix_services/mdns_enable_disable/topology.dot b/test/case/infix_services/mdns/mdns_enable_disable/topology.dot
similarity index 100%
rename from test/case/infix_services/mdns_enable_disable/topology.dot
rename to test/case/infix_services/mdns/mdns_enable_disable/topology.dot
diff --git a/test/case/infix_services/mdns_enable_disable/topology.svg b/test/case/infix_services/mdns/mdns_enable_disable/topology.svg
similarity index 100%
rename from test/case/infix_services/mdns_enable_disable/topology.svg
rename to test/case/infix_services/mdns/mdns_enable_disable/topology.svg
diff --git a/test/case/infix_services/ssh.yaml b/test/case/infix_services/ssh/ssh.yaml
similarity index 100%
rename from test/case/infix_services/ssh.yaml
rename to test/case/infix_services/ssh/ssh.yaml
diff --git a/test/case/infix_services/ssh_key_authentication/Readme.adoc b/test/case/infix_services/ssh/ssh_key_authentication/Readme.adoc
similarity index 100%
rename from test/case/infix_services/ssh_key_authentication/Readme.adoc
rename to test/case/infix_services/ssh/ssh_key_authentication/Readme.adoc
diff --git a/test/case/infix_services/ssh_key_authentication/ssh_key_authentication.adoc b/test/case/infix_services/ssh/ssh_key_authentication/ssh_key_authentication.adoc
similarity index 74%
rename from test/case/infix_services/ssh_key_authentication/ssh_key_authentication.adoc
rename to test/case/infix_services/ssh/ssh_key_authentication/ssh_key_authentication.adoc
index b5c664b1..188121fb 100644
--- a/test/case/infix_services/ssh_key_authentication/ssh_key_authentication.adoc
+++ b/test/case/infix_services/ssh/ssh_key_authentication/ssh_key_authentication.adoc
@@ -4,11 +4,11 @@ Verify that 'guest' user can fetch data using only the 'public' key
==== Topology
ifdef::topdoc[]
-image::{topdoc}../../test/case/infix_services/ssh_key_authentication/topology.svg[Generate ssh key pair topology]
+image::{topdoc}../../test/case/infix_services/ssh/ssh_key_authentication/topology.svg[Generate ssh key pair topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
-image::ssh_key_authentication/topology.svg[Generate ssh key pair topology]
+image::ssh/ssh_key_authentication/topology.svg[Generate ssh key pair topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[Generate ssh key pair topology]
diff --git a/test/case/infix_services/ssh_key_authentication/test.py b/test/case/infix_services/ssh/ssh_key_authentication/test.py
similarity index 100%
rename from test/case/infix_services/ssh_key_authentication/test.py
rename to test/case/infix_services/ssh/ssh_key_authentication/test.py
diff --git a/test/case/infix_services/ssh/ssh_key_authentication/topology.dot b/test/case/infix_services/ssh/ssh_key_authentication/topology.dot
new file mode 120000
index 00000000..54a4185e
--- /dev/null
+++ b/test/case/infix_services/ssh/ssh_key_authentication/topology.dot
@@ -0,0 +1 @@
+../../../../infamy/topologies/1x1.dot
\ No newline at end of file
diff --git a/test/case/infix_services/ssh_key_authentication/topology.svg b/test/case/infix_services/ssh/ssh_key_authentication/topology.svg
similarity index 100%
rename from test/case/infix_services/ssh_key_authentication/topology.svg
rename to test/case/infix_services/ssh/ssh_key_authentication/topology.svg
diff --git a/test/case/infix_services/ssh_server_config/Readme.adoc b/test/case/infix_services/ssh/ssh_server_config/Readme.adoc
similarity index 100%
rename from test/case/infix_services/ssh_server_config/Readme.adoc
rename to test/case/infix_services/ssh/ssh_server_config/Readme.adoc
diff --git a/test/case/infix_services/ssh_server_config/ssh_server_config.adoc b/test/case/infix_services/ssh/ssh_server_config/ssh_server_config.adoc
similarity index 78%
rename from test/case/infix_services/ssh_server_config/ssh_server_config.adoc
rename to test/case/infix_services/ssh/ssh_server_config/ssh_server_config.adoc
index ed0f3b7f..e5890611 100644
--- a/test/case/infix_services/ssh_server_config/ssh_server_config.adoc
+++ b/test/case/infix_services/ssh/ssh_server_config/ssh_server_config.adoc
@@ -7,11 +7,11 @@ Test SSH server functionality with pre-defined key pair:
==== Topology
ifdef::topdoc[]
-image::{topdoc}../../test/case/infix_services/ssh_server_config/topology.svg[SSH server configuration topology]
+image::{topdoc}../../test/case/infix_services/ssh/ssh_server_config/topology.svg[SSH server configuration topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
-image::ssh_server_config/topology.svg[SSH server configuration topology]
+image::ssh/ssh_server_config/topology.svg[SSH server configuration topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[SSH server configuration topology]
diff --git a/test/case/infix_services/ssh_server_config/test.py b/test/case/infix_services/ssh/ssh_server_config/test.py
similarity index 100%
rename from test/case/infix_services/ssh_server_config/test.py
rename to test/case/infix_services/ssh/ssh_server_config/test.py
diff --git a/test/case/infix_services/ssh/ssh_server_config/topology.dot b/test/case/infix_services/ssh/ssh_server_config/topology.dot
new file mode 120000
index 00000000..bcb3e9b6
--- /dev/null
+++ b/test/case/infix_services/ssh/ssh_server_config/topology.dot
@@ -0,0 +1 @@
+../../../../infamy/topologies/1x3.dot
\ No newline at end of file
diff --git a/test/case/infix_services/ssh/ssh_server_config/topology.svg b/test/case/infix_services/ssh/ssh_server_config/topology.svg
new file mode 100644
index 00000000..9517e639
--- /dev/null
+++ b/test/case/infix_services/ssh/ssh_server_config/topology.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/test/case/infix_services/ssh_key_authentication/topology.dot b/test/case/infix_services/ssh_key_authentication/topology.dot
deleted file mode 120000
index 02b78869..00000000
--- a/test/case/infix_services/ssh_key_authentication/topology.dot
+++ /dev/null
@@ -1 +0,0 @@
-../../../infamy/topologies/1x1.dot
\ No newline at end of file
diff --git a/test/case/infix_services/ssh_server_config/topology.dot b/test/case/infix_services/ssh_server_config/topology.dot
deleted file mode 100644
index 671ed493..00000000
--- a/test/case/infix_services/ssh_server_config/topology.dot
+++ /dev/null
@@ -1,24 +0,0 @@
-graph "1x3" {
- layout="neato";
- overlap="false";
- esep="+80";
-
- node [shape=record, fontname="DejaVu Sans Mono, Book"];
- edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
-
- host [
- label="host | { mgmt | data1 | data2 }",
- pos="0,12!",
- requires="controller",
- ];
-
- target [
- label="{ mgmt | data1 | data2 } | target",
- pos="10,12!",
- requires="infix",
- ];
-
- host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
- host:data1 -- target:data1 [color="black"]
- host:data2 -- target:data2 [color="black"]
-}
diff --git a/test/infamy/topologies/1x3.dot b/test/infamy/topologies/1x3.dot
index 2ba60559..f288f2f9 100644
--- a/test/infamy/topologies/1x3.dot
+++ b/test/infamy/topologies/1x3.dot
@@ -4,22 +4,21 @@ graph "1x3" {
esep="+80";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
- edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
- label="host | { tgt | data0 | data1 }",
- pos="0,12!",
- requires="controller",
+ label="host | { mgmt | data1 | data2 }",
+ pos="0,12!",
+ requires="controller",
];
- target [
- label="{ mgmt | data0 | data1 } | target",
- pos="10,12!",
-
- requires="infix",
+ target [
+ label="{ mgmt | data1 | data2 } | target",
+ pos="10,12!",
+ requires="infix",
];
- host:tgt -- target:mgmt [requires="mgmt"]
- host:data0 -- target:data0 [color=black]
- host:data1 -- target:data1 [color=black]
-}
\ No newline at end of file
+ host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
+ host:data1 -- target:data1 [color="black"]
+ host:data2 -- target:data2 [color="black"]
+}