Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions interface-definitions/container.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,24 @@
<children>
#include <include/generic-description.xml.i>
#include <include/interface/mtu-68-16000.xml.i>
<leafNode name="gateway">
<properties>
<help>Gateway address to use for this network</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 gateway address</description>
</valueHelp>
<valueHelp>
<format>ipv6</format>
<description>IPv6 gateway address</description>
</valueHelp>
<constraint>
<validator name="ip-address"/>
<validator name="ipv6-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<leafNode name="prefix">
<properties>
<help>Prefix which allocated to that network</help>
Expand All @@ -590,6 +608,62 @@
<valueless/>
</properties>
</leafNode>
<node name="type">
<properties>
<help>Network type (default: bridge)</help>
</properties>
<children>
<leafNode name="bridge">
<properties>
<help>Bridge network</help>
<valueless/>
</properties>
</leafNode>
<node name="macvlan">
<properties>
<help>MACVLAN network</help>
</properties>
<children>
<leafNode name="mode">
<properties>
<help>MACVLAN mode</help>
<completionHelp>
<list>bridge private vepa</list>
</completionHelp>
<valueHelp>
<format>bridge</format>
<description>Containers act as separate hosts on the parent network</description>
</valueHelp>
<valueHelp>
<format>private</format>
<description>Containers are isolated from the host and each other</description>
</valueHelp>
<valueHelp>
<format>vepa</format>
<description>Containers send all traffic through the parent switch for forwarding</description>
</valueHelp>
<constraint>
<regex>bridge|private|vepa</regex>
</constraint>
<constraintErrorMessage>Invalid mode</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="parent">
<properties>
<help>Parent network interface</help>
<completionHelp>
<script>${vyos_completion_dir}/list_interfaces --type ethernet --type bonding --type bridge</script>
</completionHelp>
<constraint>
<regex>((bond|br|eth)[0-9]+(\.[0-9]+)?)</regex>
</constraint>
<constraintErrorMessage>Invalid parent interface</constraintErrorMessage>
</properties>
</leafNode>
</children>
</node>
</children>
</node>
#include <include/interface/vrf.xml.i>
</children>
</tagNode>
Expand Down
45 changes: 45 additions & 0 deletions python/vyos/utils/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.

import hashlib
from socket import AF_INET
from socket import AF_INET6
from vyos.utils.process import cmd

def _are_same_ip(one, two):
from socket import inet_pton
Expand Down Expand Up @@ -48,6 +50,49 @@ def is_netns_interface(interface, netns):
return True
return False

def get_host_identity() -> str:
"""
Build a stable host identity string for deterministic MAC generation.

Combines:
• The system's HardwareUUID (from /sys/class/dmi/id/product_uuid)
• The system hostname

Both are normalized (lowercase, dashes removed in UUID) and joined with a colon.

Returns:
str: A string "<uuid>:<hostname>", used as part of the host-specific seed when
generating deterministic MAC addresses.
"""
uuid = cmd(f"cat /sys/class/dmi/id/product_uuid").strip().replace("-", "").lower()
host = cmd("hostname").strip().lower()
Comment on lines +67 to +68
Copy link
Preview

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These commands are executed every time gen_mac() is called through get_host_identity(). Since host identity rarely changes during runtime, consider caching the result to avoid repeated subprocess calls.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, there's no need to get that data per-container. I'll make that change.

return f"{uuid}:{host}"

def gen_mac(name: str, addr: str, ident: str) -> str:
"""
Generate a deterministic locally-administered MAC address.

The MAC is derived from:
• Host identity (UUID + hostname)
• Container name
• Concatenated address string (IPv4 and/or IPv6 addresses)

A SHA-256 digest is computed from the combined string. The first 5 bytes
of the digest are used, prefixed with 0x02 to mark the address as
locally-administered and unicast.

Args:
name (str): Container name to differentiate MACs.
addr (str): Concatenated list of container addresses (IPv4/IPv6).

Returns:
str: Deterministic MAC address in standard "xx:xx:xx:xx:xx:xx" format.
"""
h = hashlib.sha256(f"{ident}:{name}:{addr}".encode()).hexdigest()
# 0x02 = locally-administered, unicast
b = [0x02] + [int(h[i:i+2], 16) for i in range(0, 10, 2)] # 5 bytes = 40 bits
Copy link
Preview

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 10 should be extracted as a constant or calculated as 5 * 2 to make the relationship between 5 bytes and 10 hex characters explicit.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 isn't a magic number, it's the number of digits I need to grab to equal 40bits. Each hex digit is 4 bits, so 10*4=40. I don't think changing this as Copilot suggests is an improvement.

return ":".join(f"{x:02x}" for x in b)

def get_netns_all() -> list:
from json import loads
from vyos.utils.process import cmd
Expand Down
58 changes: 58 additions & 0 deletions smoketest/scripts/cli/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,64 @@ def test_cpu_limit(self):
# Check for running process
self.assertEqual(process_named_running(PROCESS_NAME), pid)

def test_network_types(self):
self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '100'])
self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '101'])

# MACVLAN Networks
self.cli_set(base_path + ['network', 'macvlan1', 'prefix', '10.0.0.0/24'])
self.cli_set(base_path + ['network', 'macvlan1', 'type', 'macvlan', 'parent', 'eth0'])
self.cli_set(base_path + ['network', 'macvlan1', 'type', 'macvlan', 'mode', 'bridge'])
self.cli_set(base_path + ['network', 'macvlan2', 'prefix', '10.0.100.0/24'])
self.cli_set(base_path + ['network', 'macvlan2', 'gateway', '10.0.100.5'])
self.cli_set(base_path + ['network', 'macvlan2', 'type', 'macvlan', 'parent', 'eth0.100'])
self.cli_set(base_path + ['network', 'macvlan2', 'type', 'macvlan', 'mode', 'private'])
self.cli_set(base_path + ['network', 'macvlan3', 'prefix', '2001::/64'])
self.cli_set(base_path + ['network', 'macvlan3', 'type', 'macvlan', 'parent', 'eth0.101'])
self.cli_set(base_path + ['network', 'macvlan3', 'type', 'macvlan', 'mode', 'vepa'])

# Bridge Network
self.cli_set(base_path + ['network', 'bridge1', 'prefix', '10.0.1.0/24'])
self.cli_set(base_path + ['network', 'bridge1', 'type', 'bridge'])

# Bridge Network before T7186; default network type is bridge
self.cli_set(base_path + ['network', 'bridge2', 'prefix', '10.0.2.0/24'])

self.cli_commit()

n = cmd_to_json(f'sudo podman network inspect macvlan1')
self.assertEqual(n['driver'], 'macvlan')
self.assertEqual(n['network_interface'], 'eth0')
self.assertEqual(n['options']['mode'], 'bridge')
self.assertEqual(n['subnets'][0]['subnet'], '10.0.0.0/24')
self.assertEqual(n['subnets'][0]['gateway'], '10.0.0.1')

n = cmd_to_json(f'sudo podman network inspect macvlan2')
self.assertEqual(n['driver'], 'macvlan')
self.assertEqual(n['network_interface'], 'eth0.100')
self.assertEqual(n['options']['mode'], 'private')
self.assertEqual(n['subnets'][0]['subnet'], '10.0.100.0/24')
self.assertEqual(n['subnets'][0]['gateway'], '10.0.100.5')

n = cmd_to_json(f'sudo podman network inspect macvlan3')
self.assertEqual(n['driver'], 'macvlan')
self.assertEqual(n['network_interface'], 'eth0.101')
self.assertEqual(n['options']['mode'], 'vepa')
self.assertEqual(n['subnets'][0]['subnet'], '2001::/64')
self.assertEqual(n['subnets'][0]['gateway'], '2001::1')

n = cmd_to_json(f'sudo podman network inspect bridge1')
self.assertEqual(n['driver'], 'bridge')
self.assertEqual(n['network_interface'], 'pod-bridge1')
self.assertEqual(n['subnets'][0]['subnet'], '10.0.1.0/24')
self.assertEqual(n['subnets'][0]['gateway'], '10.0.1.1')

n = cmd_to_json(f'sudo podman network inspect bridge2')
self.assertEqual(n['driver'], 'bridge')
self.assertEqual(n['network_interface'], 'pod-bridge2')
self.assertEqual(n['subnets'][0]['subnet'], '10.0.2.0/24')
self.assertEqual(n['subnets'][0]['gateway'], '10.0.2.1')

def test_ipv4_network(self):
prefix = '192.0.2.0/24'
base_name = 'ipv4'
Expand Down
Loading
Loading