Skip to content

Commit baeab04

Browse files
authored
Pass-through/transparent management interfaces (#268)
* vrnetlab: Add pass-through management interfaces * vjunos: Add pass-through management interface support * vrnetlab: Use JSON output of iproute2 * vrnetlab: Add exception for serial console ports 5000-5007 for transparent mode mgmt interface * vrnetlab: Remove non-working port 5000 tc mirred exception, redirect to correct interface * vrnetlab: Use tc clsact qdisc and flower matching as best practice * vrnetlab: Re-add workaround for serial ports in transparent mgmt mode * vrnetlab: Add IPv6 support to management address/gw functions * vjunos: Add IPv6 management addresses, fix v4 address templating * vrnetlab: Set dummy IPv6 address/gw for hostfwd management
1 parent 75d5060 commit baeab04

File tree

8 files changed

+187
-38
lines changed

8 files changed

+187
-38
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ Other connection mode values are:
3939
* ovs-bridge - same as a regular bridge, but uses OvS. Can pass LACP traffic.
4040
* macvtap
4141

42+
## Management interface
43+
44+
There are two types of management connectivity for NOS VMs: _pass-through_ and _host-forwarded_ (legacy) management interfaces.
45+
46+
_Pass-through management_ interfaces allows the use of the assigned management IP within the NOS VM, management traffic is transparently passed through to the VM, and the NOS configuration can accurately reflect the management IP. However, it is no longer possible to send or receive traffic directly in the vrnetlab container (e.g. for installing additional packages within the container), other than to pre-defined exceptions, such as the QEMU serial port on TCP port 5000.
47+
48+
NOSes defaulting to _pass-through_ management interfaces are:
49+
- All vJunos routers
50+
51+
In case of _host-forwarded_ management interfaces, certain ports are forwarded to the NOS VM IP, which is always 10.0.0.15/24. The management gateway in this case is 10.0.0.2/24, and outgoing traffic is NATed to the container management IP. This management interface connection mode does not allow for traffic such as LLDP to pass through the management interface.
52+
53+
NOSes defaulting to _host-forwarded_ management interfaces are:
54+
- Every NOS not listed as pass-through management
55+
56+
It is possible to change from the default management interface mode by setting the `CLAB_MGMT_PASSTHROUGH` environment variable to 'true' or 'false', however, it is left up to the user to provide a startup configuration compatible with the requested mode.
57+
4258
## Which vrnetlab routers are supported?
4359
Since the changes we made in this fork are VM specific, we added a few popular routing products:
4460

common/vrnetlab.py

+119-21
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(
7777
provision_pci_bus=True,
7878
cpu="host",
7979
smp="1",
80+
mgmt_passthrough=False,
8081
min_dp_nics=0,
8182
):
8283
self.logger = logging.getLogger()
@@ -108,6 +109,22 @@ def __init__(
108109
# to have them allocated sequential from eth1
109110
self.highest_provisioned_nic_num = 0
110111

112+
# Whether the management interface is pass-through or host-forwarded
113+
self.mgmt_nic_passthrough = mgmt_passthrough
114+
mgmt_passthrough_override = os.environ.get("CLAB_MGMT_PASSTHROUGH", "")
115+
if mgmt_passthrough_override:
116+
self.mgmt_nic_passthrough = mgmt_passthrough_override.lower() == "true"
117+
118+
# Populate management IP and gateway
119+
if self.mgmt_nic_passthrough:
120+
self.mgmt_address_ipv4, self.mgmt_address_ipv6 = self.get_mgmt_address()
121+
self.mgmt_gw_ipv4, self.mgmt_gw_ipv6 = self.get_mgmt_gw()
122+
else:
123+
self.mgmt_address_ipv4 = "10.0.0.15/24"
124+
self.mgmt_address_ipv6 = "2001:db8::2/64"
125+
self.mgmt_gw_ipv4 = "10.0.0.2"
126+
self.mgmt_gw_ipv6 = "2001:db8::1"
127+
111128
self.insuffucient_nics = False
112129
self.min_nics = 0
113130
# if an image needs minimum amount of dataplane nics to bootup, specify
@@ -282,45 +299,126 @@ def create_tc_tap_ifup(self):
282299
ip link set $TAP_IF mtu 65000
283300
284301
# create tc eth<->tap redirect rules
285-
tc qdisc add dev eth$INDEX ingress
286-
tc filter add dev eth$INDEX parent ffff: protocol all u32 match u8 0 0 action mirred egress redirect dev tap$INDEX
302+
tc qdisc add dev eth$INDEX clsact
303+
tc filter add dev eth$INDEX ingress flower action mirred egress redirect dev tap$INDEX
287304
288-
tc qdisc add dev $TAP_IF ingress
289-
tc filter add dev $TAP_IF parent ffff: protocol all u32 match u8 0 0 action mirred egress redirect dev eth$INDEX
305+
tc qdisc add dev $TAP_IF clsact
306+
tc filter add dev $TAP_IF ingress flower action mirred egress redirect dev eth$INDEX
290307
"""
291308

292309
with open("/etc/tc-tap-ifup", "w") as f:
293310
f.write(ifup_script)
294311
os.chmod("/etc/tc-tap-ifup", 0o777)
295312

313+
def create_tc_tap_mgmt_ifup(self):
314+
"""Create tap ifup script that is used in tc datapath mode, specifically for the management interface"""
315+
ifup_script = """#!/bin/bash
316+
317+
ip link set tap0 up
318+
ip link set tap0 mtu 65000
319+
320+
# create tc eth<->tap redirect rules
321+
322+
tc qdisc add dev eth0 clsact
323+
# exception for TCP ports 5000-5007
324+
tc filter add dev eth0 ingress prio 1 protocol ip flower ip_proto tcp dst_port 5000-5007 action pass
325+
# mirror ARP traffic to container
326+
tc filter add dev eth0 ingress prio 2 protocol arp flower action mirred egress mirror dev tap0
327+
# redirect rest of ingress traffic of eth0 to egress of tap0
328+
tc filter add dev eth0 ingress prio 3 flower action mirred egress redirect dev tap0
329+
330+
tc qdisc add dev tap0 clsact
331+
# redirect all ingress traffic of tap0 to egress of eth0
332+
tc filter add dev tap0 ingress flower action mirred egress redirect dev eth0
333+
334+
# clone management MAC of the VM
335+
ip link set dev eth0 address {MGMT_MAC}
336+
"""
337+
338+
ifup_script = ifup_script.replace("{MGMT_MAC}", self.mgmt_mac)
339+
340+
with open("/etc/tc-tap-mgmt-ifup", "w") as f:
341+
f.write(ifup_script)
342+
os.chmod("/etc/tc-tap-mgmt-ifup", 0o777)
343+
296344
def gen_mgmt(self):
297345
"""Generate qemu args for the mgmt interface(s)"""
298346
res = []
299-
# mgmt interface is special - we use qemu user mode network
300347
res.append("-device")
301348
mac = (
302349
"c0:00:01:00:ca:fe"
303350
if getattr(self, "_static_mgmt_mac", False)
304351
else gen_mac(0)
305352
)
306-
res.append(self.nic_type + f",netdev=p00,mac={mac}")
307-
res.append("-netdev")
308-
res.append(
309-
"user,id=p00,net=10.0.0.0/24,"
310-
"tftp=/tftpboot,"
311-
"hostfwd=tcp:0.0.0.0:22-10.0.0.15:22," # ssh
312-
"hostfwd=udp:0.0.0.0:161-10.0.0.15:161," # snmp
313-
"hostfwd=tcp:0.0.0.0:830-10.0.0.15:830," # netconf
314-
"hostfwd=tcp:0.0.0.0:80-10.0.0.15:80," # http
315-
"hostfwd=tcp:0.0.0.0:443-10.0.0.15:443," # https
316-
"hostfwd=tcp:0.0.0.0:9339-10.0.0.15:9339," # iana gnmi/gnoi
317-
"hostfwd=tcp:0.0.0.0:57400-10.0.0.15:57400," # nokia gnmi/gnoi
318-
"hostfwd=tcp:0.0.0.0:6030-10.0.0.15:6030," # gnmi/gnoi arista
319-
"hostfwd=tcp:0.0.0.0:32767-10.0.0.15:32767," # gnmi/gnoi juniper
320-
"hostfwd=tcp:0.0.0.0:8080-10.0.0.15:8080" # sonic gnmi/gnoi, other http apis
321-
)
353+
self.mgmt_mac = mac
354+
res.append(self.nic_type + f",netdev=p00,mac={self.mgmt_mac}")
355+
356+
if self.mgmt_nic_passthrough:
357+
# mgmt interface is passthrough - we just create a normal mirred tap interface
358+
if self.conn_mode == "tc":
359+
res.append("-netdev")
360+
res.append("tap,id=p00,ifname=tap0,script=/etc/tc-tap-mgmt-ifup,downscript=no")
361+
self.create_tc_tap_mgmt_ifup()
362+
else:
363+
# mgmt interface is special - we use qemu user mode network
364+
res.append("-netdev")
365+
res.append(
366+
"user,id=p00,net=10.0.0.0/24,"
367+
"tftp=/tftpboot,"
368+
"hostfwd=tcp:0.0.0.0:22-10.0.0.15:22," # ssh
369+
"hostfwd=udp:0.0.0.0:161-10.0.0.15:161," # snmp
370+
"hostfwd=tcp:0.0.0.0:830-10.0.0.15:830," # netconf
371+
"hostfwd=tcp:0.0.0.0:80-10.0.0.15:80," # http
372+
"hostfwd=tcp:0.0.0.0:443-10.0.0.15:443," # https
373+
"hostfwd=tcp:0.0.0.0:9339-10.0.0.15:9339," # iana gnmi/gnoi
374+
"hostfwd=tcp:0.0.0.0:57400-10.0.0.15:57400," # nokia gnmi/gnoi
375+
"hostfwd=tcp:0.0.0.0:6030-10.0.0.15:6030," # gnmi/gnoi arista
376+
"hostfwd=tcp:0.0.0.0:32767-10.0.0.15:32767," # gnmi/gnoi juniper
377+
"hostfwd=tcp:0.0.0.0:8080-10.0.0.15:8080" # sonic gnmi/gnoi, other http apis
378+
)
322379
return res
323380

381+
def get_mgmt_address(self):
382+
""" Returns the IPv4 and IPv6 address of the eth0 interface of the container"""
383+
stdout, _ = run_command(["ip", "--json", "address", "show", "dev", "eth0"])
384+
command_json = json.loads(stdout.decode('utf-8'))
385+
intf_addrinfos = command_json[0]['addr_info']
386+
387+
mgmt_cidr_v4 = None
388+
mgmt_cidr_v6 = None
389+
for addrinfo in intf_addrinfos:
390+
if addrinfo['family'] == 'inet' and addrinfo['scope'] == 'global':
391+
mgmt_address_v4 = addrinfo['local']
392+
mgmt_prefixlen_v4 = addrinfo['prefixlen']
393+
mgmt_cidr_v4 = mgmt_address_v4 + '/' + str(mgmt_prefixlen_v4)
394+
if addrinfo['family'] == 'inet6' and addrinfo['scope'] == 'global':
395+
mgmt_address_v6 = addrinfo['local']
396+
mgmt_prefixlen_v6 = addrinfo['prefixlen']
397+
mgmt_cidr_v6 = mgmt_address_v6 + '/' + str(mgmt_prefixlen_v6)
398+
399+
if not mgmt_cidr_v4:
400+
raise ValueError("No IPv4 address set on management interface eth0!")
401+
402+
return mgmt_cidr_v4, mgmt_cidr_v6
403+
404+
def get_mgmt_gw(self):
405+
""" Returns the IPv4 and IPv6 default gateways of the container, used for generating the management default route"""
406+
stdout_v4, _ = run_command(["ip", "--json", "-4", "route", "show", "default"])
407+
command_json_v4 = json.loads(stdout_v4.decode('utf-8'))
408+
try:
409+
mgmt_gw_v4 = command_json_v4[0]['gateway']
410+
except IndexError as e:
411+
raise IndexError("No default gateway route on management interface eth0!") from e
412+
413+
stdout_v6, _ = run_command(["ip", "--json", "-6", "route", "show", "default"])
414+
command_json_v6 = json.loads(stdout_v6.decode('utf-8'))
415+
try:
416+
mgmt_gw_v6 = command_json_v6[0]['gateway']
417+
except IndexError:
418+
mgmt_gw_v6 = None
419+
420+
return mgmt_gw_v4, mgmt_gw_v6
421+
324422
def nic_provision_delay(self) -> None:
325423
self.logger.debug(
326424
f"number of provisioned data plane interfaces is {self.num_provisioned_nics}"

vjunosevolved/docker/init.conf

+10-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ interfaces {
2525
re0:mgmt-0 {
2626
unit 0 {
2727
family inet {
28-
address 10.0.0.15/24;
28+
address {MGMT_IP_IPV4};
29+
}
30+
family inet {
31+
address {MGMT_IP_IPV6};
2932
}
3033
}
3134
}
@@ -34,7 +37,12 @@ routing-instances {
3437
mgmt_junos {
3538
routing-options {
3639
static {
37-
route 0.0.0.0/0 next-hop 10.0.0.2;
40+
route 0.0.0.0/0 next-hop {MGMT_GW_IPV4};
41+
}
42+
rib mgmt_junos.inet6.0 {
43+
static {
44+
route ::/0 next-hop {MGMT_GW_IPV6};
45+
}
3846
}
3947
}
4048
}

vjunosevolved/docker/launch.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(self, hostname, username, password, conn_mode):
5555
driveif="virtio",
5656
cpu="IvyBridge,vme=on,ss=on,vmx=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,arch-capabilities=on,pdpe1gb=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,bmi1=off,avx2=off,bmi2=off,erms=off,invpcid=off,rdseed=off,adx=off,smap=off,xsaveopt=off,abm=off,svm=off",
5757
smp="4,sockets=1,cores=4,threads=1",
58+
mgmt_passthrough=True
5859
)
5960

6061
# device hostname
@@ -67,16 +68,18 @@ def __init__(self, hostname, username, password, conn_mode):
6768
with open("init.conf", "r") as file:
6869
cfg = file.read()
6970

70-
# replace HOSTNAME file var with nodes given hostname
71+
cfg = cfg.replace("{MGMT_IP_IPV4}", self.mgmt_address_ipv4)
72+
cfg = cfg.replace("{MGMT_GW_IPV4}", self.mgmt_gw_ipv4)
73+
cfg = cfg.replace("{MGMT_IP_IPV6}", self.mgmt_address_ipv6)
74+
cfg = cfg.replace("{MGMT_GW_IPV6}", self.mgmt_gw_ipv6)
75+
cfg = cfg.replace("{HOSTNAME}", self.hostname)
7176
# replace CRYPT_PSWD file var with nodes given password
7277
# (Evo does not accept plaintext passwords in config)
73-
new_cfg = cfg.replace("{HOSTNAME}", hostname).replace(
74-
"{CRYPT_PSWD}", password_hash
75-
)
78+
cfg = cfg.replace("{CRYPT_PSWD}", password_hash)
7679

7780
# write changes to init.conf file
7881
with open("init.conf", "w") as file:
79-
file.write(new_cfg)
82+
file.write(cfg)
8083

8184
# pass in user startup config
8285
self.startup_config()

vjunosrouter/docker/init.conf

+10-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ interfaces {
2525
fxp0 {
2626
unit 0 {
2727
family inet {
28-
address 10.0.0.15/24;
28+
address {MGMT_IP_IPV4};
29+
}
30+
family inet6 {
31+
address {MGMT_IP_IPV6};
2932
}
3033
}
3134
}
@@ -34,7 +37,12 @@ routing-instances {
3437
mgmt_junos {
3538
routing-options {
3639
static {
37-
route 0.0.0.0/0 next-hop 10.0.0.2;
40+
route 0.0.0.0/0 next-hop {MGMT_GW_IPV4};
41+
}
42+
rib mgmt_junos.inet6.0 {
43+
static {
44+
route ::/0 next-hop {MGMT_GW_IPV6};
45+
}
3846
}
3947
}
4048
}

vjunosrouter/docker/launch.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(self, hostname, username, password, conn_mode):
5252
driveif="virtio",
5353
cpu="IvyBridge,vme=on,ss=on,vmx=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,arch-capabilities=on,pdpe1gb=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,bmi1=off,avx2=off,bmi2=off,erms=off,invpcid=off,rdseed=off,adx=off,smap=off,xsaveopt=off,abm=off,svm=on",
5454
smp="4,sockets=1,cores=4,threads=1",
55+
mgmt_passthrough=True
5556
)
5657
# device hostname
5758
self.hostname = hostname
@@ -61,12 +62,15 @@ def __init__(self, hostname, username, password, conn_mode):
6162
with open("init.conf", "r") as file:
6263
cfg = file.read()
6364

64-
# replace HOSTNAME file var with nodes given hostname
65-
new_cfg = cfg.replace("{HOSTNAME}", hostname)
65+
cfg = cfg.replace("{MGMT_IP_IPV4}", self.mgmt_address_ipv4)
66+
cfg = cfg.replace("{MGMT_GW_IPV4}", self.mgmt_gw_ipv4)
67+
cfg = cfg.replace("{MGMT_IP_IPV6}", self.mgmt_address_ipv6)
68+
cfg = cfg.replace("{MGMT_GW_IPV6}", self.mgmt_gw_ipv6)
69+
cfg = cfg.replace("{HOSTNAME}", self.hostname)
6670

6771
# write changes to init.conf file
6872
with open("init.conf", "w") as file:
69-
file.write(new_cfg)
73+
file.write(cfg)
7074

7175
# pass in user startup config
7276
self.startup_config()

vjunosswitch/docker/init.conf

+10-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ interfaces {
2525
fxp0 {
2626
unit 0 {
2727
family inet {
28-
address 10.0.0.15/24;
28+
address {MGMT_IP_IPV4};
29+
}
30+
family inet6 {
31+
address {MGMT_IP_IPV6};
2932
}
3033
}
3134
}
@@ -34,7 +37,12 @@ routing-instances {
3437
mgmt_junos {
3538
routing-options {
3639
static {
37-
route 0.0.0.0/0 next-hop 10.0.0.2;
40+
route 0.0.0.0/0 next-hop {MGMT_GW_IPV4};
41+
}
42+
rib mgmt_junos.inet6.0 {
43+
static {
44+
route ::/0 next-hop {MGMT_GW_IPV6};
45+
}
3846
}
3947
}
4048
}

vjunosswitch/docker/launch.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(self, hostname, username, password, conn_mode):
5252
driveif="virtio",
5353
cpu="IvyBridge,vme=on,ss=on,vmx=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,arch-capabilities=on,pdpe1gb=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,bmi1=off,avx2=off,bmi2=off,erms=off,invpcid=off,rdseed=off,adx=off,smap=off,xsaveopt=off,abm=off,svm=on",
5454
smp="4,sockets=1,cores=4,threads=1",
55+
mgmt_passthrough=True
5556
)
5657
# device hostname
5758
self.hostname = hostname
@@ -61,12 +62,15 @@ def __init__(self, hostname, username, password, conn_mode):
6162
with open("init.conf", "r") as file:
6263
cfg = file.read()
6364

64-
# replace HOSTNAME file var with nodes given hostname
65-
new_cfg = cfg.replace("{HOSTNAME}", hostname)
65+
cfg = cfg.replace("{MGMT_IP_IPV4}", self.mgmt_address_ipv4)
66+
cfg = cfg.replace("{MGMT_GW_IPV4}", self.mgmt_gw_ipv4)
67+
cfg = cfg.replace("{MGMT_IP_IPV6}", self.mgmt_address_ipv6)
68+
cfg = cfg.replace("{MGMT_GW_IPV6}", self.mgmt_gw_ipv6)
69+
cfg = cfg.replace("{HOSTNAME}", self.hostname)
6670

6771
# write changes to init.conf file
6872
with open("init.conf", "w") as file:
69-
file.write(new_cfg)
73+
file.write(cfg)
7074

7175
# pass in user startup config
7276
self.startup_config()

0 commit comments

Comments
 (0)