Skip to content
Open
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
18 changes: 14 additions & 4 deletions doc/netplan-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,21 @@ Match devices by MAC when setting options like: `wakeonlan` or `*-offload`.
> Stateless Address Auto-configuration.
> Possible values are `eui64` or `stable-privacy`.

- **`ipv6-address-token`** (scalar) – since 0.100
- **`ipv6-address-token`** (scalar or sequence) – since 0.100

> Define an IPv6 address token for creating a static interface identifier for
> IPv6 Stateless Address Auto-configuration. This is mutually exclusive with
> `ipv6-address-generation`.
> Define one or more IPv6 address tokens for creating static interface
> identifiers for IPv6 Stateless Address Auto-configuration. This is mutually
> exclusive with `ipv6-address-generation`.
>
> Multiple tokens (as a sequence) are supported since 1.3. When using
> systemd-networkd as the back end renderer, multiple tokens can be specified
> to configure multiple IPv6 addresses. NetworkManager only supports a single
> token; if multiple tokens are specified, only the first will be used.

Examples:

- Single token: `ipv6-address-token: ::42`
- Multiple tokens: `ipv6-address-token: [::31, ::32, ::33]`

- **`gateway4`**, **`gateway6`** (scalar)

Expand Down
5 changes: 4 additions & 1 deletion src/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ struct netplan_net_definition {
GArray* address_options;
gboolean ip6_privacy;
guint ip6_addr_gen_mode;
char* ip6_addr_gen_token;
char* ip6_addr_gen_token; /* deprecated: use ip6_addr_gen_tokens for multiple tokens */
char* gateway4;
char* gateway6;
GArray* ip4_nameservers;
Expand Down Expand Up @@ -431,4 +431,7 @@ struct netplan_net_definition {
NetplanTristate bridge_learning;

NetplanRAOverrides ra_overrides;

/* netplan-feature: ipv6-address-token-array */
GArray* ip6_addr_gen_tokens; /* array of char* - added for multiple token support */
};
23 changes: 22 additions & 1 deletion src/netplan.c
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,28 @@ _serialize_yaml(
YAML_STRING(def, event, emitter, "macaddress", def->set_mac);
YAML_STRING(def, event, emitter, "set-name", def->set_name);
YAML_NONNULL_STRING(event, emitter, "ipv6-address-generation", netplan_addr_gen_mode_name(def->ip6_addr_gen_mode));
YAML_STRING(def, event, emitter, "ipv6-address-token", def->ip6_addr_gen_token);
/* Serialize ipv6-address-token as array if multiple tokens, scalar if single */
if (def->ip6_addr_gen_tokens && def->ip6_addr_gen_tokens->len > 0) {
if (def->ip6_addr_gen_tokens->len == 1) {
/* Single token - output as scalar for backward compatibility */
YAML_STRING(def, event, emitter, "ipv6-address-token", g_array_index(def->ip6_addr_gen_tokens, char*, 0));
} else {
/* Multiple tokens - output as sequence */
YAML_SCALAR_PLAIN(event, emitter, "ipv6-address-token");
YAML_SEQUENCE_OPEN(event, emitter);
for (unsigned i = 0; i < def->ip6_addr_gen_tokens->len; ++i)
YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->ip6_addr_gen_tokens, char*, i));
YAML_SEQUENCE_CLOSE(event, emitter);
}
} else if (def->ip6_addr_gen_token) {
/* Fallback to old field for backward compatibility.
* This path is only hit when code directly sets ip6_addr_gen_token via C API
* without going through the YAML parser (which always sets both fields).
* Excluded from coverage as it's defensive code for ABI compatibility. */
/* LCOV_EXCL_START */
YAML_STRING(def, event, emitter, "ipv6-address-token", def->ip6_addr_gen_token);
/* LCOV_EXCL_STOP */
}
YAML_BOOL_TRUE(def, event, emitter, "ipv6-privacy", def->ip6_privacy);
YAML_UINT_0(def, event, emitter, "ipv6-mtu", def->ipv6_mtubytes);
YAML_UINT_0(def, event, emitter, "mtu", def->mtubytes);
Expand Down
12 changes: 11 additions & 1 deletion src/networkd.c
Original file line number Diff line number Diff line change
Expand Up @@ -799,8 +799,18 @@ _netplan_netdef_write_network_file(
if (def->ip6_addresses)
for (unsigned i = 0; i < def->ip6_addresses->len; ++i)
g_string_append_printf(network, "Address=%s\n", g_array_index(def->ip6_addresses, char*, i));
if (def->ip6_addr_gen_token) {
/* Handle multiple IPv6 tokens */
if (def->ip6_addr_gen_tokens && def->ip6_addr_gen_tokens->len > 0) {
for (unsigned i = 0; i < def->ip6_addr_gen_tokens->len; ++i)
g_string_append_printf(network, "IPv6Token=static:%s\n", g_array_index(def->ip6_addr_gen_tokens, char*, i));
} else if (def->ip6_addr_gen_token) {
/* Fallback to old single token field for backward compatibility.
* This path is only hit when code directly sets ip6_addr_gen_token via C API
* without going through the YAML parser (which always sets both fields).
* Excluded from coverage as it's defensive code for ABI compatibility. */
/* LCOV_EXCL_START */
g_string_append_printf(network, "IPv6Token=static:%s\n", def->ip6_addr_gen_token);
/* LCOV_EXCL_STOP */
} else {
switch (def->ip6_addr_gen_mode) {
/* EUI-64 mode is enabled by default, if no IPv6Token= is specified */
Expand Down
14 changes: 13 additions & 1 deletion src/nm.c
Original file line number Diff line number Diff line change
Expand Up @@ -954,10 +954,22 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
g_free(tmp_key);
}
}
if (def->ip6_addr_gen_token) {
/* NetworkManager only supports a single IPv6 token, use first from array */
if (def->ip6_addr_gen_tokens && def->ip6_addr_gen_tokens->len > 0) {
if (def->ip6_addr_gen_tokens->len > 1)
g_warning("%s: NetworkManager does not support multiple IPv6 address tokens, only the first one will be used\n", def->id);
/* Token implies EUI-64, i.e mode=0 */
g_key_file_set_integer(kf, "ipv6", "addr-gen-mode", 0);
g_key_file_set_string(kf, "ipv6", "token", g_array_index(def->ip6_addr_gen_tokens, char*, 0));
} else if (def->ip6_addr_gen_token) {
/* Fallback to old single token field for backward compatibility.
* This path is only hit when code directly sets ip6_addr_gen_token via C API
* without going through the YAML parser (which always sets both fields).
* Excluded from coverage as it's defensive code for ABI compatibility. */
/* LCOV_EXCL_START */
g_key_file_set_integer(kf, "ipv6", "addr-gen-mode", 0);
g_key_file_set_string(kf, "ipv6", "token", def->ip6_addr_gen_token);
/* LCOV_EXCL_STOP */
} else if (def->ip6_addr_gen_mode)
g_key_file_set_string(kf, "ipv6", "addr-gen-mode", addr_gen_mode_str(def->ip6_addr_gen_mode));
if (def->ip6_privacy)
Expand Down
8 changes: 8 additions & 0 deletions src/parse-nm.c
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,15 @@ netplan_parser_load_keyfile(NetplanParser* npp, const char* filename, GError** e
}
}
g_free(tmp_str);
/* NM only supports a single token, store in both old and new fields */
keyfile_handle_generic_str(kf, "ipv6", "token", &nd->ip6_addr_gen_token);
if (nd->ip6_addr_gen_token) {
if (!nd->ip6_addr_gen_tokens)
nd->ip6_addr_gen_tokens = g_array_new(FALSE, FALSE, sizeof(char*));
/* Duplicate to avoid double-free */
char* s = g_strdup(nd->ip6_addr_gen_token);
g_array_append_val(nd->ip6_addr_gen_tokens, s);
}

/* ip6-privacy is not fully supported, NM supports additional modes, like -1 or 1
* handle known modes, but keep any unsupported "ip6-privacy" value in passthrough */
Expand Down
63 changes: 57 additions & 6 deletions src/parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -809,13 +809,63 @@ handle_netdef_addrgen(NetplanParser* npp, yaml_node_t* node, __unused const void
}

STATIC gboolean
handle_netdef_addrtok(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error)
handle_netdef_addrtok(NetplanParser* npp, yaml_node_t* node, __unused const char* prefix, const void* data, GError** error)
{
g_assert(npp->current.netdef);
gboolean ret = handle_netdef_str(npp, node, data, error);
if (!is_ip6_address(npp->current.netdef->ip6_addr_gen_token))
return yaml_error(npp, node, error, "invalid ipv6-address-token '%s'", scalar(node));
return ret;

/* Handle both scalar (single token) and sequence (multiple tokens) */
if (node->type == YAML_SCALAR_NODE) {
/* Single token - use original handler then validate */
gboolean ret = handle_netdef_str(npp, node, data, error);
if (!ret)
return FALSE; // LCOV_EXCL_LINE - handle_netdef_str only fails on catastrophic errors

if (!is_ip6_address(npp->current.netdef->ip6_addr_gen_token))
return yaml_error(npp, node, error, "invalid ipv6-address-token '%s'", scalar(node));

/* Also store in new array field (duplicate to avoid double-free) */
if (!npp->current.netdef->ip6_addr_gen_tokens)
npp->current.netdef->ip6_addr_gen_tokens = g_array_new(FALSE, FALSE, sizeof(char*));
char* s = g_strdup(npp->current.netdef->ip6_addr_gen_token);
g_array_append_val(npp->current.netdef->ip6_addr_gen_tokens, s);
mark_data_as_dirty(npp, &npp->current.netdef->ip6_addr_gen_tokens);

return TRUE;
} else if (node->type == YAML_SEQUENCE_NODE) {
/* Multiple tokens - check not empty first */
if (node->data.sequence.items.start >= node->data.sequence.items.top)
return yaml_error(npp, node, error, "ipv6-address-token must not be empty");

/* Validate all tokens first before storing any */
for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i);
assert_type(npp, entry, YAML_SCALAR_NODE);

if (!is_ip6_address(scalar(entry)))
return yaml_error(npp, entry, error, "invalid ipv6-address-token '%s'", scalar(entry));
}

/* All tokens are valid, now store them */
if (!npp->current.netdef->ip6_addr_gen_tokens)
npp->current.netdef->ip6_addr_gen_tokens = g_array_new(FALSE, FALSE, sizeof(char*));

for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i);
char* s = g_strdup(scalar(entry));
g_array_append_val(npp->current.netdef->ip6_addr_gen_tokens, s);
}

/* For backward compatibility, store first token in old field (duplicate to avoid double-free) */
if (npp->current.netdef->ip6_addr_gen_tokens->len > 0) {
npp->current.netdef->ip6_addr_gen_token = g_strdup(g_array_index(npp->current.netdef->ip6_addr_gen_tokens, char*, 0));
mark_data_as_dirty(npp, &npp->current.netdef->ip6_addr_gen_token);
}

mark_data_as_dirty(npp, &npp->current.netdef->ip6_addr_gen_tokens);
return TRUE;
}

return yaml_error(npp, node, error, "invalid type for ipv6-address-token: must be a scalar or sequence");
}

STATIC gboolean
Expand Down Expand Up @@ -2980,7 +3030,8 @@ static const mapping_entry_handler ra_overrides_handlers[] = {
{"gateway4", YAML_SCALAR_NODE, {.generic=handle_gateway4}, NULL}, \
{"gateway6", YAML_SCALAR_NODE, {.generic=handle_gateway6}, NULL}, \
{"ipv6-address-generation", YAML_SCALAR_NODE, {.generic=handle_netdef_addrgen}, NULL}, \
{"ipv6-address-token", YAML_SCALAR_NODE, {.generic=handle_netdef_addrtok}, netdef_offset(ip6_addr_gen_token)}, \
/* Type 0 (YAML_NO_NODE) accepts both scalar and sequence; .variable handler gets prefix parameter */ \
{"ipv6-address-token", 0, {.variable=handle_netdef_addrtok}, netdef_offset(ip6_addr_gen_token)}, \
{"ipv6-mtu", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(ipv6_mtubytes)}, \
{"ipv6-privacy", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(ip6_privacy)}, \
{"link-local", YAML_SEQUENCE_NODE, {.generic=handle_link_local}, NULL}, \
Expand Down
1 change: 1 addition & 0 deletions src/types.c
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ reset_netdef(NetplanNetDefinition* netdef, NetplanDefType new_type, NetplanBacke
netdef->ip6_privacy = FALSE;
netdef->ip6_addr_gen_mode = NETPLAN_ADDRGEN_DEFAULT;
FREE_AND_NULLIFY(netdef->ip6_addr_gen_token);
free_garray_with_destructor(&netdef->ip6_addr_gen_tokens, g_free);

FREE_AND_NULLIFY(netdef->gateway4);
FREE_AND_NULLIFY(netdef->gateway6);
Expand Down
3 changes: 2 additions & 1 deletion src/validation.c
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,8 @@ validate_netdef_grammar(const NetplanParser* npp, NetplanNetDefinition* nd, GErr
}
}

if (nd->ip6_addr_gen_mode != NETPLAN_ADDRGEN_DEFAULT && nd->ip6_addr_gen_token) {
if (nd->ip6_addr_gen_mode != NETPLAN_ADDRGEN_DEFAULT &&
(nd->ip6_addr_gen_token || (nd->ip6_addr_gen_tokens && nd->ip6_addr_gen_tokens->len > 0))) {
return yaml_error(npp, NULL, error, "%s: ipv6-address-generation and ipv6-address-token are mutually exclusive", nd->id);
}

Expand Down
78 changes: 78 additions & 0 deletions tests/generator/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,54 @@ def test_ip6_addr_gen_token(self):
LinkLocalAddressing=ipv6
IPv6Token=static:::2

[DHCP]
RouteMetric=100
UseMTU=true
'''})

def test_dhcp6_token_multiple(self):
self.generate('''network:
version: 2
renderer: networkd
ethernets:
engreen:
dhcp6: yes
ipv6-address-token:
- ::31
- ::32
- ::33''')
self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6Token=static:::31
IPv6Token=static:::32
IPv6Token=static:::33

[DHCP]
RouteMetric=100
UseMTU=true
'''})

def test_dhcp6_token_single_element_array(self):
self.generate('''network:
version: 2
renderer: networkd
ethernets:
engreen:
dhcp6: yes
ipv6-address-token:
- ::42''', skip_generated_yaml_validation=True)
self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6Token=static:::42

[DHCP]
RouteMetric=100
UseMTU=true
Expand Down Expand Up @@ -1428,6 +1476,36 @@ def test_nm_file_paths_escaped(self):
method=ignore
'''})

def test_dhcp6_token_multiple_nm_uses_first(self):
out = self.generate('''network:
version: 2
renderer: NetworkManager
ethernets:
engreen:
dhcp6: yes
ipv6-address-token:
- ::31
- ::32
- ::33''')
self.assertIn('engreen: NetworkManager does not support multiple IPv6 address tokens', out)
self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=auto
addr-gen-mode=0
token=::31
ip6-privacy=0
'''})


class TestForwardDeclaration(TestBase):

Expand Down
44 changes: 44 additions & 0 deletions tests/generator/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,18 @@ def test_addr_gen_mode_and_addr_gen_token(self):
ipv6-address-generation: eui64''', expect_fail=True)
self.assertIn("engreen: ipv6-address-generation and ipv6-address-token are mutually exclusive", err)

def test_addr_gen_mode_and_addr_gen_token_array(self):
err = self.generate('''network:
version: 2
renderer: networkd
ethernets:
engreen:
ipv6-address-token:
- ::31
- ::32
ipv6-address-generation: stable-privacy''', expect_fail=True)
self.assertIn("engreen: ipv6-address-generation and ipv6-address-token are mutually exclusive", err)

def test_invalid_addr_gen_token(self):
err = self.generate('''network:
version: 2
Expand All @@ -369,6 +381,38 @@ def test_invalid_addr_gen_token(self):
ipv6-address-token: INVALID''', expect_fail=True)
self.assertIn("invalid ipv6-address-token 'INVALID'", err)

def test_invalid_addr_gen_token_in_array(self):
err = self.generate('''network:
version: 2
renderer: networkd
ethernets:
engreen:
ipv6-address-token:
- ::31
- INVALID
- ::33''', expect_fail=True)
self.assertIn("invalid ipv6-address-token 'INVALID'", err)

def test_empty_addr_gen_token_array(self):
err = self.generate('''network:
version: 2
renderer: networkd
ethernets:
engreen:
dhcp6: true
ipv6-address-token: []''', expect_fail=True)
self.assertIn("ipv6-address-token must not be empty", err)

def test_invalid_type_addr_gen_token(self):
err = self.generate('''network:
version: 2
renderer: networkd
ethernets:
engreen:
dhcp6: true
ipv6-address-token: {foo: bar}''', expect_fail=True)
self.assertIn("invalid type for ipv6-address-token: must be a scalar or sequence", err)

def test_nm_devices_missing_passthrough(self):
err = self.generate('''network:
version: 2
Expand Down
Loading
Loading