mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-06 23:43:20 +02:00
Merge pull request #799 from kernelkit/ospf-bfd
test: Multi-Port Network Namespaces
This commit is contained in:
@@ -16,10 +16,12 @@ endif::testgroup[]
|
||||
endif::topdoc[]
|
||||
==== Test sequence
|
||||
. Set up topology and attach to target DUT
|
||||
. Set up VLAN interface on host:data with IP 10.0.0.1
|
||||
. Configure VLAN 10 interface on target:data with IP 10.0.0.2
|
||||
. Waiting for links to come up
|
||||
. Ping 10.0.0.2 from VLAN 10 on host:data with IP 10.0.0.1
|
||||
. Remove VLAN interface from target:data, and test again (should not be able to ping)
|
||||
. Wait for links to come up
|
||||
. Verify that host:data can reach 10.0.0.2
|
||||
. Remove VLAN interface from target:data
|
||||
. Verify that host:data can no longer reach 10.0.0.2
|
||||
|
||||
|
||||
<<<
|
||||
|
||||
@@ -10,31 +10,21 @@ import infamy.iface as iface
|
||||
|
||||
from infamy import until
|
||||
|
||||
def test_ping(hport, should_pass):
|
||||
with infamy.IsolatedMacVlan(hport) as ns:
|
||||
try:
|
||||
ns.runsh("""
|
||||
set -ex
|
||||
ip link set iface up
|
||||
ip link add dev vlan10 link iface up type vlan id 10
|
||||
ip addr add 10.0.0.1/24 dev vlan10
|
||||
""")
|
||||
|
||||
if should_pass:
|
||||
ns.must_reach("10.0.0.2")
|
||||
else:
|
||||
ns.must_not_reach("10.0.0.2")
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred during the VLAN setup or ping test: {e}")
|
||||
raise
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
_, tport = env.ltop.xlate("target", "data")
|
||||
|
||||
with test.step("Set up VLAN interface on host:data with IP 10.0.0.1"):
|
||||
_, hport = env.ltop.xlate("host", "data")
|
||||
datanet = infamy.IsolatedMacVlan(hport).start()
|
||||
datanet.runsh("""
|
||||
set -ex
|
||||
ip link set iface up
|
||||
ip link add dev vlan10 link iface up type vlan id 10
|
||||
ip addr add 10.0.0.1/24 dev vlan10
|
||||
""")
|
||||
|
||||
with test.step("Configure VLAN 10 interface on target:data with IP 10.0.0.2"):
|
||||
target.put_config_dict("ietf-interfaces", {
|
||||
@@ -64,16 +54,18 @@ with infamy.Test() as test:
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Waiting for links to come up"):
|
||||
with test.step("Wait for links to come up"):
|
||||
until(lambda: iface.get_param(target, tport, "oper-status") == "up")
|
||||
|
||||
with test.step("Ping 10.0.0.2 from VLAN 10 on host:data with IP 10.0.0.1"):
|
||||
with test.step("Verify that host:data can reach 10.0.0.2"):
|
||||
_, hport = env.ltop.xlate("host", "data")
|
||||
test_ping(hport,True)
|
||||
datanet.must_reach("10.0.0.2")
|
||||
|
||||
with test.step("Remove VLAN interface from target:data, and test again (should not be able to ping)"):
|
||||
with test.step("Remove VLAN interface from target:data"):
|
||||
target.delete_xpath(f"/ietf-interfaces:interfaces/interface[name='{tport}.10']")
|
||||
|
||||
with test.step("Verify that host:data can no longer reach 10.0.0.2"):
|
||||
_, hport = env.ltop.xlate("host", "data")
|
||||
test_ping(hport,False)
|
||||
datanet.must_not_reach("10.0.0.2")
|
||||
|
||||
test.succeed()
|
||||
|
||||
@@ -11,3 +11,5 @@ include::ospf_basic/Readme.adoc[]
|
||||
include::ospf_unnumbered_interface/Readme.adoc[]
|
||||
|
||||
include::ospf_multiarea/Readme.adoc[]
|
||||
|
||||
include::ospf_bfd/Readme.adoc[]
|
||||
|
||||
@@ -10,3 +10,6 @@
|
||||
|
||||
- name: ospf_multiarea
|
||||
case: ospf_multiarea/test.py
|
||||
|
||||
- name: ospf_bfd
|
||||
case: ospf_bfd/test.py
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
=== OSPF BFD
|
||||
==== Description
|
||||
Verify that a router running OSPF, with Bidirectional Forwarding
|
||||
Detection (BFD) enabled, will detect link faults even when the
|
||||
physical layer is still operational.
|
||||
|
||||
This can typically happen when one logical link, from OSPF's
|
||||
perspective, is made up of multiple physical links containing media
|
||||
converters without link fault forwarding.
|
||||
|
||||
==== Topology
|
||||
ifdef::topdoc[]
|
||||
image::../../test/case/ietf_routing/ospf_bfd/topology.svg[OSPF BFD topology]
|
||||
endif::topdoc[]
|
||||
ifndef::topdoc[]
|
||||
ifdef::testgroup[]
|
||||
image::ospf_bfd/topology.svg[OSPF BFD topology]
|
||||
endif::testgroup[]
|
||||
ifndef::testgroup[]
|
||||
image::topology.svg[OSPF BFD topology]
|
||||
endif::testgroup[]
|
||||
endif::topdoc[]
|
||||
==== Test sequence
|
||||
. Set up topology and attach to target DUTs
|
||||
. Setup TPMR between R1fast and R2fast
|
||||
. Configure R1 and R2
|
||||
. Setup IP addresses and default routes on h1 and h2
|
||||
. Wait for R1 and R2 to peer
|
||||
. Verify connectivity from PC:src to PC:dst via fast link
|
||||
. Disable forwarding between R1fast and R2fast to trigger fail-over
|
||||
. Verify connectivity from PC:src to PC:dst via slow link
|
||||
|
||||
|
||||
<<<
|
||||
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
OSPF BFD
|
||||
|
||||
Verify that a router running OSPF, with Bidirectional Forwarding
|
||||
Detection (BFD) enabled, will detect link faults even when the
|
||||
physical layer is still operational.
|
||||
|
||||
This can typically happen when one logical link, from OSPF's
|
||||
perspective, is made up of multiple physical links containing media
|
||||
converters without link fault forwarding.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import infamy
|
||||
import infamy.route as route
|
||||
from infamy.netns import TPMR
|
||||
from infamy.util import until, parallel
|
||||
|
||||
def config(target, params):
|
||||
name = params["name"]
|
||||
dif, fif, sif = \
|
||||
params["link"]["data"], \
|
||||
params["link"]["fast"], \
|
||||
params["link"]["slow"]
|
||||
rid, daddr, faddr, saddr = \
|
||||
params["addr"]["rid"], \
|
||||
params["addr"]["data"], \
|
||||
params["addr"]["fast"], \
|
||||
params["addr"]["slow"]
|
||||
|
||||
def ifconfig(name, addr, plen):
|
||||
return {
|
||||
"name": name,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"forwarding": True,
|
||||
"address": [{
|
||||
"ip": addr,
|
||||
"prefix-length": plen,
|
||||
}]}
|
||||
}
|
||||
|
||||
target.put_config_dict("ietf-interfaces", {
|
||||
"interfaces": {
|
||||
"interface": [
|
||||
ifconfig("lo", rid, 32),
|
||||
|
||||
ifconfig(dif, daddr, 24),
|
||||
ifconfig(fif, faddr, 30),
|
||||
ifconfig(sif, saddr, 30),
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
target.put_config_dict("ietf-system", {
|
||||
"system": {
|
||||
"hostname": name,
|
||||
}
|
||||
})
|
||||
|
||||
target.put_config_dict("ietf-routing", {
|
||||
"routing": {
|
||||
"control-plane-protocols": {
|
||||
"control-plane-protocol": [{
|
||||
"type": "infix-routing:ospfv2",
|
||||
"name": "default",
|
||||
"ospf": {
|
||||
"areas": {
|
||||
"area": [{
|
||||
"area-id": "0.0.0.0",
|
||||
"interfaces":
|
||||
{
|
||||
"interface": [{
|
||||
"bfd": {
|
||||
"enabled": True
|
||||
},
|
||||
"name": fif,
|
||||
"hello-interval": 1,
|
||||
"dead-interval": 10,
|
||||
"cost": 100,
|
||||
},
|
||||
{
|
||||
"bfd": {
|
||||
"enabled": True
|
||||
},
|
||||
"name": sif,
|
||||
"hello-interval": 1,
|
||||
"dead-interval": 10,
|
||||
"cost": 200,
|
||||
}, {
|
||||
"name": dif,
|
||||
"passive": 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")
|
||||
|
||||
with test.step("Setup TPMR between R1fast and R2fast"):
|
||||
breaker = TPMR(env.ltop.xlate("PC", "R1fast")[1],
|
||||
env.ltop.xlate("PC", "R2fast")[1]).start()
|
||||
|
||||
with test.step("Configure R1 and R2"):
|
||||
r1cfg = {
|
||||
"name": "R1",
|
||||
"addr": {
|
||||
"rid": "192.168.1.1",
|
||||
|
||||
"data": "192.168.10.1",
|
||||
"fast": "192.168.100.1",
|
||||
"slow": "192.168.200.1",
|
||||
},
|
||||
"link": {
|
||||
"data": env.ltop.xlate("R1", "h1")[1],
|
||||
"fast": env.ltop.xlate("R1", "fast")[1],
|
||||
"slow": env.ltop.xlate("R1", "slow")[1],
|
||||
}
|
||||
}
|
||||
r2cfg = {
|
||||
"name": "R2",
|
||||
"addr": {
|
||||
"rid": "192.168.1.2",
|
||||
|
||||
"data": "192.168.20.1",
|
||||
"fast": "192.168.100.2",
|
||||
"slow": "192.168.200.2",
|
||||
},
|
||||
"link": {
|
||||
"data": env.ltop.xlate("R2", "h2")[1],
|
||||
"fast": env.ltop.xlate("R2", "fast")[1],
|
||||
"slow": env.ltop.xlate("R2", "slow")[1],
|
||||
}
|
||||
}
|
||||
|
||||
parallel(config(R1, r1cfg), config(R2, r2cfg))
|
||||
|
||||
with test.step("Setup IP addresses and default routes on h1 and h2"):
|
||||
_, h1 = env.ltop.xlate("PC", "h1")
|
||||
_, h2 = env.ltop.xlate("PC", "h2")
|
||||
|
||||
h1net = infamy.IsolatedMacVlan(h1).start()
|
||||
h1net.addip("192.168.10.2")
|
||||
h1net.addroute("default", "192.168.10.1")
|
||||
|
||||
h2net = infamy.IsolatedMacVlan(h2).start()
|
||||
h2net.addip("192.168.20.2")
|
||||
h2net.addroute("default", "192.168.20.1")
|
||||
|
||||
with test.step("Wait for R1 and R2 to peer"):
|
||||
print("Waiting for R1 and R2 to peer")
|
||||
until(lambda: route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-ospf:ospfv2"), attempts=200)
|
||||
until(lambda: route.ipv4_route_exist(R2, "192.168.10.0/24", proto="ietf-ospf:ospfv2"), attempts=200)
|
||||
|
||||
with test.step("Verify connectivity from PC:src to PC:dst via fast link"):
|
||||
h1net.must_reach("192.168.20.2")
|
||||
hops = [row[1] for row in h1net.traceroute("192.168.20.2")]
|
||||
assert "192.168.100.2" in hops, f"Path to h2 ({repr(hops)}), does not use fast link"
|
||||
|
||||
with test.step("Disable forwarding between R1fast and R2fast to trigger fail-over"):
|
||||
breaker.block()
|
||||
print("Give BFD some time to detect the bad link, " +
|
||||
"but not enough for the OSPF dead interval expire")
|
||||
time.sleep(1)
|
||||
|
||||
with test.step("Verify connectivity from PC:src to PC:dst via slow link"):
|
||||
h1net.must_reach("192.168.20.2")
|
||||
hops = [row[1] for row in h1net.traceroute("192.168.20.2")]
|
||||
assert "192.168.200.2" in hops, f"Path to h2 ({repr(hops)}), does not use slow link"
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,39 @@
|
||||
graph "ospf-bfd" {
|
||||
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"];
|
||||
|
||||
R1 [
|
||||
label=" { { R1 | <slow> slow } | { <mgmt> mgmt | <h1> h1 | <fast> fast } }",
|
||||
pos="0,6!",
|
||||
|
||||
kind="infix",
|
||||
];
|
||||
R2 [
|
||||
label="{ { <slow> slow | R2 } | { <fast> fast | <h2> h2 | <mgmt> mgmt } }",
|
||||
pos="18,6!",
|
||||
|
||||
kind="infix",
|
||||
];
|
||||
|
||||
PC [
|
||||
label="{ { <R1mgmt> R1mgmt | <h1> h1 | <R1fast> R1fast | <R2fast> R2fast | <h2> h2 | <R2mgmt> R2mgmt } | PC }",
|
||||
pos="9,0!",
|
||||
kind="controller",
|
||||
];
|
||||
|
||||
PC:R1mgmt -- R1:mgmt [kind=mgmt, color="lightgray"]
|
||||
PC:R2mgmt -- R2:mgmt [kind=mgmt, color="lightgray"]
|
||||
|
||||
PC:h1 -- R1:h1
|
||||
PC:h2 -- R2:h2
|
||||
|
||||
R1:fast -- PC:R1fast [color="lightgreen", taillabel="Cost: 100"]
|
||||
R2:fast -- PC:R2fast [color="lightgreen"]
|
||||
|
||||
R1:slow -- R2:slow [color="crimson", taillabel="Cost: 200"]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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: ospf-bfd Pages: 1 -->
|
||||
<svg width="432pt" height="156pt"
|
||||
viewBox="0.00 0.00 432.03 155.51" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 151.51)">
|
||||
<title>ospf-bfd</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-151.51 428.03,-151.51 428.03,4 -4,4"/>
|
||||
<!-- R1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>R1</title>
|
||||
<polygon fill="none" stroke="black" points="0,-97.51 0,-143.51 133,-143.51 133,-97.51 0,-97.51"/>
|
||||
<text text-anchor="middle" x="29" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1</text>
|
||||
<polyline fill="none" stroke="black" points="58,-120.51 58,-143.51 "/>
|
||||
<text text-anchor="middle" x="95.5" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">slow</text>
|
||||
<polyline fill="none" stroke="black" points="0,-120.51 133,-120.51 "/>
|
||||
<text text-anchor="middle" x="25" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="50,-97.51 50,-120.51 "/>
|
||||
<text text-anchor="middle" x="66.5" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">h1</text>
|
||||
<polyline fill="none" stroke="black" points="83,-97.51 83,-120.51 "/>
|
||||
<text text-anchor="middle" x="108" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">fast</text>
|
||||
</g>
|
||||
<!-- R2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>R2</title>
|
||||
<polygon fill="none" stroke="black" points="291.03,-97.51 291.03,-143.51 424.03,-143.51 424.03,-97.51 291.03,-97.51"/>
|
||||
<text text-anchor="middle" x="328.53" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">slow</text>
|
||||
<polyline fill="none" stroke="black" points="366.03,-120.51 366.03,-143.51 "/>
|
||||
<text text-anchor="middle" x="395.03" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2</text>
|
||||
<polyline fill="none" stroke="black" points="291.03,-120.51 424.03,-120.51 "/>
|
||||
<text text-anchor="middle" x="316.03" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">fast</text>
|
||||
<polyline fill="none" stroke="black" points="341.03,-97.51 341.03,-120.51 "/>
|
||||
<text text-anchor="middle" x="357.53" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">h2</text>
|
||||
<polyline fill="none" stroke="black" points="374.03,-97.51 374.03,-120.51 "/>
|
||||
<text text-anchor="middle" x="399.03" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
</g>
|
||||
<!-- R1--R2 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>R1:slow--R2:slow</title>
|
||||
<path fill="none" stroke="crimson" stroke-width="2" d="M133.5,-132.51C133.5,-132.51 290.53,-132.51 290.53,-132.51"/>
|
||||
<text text-anchor="middle" x="168" y="-136.31" font-family="DejaVu Serif, Book" font-size="14.00">Cost: 200</text>
|
||||
</g>
|
||||
<!-- PC -->
|
||||
<g id="node3" class="node">
|
||||
<title>PC</title>
|
||||
<polygon fill="none" stroke="black" points="47.01,-0.5 47.01,-46.5 377.01,-46.5 377.01,-0.5 47.01,-0.5"/>
|
||||
<text text-anchor="middle" x="80.01" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="113.01,-23.5 113.01,-46.5 "/>
|
||||
<text text-anchor="middle" x="129.51" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">h1</text>
|
||||
<polyline fill="none" stroke="black" points="146.01,-23.5 146.01,-46.5 "/>
|
||||
<text text-anchor="middle" x="179.01" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1fast</text>
|
||||
<polyline fill="none" stroke="black" points="212.01,-23.5 212.01,-46.5 "/>
|
||||
<text text-anchor="middle" x="245.01" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2fast</text>
|
||||
<polyline fill="none" stroke="black" points="278.01,-23.5 278.01,-46.5 "/>
|
||||
<text text-anchor="middle" x="294.51" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">h2</text>
|
||||
<polyline fill="none" stroke="black" points="311.01,-23.5 311.01,-46.5 "/>
|
||||
<text text-anchor="middle" x="344.01" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="47.01,-23.5 377.01,-23.5 "/>
|
||||
<text text-anchor="middle" x="212.01" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">PC</text>
|
||||
</g>
|
||||
<!-- R1--PC -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>R1:fast--PC:R1fast</title>
|
||||
<path fill="none" stroke="lightgreen" stroke-width="2" d="M133.5,-108.51C133.5,-108.51 179.01,-46.5 179.01,-46.5"/>
|
||||
<text text-anchor="middle" x="168" y="-112.31" font-family="DejaVu Serif, Book" font-size="14.00">Cost: 100</text>
|
||||
</g>
|
||||
<!-- R2--PC -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>R2:fast--PC:R2fast</title>
|
||||
<path fill="none" stroke="lightgreen" stroke-width="2" d="M290.53,-108.51C290.53,-108.51 245.01,-46.5 245.01,-46.5"/>
|
||||
</g>
|
||||
<!-- PC--R1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>PC:R1mgmt--R1:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M80.01,-46.5C80.01,-46.5 24.5,-97.51 24.5,-97.51"/>
|
||||
</g>
|
||||
<!-- PC--R1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>PC:h1--R1:h1</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M129.01,-46.5C129.01,-46.5 66.5,-97.51 66.5,-97.51"/>
|
||||
</g>
|
||||
<!-- PC--R2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>PC:R2mgmt--R2:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M344.01,-46.5C344.01,-46.5 399.53,-97.51 399.53,-97.51"/>
|
||||
</g>
|
||||
<!-- PC--R2 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>PC:h2--R2:h2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M295.01,-46.5C295.01,-46.5 357.53,-97.51 357.53,-97.51"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
+164
-27
@@ -1,4 +1,5 @@
|
||||
import ctypes
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import random
|
||||
@@ -18,31 +19,58 @@ def setns(fd, nstype):
|
||||
__NR_setns = 308
|
||||
__libc.syscall(__NR_setns, fd, nstype)
|
||||
|
||||
class IsolatedMacVlan:
|
||||
"""Create an isolated interface on top of a PC interface."""
|
||||
def __init__(self, parent, ifname="iface", lo=True):
|
||||
class IsolatedMacVlans:
|
||||
"""A network namespace containing a multiple MACVLANs
|
||||
|
||||
Stacks a MACVLAN on top of each specificed controller interface,
|
||||
and moves those interfaces to a separate namespace, isolating it
|
||||
from all others.
|
||||
|
||||
NOTE: For the simple case when only one interface needs to be
|
||||
mapped, see IsolatedMacVlan below.
|
||||
|
||||
Example:
|
||||
|
||||
netns = IsolatedMacVlans({ "eth2": "a", "eth3": "b" })
|
||||
|
||||
netns:
|
||||
.--------.
|
||||
| a b | (MACVLANs)
|
||||
'-+----+-'
|
||||
| |
|
||||
eth0 eth1 eth2 eth3
|
||||
|
||||
"""
|
||||
|
||||
Instances = []
|
||||
def Cleanup():
|
||||
for ns in list(IsolatedMacVlans.Instances):
|
||||
ns.stop()
|
||||
|
||||
def __init__(self, ifmap, lo=True):
|
||||
self.sleeper = None
|
||||
self.parent, self.ifname, self.lo = parent, ifname, lo
|
||||
self.ifmap, self.lo = ifmap, lo
|
||||
self.ping_timeout = env.ENV.attr("ping_timeout", 5)
|
||||
|
||||
def __enter__(self):
|
||||
def start(self):
|
||||
self.sleeper = subprocess.Popen(["unshare", "-r", "-n", "sh", "-c",
|
||||
"echo && exec sleep infinity"],
|
||||
stdout=subprocess.PIPE)
|
||||
self.sleeper.stdout.readline()
|
||||
|
||||
try:
|
||||
subprocess.run(["ip", "link", "add",
|
||||
"dev", self.ifname,
|
||||
"link", self.parent,
|
||||
"address", self._stable_mac(),
|
||||
"netns", str(self.sleeper.pid),
|
||||
"type", "macvlan"], check=True)
|
||||
self.runsh(f"""
|
||||
while ! ip link show dev {self.ifname}; do
|
||||
sleep 0.1
|
||||
done
|
||||
""")
|
||||
for parent, ifname in self.ifmap.items():
|
||||
subprocess.run(["ip", "link", "add",
|
||||
"dev", ifname,
|
||||
"link", parent,
|
||||
"address", self._stable_mac(parent),
|
||||
"netns", str(self.sleeper.pid),
|
||||
"type", "macvlan", "mode", "passthru"], check=True)
|
||||
self.runsh(f"""
|
||||
while ! ip link show dev {ifname}; do
|
||||
sleep 0.1
|
||||
done
|
||||
""")
|
||||
except Exception as e:
|
||||
self.__exit__(None, None, None)
|
||||
raise e
|
||||
@@ -54,14 +82,43 @@ class IsolatedMacVlan:
|
||||
self.__exit__(None, None, None)
|
||||
raise e
|
||||
|
||||
self.Instances.append(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, val, typ, tb):
|
||||
def stop(self):
|
||||
self.sleeper.kill()
|
||||
self.sleeper.wait()
|
||||
time.sleep(0.5)
|
||||
|
||||
def _stable_mac(self):
|
||||
for n in range(100):
|
||||
promisc = False
|
||||
for parent in self.ifmap.keys():
|
||||
iplink = subprocess.run(f"ip -d -j link show dev {parent}".split(),
|
||||
stdout=subprocess.PIPE, check=True)
|
||||
link = json.loads(iplink.stdout)[0]
|
||||
if link["promiscuity"]:
|
||||
# Use promisc as a substitute for an indicator
|
||||
# of whether the kernel has actually removed
|
||||
# the passthru MACVLAN yet or not
|
||||
promisc = True
|
||||
break
|
||||
|
||||
if not promisc:
|
||||
break
|
||||
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise TimeoutError("Lingering MACVLAN")
|
||||
|
||||
if self in self.Instances:
|
||||
self.Instances.remove(self)
|
||||
|
||||
def __enter__(self):
|
||||
return self.start()
|
||||
|
||||
def __exit__(self, val, typ, tb):
|
||||
return self.stop()
|
||||
|
||||
def _stable_mac(self, parent):
|
||||
"""Generate address for MACVLAN
|
||||
|
||||
By default, the kernel will assign a random address. This
|
||||
@@ -75,7 +132,7 @@ class IsolatedMacVlan:
|
||||
the resource exhaustion issue.
|
||||
|
||||
"""
|
||||
random.seed(self.parent)
|
||||
random.seed(parent)
|
||||
a = list(random.randbytes(6))
|
||||
a[0] |= 0x02
|
||||
a[0] &= ~0x01
|
||||
@@ -146,13 +203,13 @@ class IsolatedMacVlan:
|
||||
ip -{p} route add {subnet}{prefix_length} via {nexthop}
|
||||
""", check=True)
|
||||
|
||||
def addip(self, addr, prefix_length=24, proto="ipv4"):
|
||||
def addip(self, ifname, addr, prefix_length=24, proto="ipv4"):
|
||||
p=proto[3]
|
||||
|
||||
self.runsh(f"""
|
||||
set -ex
|
||||
ip link set iface up
|
||||
ip -{p} addr add {addr}/{prefix_length} dev iface
|
||||
ip -{p} addr add {addr}/{prefix_length} dev {ifname}
|
||||
""", check=True)
|
||||
|
||||
|
||||
@@ -188,8 +245,7 @@ class IsolatedMacVlan:
|
||||
|
||||
raise Exception(res)
|
||||
|
||||
def must_receive(self, expr, timeout=None, ifname=None, must=True):
|
||||
ifname = ifname if ifname else self.ifname
|
||||
def must_receive(self, expr, ifname, timeout=None, must=True):
|
||||
timeout = timeout if timeout else self.ping_timeout
|
||||
|
||||
tshark = self.run(["tshark", "-nl", f"-i{ifname}",
|
||||
@@ -207,10 +263,43 @@ class IsolatedMacVlan:
|
||||
def must_not_receive(self, *args, **kwargs):
|
||||
self.must_receive(*args, **kwargs, must=False)
|
||||
|
||||
def pcap(self, expr, ifname=None):
|
||||
ifname = ifname if ifname else self.ifname
|
||||
def pcap(self, expr, ifname):
|
||||
return Pcap(self, ifname, expr)
|
||||
|
||||
class IsolatedMacVlan(IsolatedMacVlans):
|
||||
"""A network namespace containing a single MACVLAN
|
||||
|
||||
Stacks a MACVLAN on top of an interface on the controller, and
|
||||
moves that interface to a separate namespace, isolating it from
|
||||
all other interfaces.
|
||||
|
||||
Example:
|
||||
|
||||
netns = IsolatedMacVlan("eth3")
|
||||
|
||||
netns:
|
||||
.-------.
|
||||
| iface | (MACVLAN)
|
||||
'---+---'
|
||||
|
|
||||
eth0 eth1 eth2 eth3
|
||||
|
||||
"""
|
||||
def __init__(self, parent, ifname="iface", lo=True):
|
||||
self._ifname = ifname
|
||||
return super().__init__(ifmap={ parent: ifname }, lo=lo)
|
||||
|
||||
def addip(self, addr, prefix_length=24, proto="ipv4"):
|
||||
return super().addip(ifname=self._ifname, addr=addr, prefix_length=prefix_length, proto=proto)
|
||||
|
||||
def must_receive(self, expr, timeout=None, ifname=None, must=True):
|
||||
ifname = ifname if ifname else self._ifname
|
||||
return super().must_receive(expr=expr, ifname=ifname, timeout=timeout, must=must)
|
||||
|
||||
def pcap(self, expr, ifname=None):
|
||||
ifname = ifname if ifname else self._ifname
|
||||
return super().pcap(expr=expr, ifname=ifname)
|
||||
|
||||
class Pcap:
|
||||
def __init__(self, netns, ifname, expr):
|
||||
self.netns, self.ifname, self.expr = netns, ifname, expr
|
||||
@@ -253,7 +342,6 @@ class Pcap:
|
||||
# terminating the capture.
|
||||
time.sleep(sleep)
|
||||
|
||||
self.proc.send_signal(signal.SIGUSR2)
|
||||
self.proc.terminate()
|
||||
try:
|
||||
_, stderr = self.proc.communicate(5)
|
||||
@@ -272,3 +360,52 @@ class Pcap:
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, check=True)
|
||||
return tcpdump.stdout
|
||||
|
||||
class TPMR(IsolatedMacVlans):
|
||||
"""Two-Port MAC Relay
|
||||
|
||||
Creates a network namespace containing two controller interfaces
|
||||
(`a` and `b`). By default, tc rules are setup to copy all frames
|
||||
ingressing on `a` to egress on `b`, and vice versa.
|
||||
|
||||
These rules can be removed and reinserted dynamically using the
|
||||
`block()` and `forward()` methods, respectively.
|
||||
|
||||
This is useful to verify the correctness of fail-over behavior in
|
||||
various protocols. See ospf_bfd for a usage example.
|
||||
"""
|
||||
|
||||
def __init__(self, a, b):
|
||||
super().__init__(ifmap={ a: "a", b: "b" }, lo=False)
|
||||
|
||||
def start(self, forward=True):
|
||||
ret = super().start()
|
||||
|
||||
for dev in ("a", "b"):
|
||||
self.run(f"ip link set dev {dev} promisc on up".split())
|
||||
self.run(f"tc qdisc add dev {dev} clsact".split())
|
||||
|
||||
if forward:
|
||||
self.forward()
|
||||
|
||||
return ret
|
||||
|
||||
def _clear_ingress(self, iface):
|
||||
return self.run(f"tc filter del dev {iface} ingress".split())
|
||||
|
||||
def _add_redir(self, frm, to):
|
||||
cmd = \
|
||||
"tc filter add dev".split() \
|
||||
+ [frm] \
|
||||
+ "ingress matchall action mirred egress redirect dev".split() \
|
||||
+ [to]
|
||||
return self.run(cmd)
|
||||
|
||||
def forward(self):
|
||||
for iface in ("a", "b"):
|
||||
self._clear_ingress(iface)
|
||||
self._add_redir(iface, "a" if iface == "b" else "b")
|
||||
|
||||
def block(self):
|
||||
for iface in ("a", "b"):
|
||||
self._clear_ingress(iface)
|
||||
|
||||
@@ -4,6 +4,8 @@ import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import infamy.netns
|
||||
|
||||
class Test:
|
||||
def __init__(self, output=sys.stdout):
|
||||
self.out = output
|
||||
@@ -24,6 +26,8 @@ class Test:
|
||||
self.out.write(f"# Exiting ({now})\n")
|
||||
self.out.flush()
|
||||
|
||||
infamy.netns.IsolatedMacVlans.Cleanup()
|
||||
|
||||
if not e:
|
||||
self._not_ok("Missing explicit test result\n")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user