diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index 2857e7eab..aab311339 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -422,6 +422,12 @@ Match devices by MAC when setting options like: `wakeonlan` or `*-offload`. > In addition to the addresses themselves one can specify configuration > parameters as mappings. Current supported options are: + - **`duplicate-address-detection`** (scalar) + + > Configure the duplicate address detection (DAD). Valid options + > are `ipv4`, `ipv6`, `both` or `none`. Currently supported on the + > networkd back end only. + - **`lifetime`** (scalar) – since 0.100 > Default: `forever`. This can be `forever` or `0` and corresponds @@ -445,6 +451,8 @@ Match devices by MAC when setting options like: `wakeonlan` or `*-offload`. - "10.0.0.15/24": lifetime: 0 label: "maas" + - "169.254.10.1/24": + duplicate-address-detection: "none" - "2001:1::1/64" ``` diff --git a/python-cffi/netplan/_build_cffi.py b/python-cffi/netplan/_build_cffi.py index 17cc3cefe..3941d482a 100644 --- a/python-cffi/netplan/_build_cffi.py +++ b/python-cffi/netplan/_build_cffi.py @@ -40,6 +40,7 @@ char* address; char* lifetime; char* label; + char* duplicate_address_detection; } NetplanAddressOptions; struct address_iter { ...; }; struct nameserver_iter { ...; }; diff --git a/python-cffi/netplan/netdef.py b/python-cffi/netplan/netdef.py index d496c0902..ffe6a2036 100644 --- a/python-cffi/netplan/netdef.py +++ b/python-cffi/netplan/netdef.py @@ -211,10 +211,11 @@ def __next__(self): class NetplanAddress: - def __init__(self, address: str, lifetime: str, label: str): + def __init__(self, address: str, lifetime: str, label: str, duplicate_address_detection: str): self.address = address self.lifetime = lifetime self.label = label + self.duplicate_address_detection = duplicate_address_detection def __str__(self) -> str: return self.address @@ -241,7 +242,9 @@ def __next__(self): address = ffi.string(content.address).decode('utf-8') if content.address else None lifetime = ffi.string(content.lifetime).decode('utf-8') if content.lifetime else None label = ffi.string(content.label).decode('utf-8') if content.label else None - return NetplanAddress(address, lifetime, label) + duplicate_address_detection = ffi.string(content.duplicate_address_detection).decode('utf-8') \ + if content.duplicate_address_detection else None + return NetplanAddress(address, lifetime, label, duplicate_address_detection) class _NetdefNameserverIterator: diff --git a/src/netplan.c b/src/netplan.c index d6ab84902..c1e4e92c0 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -420,6 +420,7 @@ write_addresses(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDe YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING(event, emitter, "label", opts->label); YAML_NONNULL_STRING(event, emitter, "lifetime", opts->lifetime); + YAML_NONNULL_STRING(event, emitter, "duplicate-address-detection", opts->duplicate_address_detection); YAML_MAPPING_CLOSE(event, emitter); YAML_MAPPING_CLOSE(event, emitter); } diff --git a/src/networkd.c b/src/networkd.c index aed0273f6..e2470ca20 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -767,6 +767,8 @@ write_addr_option(NetplanAddressOptions* o, GString* s) g_assert(o->address != NULL); g_string_append_printf(s, "Address=%s\n", o->address); + if (o->duplicate_address_detection) + g_string_append_printf(s, "DuplicateAddressDetection=%s\n", o->duplicate_address_detection); if (o->lifetime) g_string_append_printf(s, "PreferredLifetime=%s\n", o->lifetime); if (o->label) diff --git a/src/parse.c b/src/parse.c index 6500c7a9c..17368b460 100644 --- a/src/parse.c +++ b/src/parse.c @@ -1393,9 +1393,22 @@ handle_address_option_label(NetplanParser* npp, yaml_node_t* node, const void* d return handle_generic_str(npp, node, npp->current.addr_options, data, error); } +STATIC gboolean +handle_address_option_duplicate_address_detection(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) +{ + if (g_ascii_strcasecmp(scalar(node), "ipv4") != 0 && + g_ascii_strcasecmp(scalar(node), "ipv6") != 0 && + g_ascii_strcasecmp(scalar(node), "both") != 0 && + g_ascii_strcasecmp(scalar(node), "none") != 0) { + return yaml_error(npp, node, error, "invalid duplicate-address-detection value '%s'", scalar(node)); + } + return handle_generic_str(npp, node, npp->current.addr_options, data, error); +} + const mapping_entry_handler address_option_handlers[] = { {"lifetime", YAML_SCALAR_NODE, {.generic=handle_address_option_lifetime}, addr_option_offset(lifetime)}, {"label", YAML_SCALAR_NODE, {.generic=handle_address_option_label}, addr_option_offset(label)}, + {"duplicate-address-detection", YAML_SCALAR_NODE, {.generic=handle_address_option_duplicate_address_detection}, addr_option_offset(duplicate_address_detection)}, {NULL} }; diff --git a/src/types-internal.h b/src/types-internal.h index f8c1df3df..35d19fac4 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -97,6 +97,7 @@ typedef struct { char* address; char* lifetime; char* label; + char* duplicate_address_detection; } NetplanAddressOptions; struct address_iter { diff --git a/src/types.c b/src/types.c index 7a1c20ed2..904317b31 100644 --- a/src/types.c +++ b/src/types.c @@ -66,6 +66,7 @@ free_address_options(void* ptr) g_free(opts->address); g_free(opts->label); g_free(opts->lifetime); + g_free(opts->duplicate_address_detection); g_free(opts); } diff --git a/src/util.c b/src/util.c index 294a2f457..57f53eed7 100644 --- a/src/util.c +++ b/src/util.c @@ -802,6 +802,7 @@ _netplan_address_iter_next(struct address_iter* it) options->address = g_strdup(netdef_options->address); options->lifetime = g_strdup(netdef_options->lifetime); options->label = g_strdup(netdef_options->label); + options->duplicate_address_detection = g_strdup(netdef_options->duplicate_address_detection); it->last_address = options; return options; } diff --git a/tests/config_fuzzer/schemas/common.js b/tests/config_fuzzer/schemas/common.js index 9ae0ebbfa..f775a1586 100644 --- a/tests/config_fuzzer/schemas/common.js +++ b/tests/config_fuzzer/schemas/common.js @@ -147,6 +147,11 @@ export const common_properties = { label: { type: "string", maxLength: 15, + }, + "duplicate-address-detection": { + { + type: "string", + enum: ["ipv4", "ipv6", "both", "none"], } } } diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py index 9521c9f2a..680182230 100644 --- a/tests/generator/test_common.py +++ b/tests/generator/test_common.py @@ -392,6 +392,34 @@ def test_eth_address_option_label(self): [Address] Address=192.168.14.2/24 Label=test-label +'''}) + + def test_eth_address_option_duplicate_address_detection(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.2/24: + duplicate-address-detection: ipv4 + - 2001:FFfe::1/64: + duplicate-address-detection: none + - 10.0.0.1/24''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=10.0.0.1/24 + +[Address] +Address=192.168.14.2/24 +DuplicateAddressDetection=ipv4 + +[Address] +Address=2001:FFfe::1/64 +DuplicateAddressDetection=none '''}) def test_eth_address_option_multi_pass(self): diff --git a/tests/generator/test_errors.py b/tests/generator/test_errors.py index d447c4920..2bd8177fd 100644 --- a/tests/generator/test_errors.py +++ b/tests/generator/test_errors.py @@ -408,6 +408,16 @@ def test_invalid_address_option_lifetime(self): lifetime: 1''', expect_fail=True) self.assertIn("invalid lifetime value '1'", err) + def test_invalid_address_option_duplicate_address_detection(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.1.15/24: + duplicate-address-detection: a''', expect_fail=True) + self.assertIn("invalid duplicate-address-detection value 'a'", err) + def test_invalid_nm_options(self): err = self.generate('''network: version: 2 diff --git a/tests/test_libnetplan.py b/tests/test_libnetplan.py index 33a07f476..93859e4e8 100644 --- a/tests/test_libnetplan.py +++ b/tests/test_libnetplan.py @@ -200,17 +200,23 @@ def test_iter_ethernets_with_options(self): - 172.16.0.1/24: lifetime: 0 label: label1 + duplicate-address-detection: none - 1234:4321:abcd::cdef/96: lifetime: forever - label: label2''') + label: label2 + duplicate-address-detection: both''') expected_ips = set(["1234:4321:abcd::cdef/96", "192.168.0.1/24", "172.16.0.1/24"]) expected_lifetime_options = set([None, "0", "forever"]) expected_label_options = set([None, "label1", "label2"]) + expected_duplicate_address_detection_options = set([None, "none", "both"]) netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertSetEqual(expected_ips, set(ip.address for ip in netdef.addresses)) self.assertSetEqual(expected_lifetime_options, set(ip.lifetime for ip in netdef.addresses)) self.assertSetEqual(expected_label_options, set(ip.label for ip in netdef.addresses)) + self.assertSetEqual( + expected_duplicate_address_detection_options, set(ip.duplicate_address_detection for ip in netdef.addresses) + ) def test_drop_iterator_before_finishing(self): state = state_from_yaml(self.confdir, '''network: