@@ -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 (""" , '"' ).replace ("'" , "'" ))
637687
0 commit comments