Skip to content

Commit c21e337

Browse files
committed
container: T7736: give container veths a deterministic host_interface_name
Podman's default "vethN" auto-naming for a container's host-side veth can collide with VyOS's own "virtual-ethernet vethN" interfaces. Bump the minimum Podman dependency to 5.8 (which supports "host_interface_name" network connect option) and use it to name every non-macvlan container network attachment "veth-<container name>" instead, eliminating the collision by construction.
1 parent 3a1ce3a commit c21e337

3 files changed

Lines changed: 82 additions & 5 deletions

File tree

debian/control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ Depends:
332332
kbd,
333333
# End "system option keyboard-layout"
334334
# For "container"
335-
podman (>=4.9.5),
335+
podman (>=5.8),
336336
netavark,
337337
aardvark-dns,
338338
# iptables is only used for containers now, not the the firewall CLI

smoketest/scripts/cli/test_container.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,33 @@ def test_user_defined_mac(self):
264264
n = cmd_to_json(['container', 'inspect', 'test2'])
265265
self.assertEqual(n['NetworkSettings']['Networks']['bridge1']['MacAddress'], '02:00:00:00:00:02')
266266

267+
def test_long_name_host_interface_uniqueness(self):
268+
# T7736: the deterministic host-side veth interface name derived
269+
# from a container name is truncated to fit IFNAMSIZ. Two distinct
270+
# but similarly-prefixed long names must not truncate to the same
271+
# interface name - Podman would then refuse to attach the second
272+
# container's network, and its systemd unit would fail to start.
273+
net_name = 'longiftest'
274+
prefix = '192.0.2.0/24'
275+
name_1 = 'abcdefghij-1'
276+
name_2 = 'abcdefghij-2'
277+
278+
self.cli_set(base_path + ['network', net_name, 'prefix', prefix])
279+
self.cli_set(base_path + ['name', name_1, 'image', busybox_image])
280+
self.cli_set(base_path + ['name', name_1, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)])
281+
self.cli_set(base_path + ['name', name_2, 'image', busybox_image])
282+
self.cli_set(base_path + ['name', name_2, 'network', net_name, 'address', str(ip_interface(prefix).ip + 3)])
283+
self.cli_commit()
284+
285+
# Both containers run a "conmon" process at once, so checking by
286+
# process name alone can't distinguish which container it belongs
287+
# to - verify each container's own recorded PID is still alive
288+
for name in (name_1, name_2):
289+
pid = 0
290+
with open(PROCESS_PIDFILE.format(name)) as f:
291+
pid = int(f.read())
292+
self.assertTrue(os.path.exists(f'/proc/{pid}'))
293+
267294
def test_ipv4_network(self):
268295
prefix = '192.0.2.0/24'
269296
base_name = 'ipv4'

src/conf_mode/container.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def verify(container):
122122
net_dict = {}
123123
net_dict['mac'] = {}
124124
net_dict['address'] = {}
125+
net_dict['host_ifname'] = {}
125126

126127
for name, container_config in container['name'].items():
127128
# Container image is a mandatory option
@@ -158,6 +159,19 @@ def verify(container):
158159
if network_name not in container.get('network', {}):
159160
raise ConfigError(f'Container network "{network_name}" does not exist!')
160161

162+
# T7736: two distinct (long) container names could truncate
163+
# to the same host_interface_name - not applicable to
164+
# macvlan networks, they attach without a paired host veth
165+
network_type = dict_search(f'{network_name}.type', container['network'])
166+
if dict_search('macvlan', network_type) is None:
167+
host_ifname = get_container_host_ifname(name)
168+
if host_ifname in net_dict['host_ifname']:
169+
raise ConfigError(
170+
f'Container "{name}" and "{net_dict["host_ifname"][host_ifname]}" '
171+
f'both generate the host interface name "{host_ifname}" - please '
172+
f'use less similar container names!')
173+
net_dict['host_ifname'][host_ifname] = name
174+
161175
if 'name_server' in container_config and 'no_name_server' not in container['network'][network_name]:
162176
raise ConfigError(f'Setting name server has no effect when attached container network has DNS enabled!')
163177

@@ -361,7 +375,28 @@ def verify(container):
361375
return None
362376

363377

364-
def generate_run_arguments(name, container_config, host_ident):
378+
def get_container_host_ifname(name: str) -> str:
379+
"""
380+
Deterministic host-side veth interface name for a container's network
381+
attachment (verify() only allows one network per container). Kept
382+
within IFNAMSIZ (15 usable characters) and - thanks to the leading
383+
"veth-" (a hyphen can never appear in a VyOS "vethN" interface name) -
384+
guaranteed to never collide with the VyOS "virtual-ethernet" naming
385+
scheme - T7736.
386+
387+
Container names are unbounded in length, so a name longer than fits is
388+
truncated - a short hash of the full name is appended so two distinct
389+
long names can never truncate to the same interface name; verify()
390+
additionally rejects any resulting duplicate.
391+
"""
392+
prefix = f'veth-{name}'
393+
if len(prefix) <= 15:
394+
return prefix
395+
digest = sha256(name.encode()).hexdigest()[:4]
396+
return f'veth-{name[:5]}-{digest}'
397+
398+
399+
def generate_run_arguments(name, container_config, host_ident, network_config):
365400
image = container_config['image']
366401
cpu_quota = container_config['cpu_quota']
367402
memory = container_config['memory']
@@ -511,9 +546,21 @@ def generate_run_arguments(name, container_config, host_ident):
511546
else:
512547
ip_param = ''
513548
addr_info = ''
514-
networks = ",".join(container_config['network'])
549+
network_opts = []
515550
for network in container_config['network']:
516551
network_name = network
552+
# T7736: give the host-side veth a name that can never collide
553+
# with a VyOS "virtual-ethernet vethN" interface, instead of
554+
# leaving it to Podman's own "vethN" auto-naming. Not applicable
555+
# to macvlan networks - they attach without a paired host veth.
556+
type_config = dict_search(f'{network}.type', network_config)
557+
is_macvlan = dict_search('macvlan', type_config) is not None
558+
net_opt = network
559+
if not is_macvlan:
560+
ifname = get_container_host_ifname(name)
561+
net_opt += f':host_interface_name={ifname}'
562+
network_opts.append(net_opt)
563+
517564
if 'address' not in container_config['network'][network]:
518565
continue
519566
for address in container_config['network'][network]['address']:
@@ -524,6 +571,8 @@ def generate_run_arguments(name, container_config, host_ident):
524571

525572
addr_info = ''.join(container_config['network'][network]['address'])
526573

574+
networks = ' '.join(f'--network {opt}' for opt in network_opts)
575+
527576
get_mac = dict_search(f'network.{network_name}.mac', container_config)
528577
if get_mac == 'auto' or get_mac is None:
529578
mac_add = gen_mac(name, addr_info, host_ident)
@@ -546,7 +595,7 @@ def generate_run_arguments(name, container_config, host_ident):
546595
delete_cli_node(mac_config_path)
547596
add_cli_node(mac_config_path, value=mac_add)
548597

549-
net = f'--net {networks} {ip_param} {mac_address}'
598+
net = f'{networks} {ip_param} {mac_address}'
550599

551600
return f'{container_base_cmd} {healthcheck} {net} {entrypoint} {image} {command} {command_arguments}'.strip()
552601

@@ -626,12 +675,13 @@ def generate(container):
626675

627676
if 'name' in container:
628677
host_ident = get_host_identity()
678+
network_config = container.get('network', {})
629679
for name, container_config in container['name'].items():
630680
if 'disable' in container_config:
631681
continue
632682

633683
file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
634-
run_args = generate_run_arguments(name, container_config, host_ident)
684+
run_args = generate_run_arguments(name, container_config, host_ident, network_config)
635685
render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args, },
636686
formatter=lambda _: _.replace("&quot;", '"').replace("&apos;", "'"))
637687

0 commit comments

Comments
 (0)