summaryrefslogtreecommitdiffstats
path: root/src/network/generator
diff options
context:
space:
mode:
Diffstat (limited to 'src/network/generator')
-rw-r--r--src/network/generator/main.c218
-rw-r--r--src/network/generator/network-generator.c1432
-rw-r--r--src/network/generator/network-generator.h116
-rw-r--r--src/network/generator/test-network-generator.c462
4 files changed, 2228 insertions, 0 deletions
diff --git a/src/network/generator/main.c b/src/network/generator/main.c
new file mode 100644
index 0000000..0439a9d
--- /dev/null
+++ b/src/network/generator/main.c
@@ -0,0 +1,218 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "build.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "generator.h"
+#include "macro.h"
+#include "main-func.h"
+#include "mkdir.h"
+#include "network-generator.h"
+#include "path-util.h"
+#include "proc-cmdline.h"
+
+#define NETWORKD_UNIT_DIRECTORY "/run/systemd/network"
+
+static const char *arg_root = NULL;
+
+static int network_save(Network *network, const char *dest_dir) {
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ assert(network);
+
+ r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path);
+ if (r < 0)
+ return r;
+
+ network_dump(network, f);
+
+ if (asprintf(&p, "%s/%s-%s.network",
+ dest_dir,
+ isempty(network->ifname) ? "71" : "70",
+ isempty(network->ifname) ? "default" : network->ifname) < 0)
+ return log_oom();
+
+ r = conservative_rename(temp_path, p);
+ if (r < 0)
+ return r;
+
+ temp_path = mfree(temp_path);
+ return 0;
+}
+
+static int netdev_save(NetDev *netdev, const char *dest_dir) {
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ assert(netdev);
+
+ r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path);
+ if (r < 0)
+ return r;
+
+ netdev_dump(netdev, f);
+
+ if (asprintf(&p, "%s/70-%s.netdev", dest_dir, netdev->ifname) < 0)
+ return log_oom();
+
+ r = conservative_rename(temp_path, p);
+ if (r < 0)
+ return r;
+
+ temp_path = mfree(temp_path);
+ return 0;
+}
+
+static int link_save(Link *link, const char *dest_dir) {
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ assert(link);
+
+ r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path);
+ if (r < 0)
+ return r;
+
+ link_dump(link, f);
+
+ if (asprintf(&p, "%s/%s-%s.link",
+ dest_dir,
+ !isempty(link->ifname) ? "70" : !hw_addr_is_null(&link->mac) ? "71" : "72",
+ link->filename) < 0)
+ return log_oom();
+
+ r = conservative_rename(temp_path, p);
+ if (r < 0)
+ return r;
+
+ temp_path = mfree(temp_path);
+ return 0;
+}
+
+static int context_save(Context *context) {
+ Network *network;
+ NetDev *netdev;
+ Link *link;
+ int r;
+
+ const char *p = prefix_roota(arg_root, NETWORKD_UNIT_DIRECTORY);
+
+ r = mkdir_p(p, 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create directory " NETWORKD_UNIT_DIRECTORY ": %m");
+
+ HASHMAP_FOREACH(network, context->networks_by_name)
+ RET_GATHER(r, network_save(network, p));
+
+ HASHMAP_FOREACH(netdev, context->netdevs_by_name)
+ RET_GATHER(r, netdev_save(netdev, p));
+
+ HASHMAP_FOREACH(link, context->links_by_filename)
+ RET_GATHER(r, link_save(link, p));
+
+ return r;
+}
+
+static int help(void) {
+ printf("%s [OPTIONS...] [-- KERNEL_CMDLINE]\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --root=PATH Operate on an alternate filesystem root\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_ROOT,
+ };
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "root", required_argument, NULL, ARG_ROOT },
+ {},
+ };
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help();
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_ROOT:
+ arg_root = optarg;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(context_clear) Context context = {};
+ int r;
+
+ log_setup();
+
+ umask(0022);
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (optind >= argc) {
+ r = proc_cmdline_parse(parse_cmdline_item, &context, 0);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse kernel command line: %m");
+ } else {
+ for (int i = optind; i < argc; i++) {
+ _cleanup_free_ char *word = NULL;
+ char *value;
+
+ word = strdup(argv[i]);
+ if (!word)
+ return log_oom();
+
+ value = strchr(word, '=');
+ if (value)
+ *(value++) = 0;
+
+ r = parse_cmdline_item(word, value, &context);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse command line \"%s%s%s\": %m",
+ word, value ? "=" : "", strempty(value));
+ }
+ }
+
+ r = context_merge_networks(&context);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to merge multiple command line options: %m");
+
+ return context_save(&context);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/network/generator/network-generator.c b/src/network/generator/network-generator.c
new file mode 100644
index 0000000..48527a2
--- /dev/null
+++ b/src/network/generator/network-generator.c
@@ -0,0 +1,1432 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "hostname-util.h"
+#include "log.h"
+#include "macro.h"
+#include "memstream-util.h"
+#include "netif-naming-scheme.h"
+#include "network-generator.h"
+#include "parse-util.h"
+#include "proc-cmdline.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+/*
+ # .network
+ ip={dhcp|on|any|dhcp6|auto6|either6|link6|link-local}
+ ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6|link-local}[:[<mtu>][:<macaddr>]]
+ ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|link6|ibft|link-local}[:[<mtu>][:<macaddr>]]
+ ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|link6|ibft|link-local}[:[<dns1>][:<dns2>]]
+ rd.route=<net>/<netmask>:<gateway>[:<interface>]
+ nameserver=<IP> [nameserver=<IP> ...]
+ rd.peerdns=0
+
+ # .link
+ ifname=<interface>:<MAC>
+ net.ifname-policy=policy1[,policy2,...][,<MAC>] # This is an original rule, not supported by other tools.
+
+ # .netdev
+ vlan=<vlanname>:<phydevice>
+ bond=<bondname>[:<bondslaves>:[:<options>[:<mtu>]]]
+ team=<teammaster>:<teamslaves> # not supported
+ bridge=<bridgename>:<ethnames>
+
+ # ignored
+ bootdev=<interface>
+ BOOTIF=<MAC>
+ rd.bootif=0
+ biosdevname=0
+ rd.neednet=1
+*/
+
+static const char * const dracut_dhcp_type_table[_DHCP_TYPE_MAX] = {
+ [DHCP_TYPE_NONE] = "none",
+ [DHCP_TYPE_OFF] = "off",
+ [DHCP_TYPE_ON] = "on",
+ [DHCP_TYPE_ANY] = "any",
+ [DHCP_TYPE_DHCP] = "dhcp",
+ [DHCP_TYPE_DHCP6] = "dhcp6",
+ [DHCP_TYPE_AUTO6] = "auto6",
+ [DHCP_TYPE_EITHER6] = "either6",
+ [DHCP_TYPE_IBFT] = "ibft",
+ [DHCP_TYPE_LINK6] = "link6",
+ [DHCP_TYPE_LINK_LOCAL] = "link-local",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(dracut_dhcp_type, DHCPType);
+
+static const char * const networkd_dhcp_type_table[_DHCP_TYPE_MAX] = {
+ [DHCP_TYPE_NONE] = "no",
+ [DHCP_TYPE_OFF] = "no",
+ [DHCP_TYPE_ON] = "yes",
+ [DHCP_TYPE_ANY] = "yes",
+ [DHCP_TYPE_DHCP] = "ipv4",
+ [DHCP_TYPE_DHCP6] = "ipv6",
+ [DHCP_TYPE_AUTO6] = "no", /* TODO: enable other setting? */
+ [DHCP_TYPE_EITHER6] = "ipv6", /* TODO: enable other setting? */
+ [DHCP_TYPE_IBFT] = "no",
+ [DHCP_TYPE_LINK6] = "no",
+ [DHCP_TYPE_LINK_LOCAL] = "no",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(networkd_dhcp_type, DHCPType);
+
+static const char * const networkd_ipv6ra_type_table[_DHCP_TYPE_MAX] = {
+ [DHCP_TYPE_NONE] = "no",
+ [DHCP_TYPE_OFF] = "no",
+ [DHCP_TYPE_LINK6] = "no",
+ [DHCP_TYPE_LINK_LOCAL] = "no",
+ /* We omit the other entries, to leave the default in effect */
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(networkd_ipv6ra_type, DHCPType);
+
+static const char * const networkd_link_local_type_table[_DHCP_TYPE_MAX] = {
+ [DHCP_TYPE_NONE] = "no",
+ [DHCP_TYPE_OFF] = "no",
+ [DHCP_TYPE_LINK6] = "ipv6",
+ [DHCP_TYPE_LINK_LOCAL] = "yes",
+ /* We omit the other entries, to leave the default in effect */
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(networkd_link_local_type, DHCPType);
+
+static Address *address_free(Address *address) {
+ if (!address)
+ return NULL;
+
+ if (address->network)
+ LIST_REMOVE(addresses, address->network->addresses, address);
+
+ return mfree(address);
+}
+
+static int address_new(Network *network, int family, unsigned char prefixlen,
+ union in_addr_union *addr, union in_addr_union *peer, Address **ret) {
+ Address *address;
+
+ assert(network);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(addr);
+
+ address = new(Address, 1);
+ if (!address)
+ return -ENOMEM;
+
+ *address = (Address) {
+ .family = family,
+ .prefixlen = prefixlen,
+ .address = *addr,
+ .peer = peer ? *peer : IN_ADDR_NULL,
+ };
+
+ LIST_PREPEND(addresses, network->addresses, address);
+
+ address->network = network;
+
+ if (ret)
+ *ret = address;
+ return 0;
+}
+
+static Route *route_free(Route *route) {
+ if (!route)
+ return NULL;
+
+ if (route->network)
+ LIST_REMOVE(routes, route->network->routes, route);
+
+ return mfree(route);
+}
+
+static int route_new(Network *network, int family, unsigned char prefixlen,
+ union in_addr_union *dest, union in_addr_union *gateway, Route **ret) {
+ Route *route;
+
+ assert(network);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(dest || gateway);
+
+ route = new(Route, 1);
+ if (!route)
+ return -ENOMEM;
+
+ *route = (Route) {
+ .family = family,
+ .prefixlen = prefixlen,
+ .dest = dest ? *dest : IN_ADDR_NULL,
+ .gateway = gateway ? *gateway : IN_ADDR_NULL,
+ };
+
+ LIST_PREPEND(routes, network->routes, route);
+
+ route->network = network;
+
+ if (ret)
+ *ret = route;
+ return 0;
+}
+
+static Network *network_free(Network *network) {
+ Address *address;
+ Route *route;
+
+ if (!network)
+ return NULL;
+
+ free(network->ifname);
+ free(network->hostname);
+ strv_free(network->dns);
+ free(network->vlan);
+ free(network->bridge);
+ free(network->bond);
+
+ while ((address = network->addresses))
+ address_free(address);
+
+ while ((route = network->routes))
+ route_free(route);
+
+ return mfree(network);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Network*, network_free);
+
+static int network_new(Context *context, const char *name, Network **ret) {
+ _cleanup_(network_freep) Network *network = NULL;
+ _cleanup_free_ char *ifname = NULL;
+ int r;
+
+ assert(context);
+
+ if (!isempty(name) && !ifname_valid(name))
+ return -EINVAL;
+
+ ifname = strdup(name);
+ if (!ifname)
+ return -ENOMEM;
+
+ network = new(Network, 1);
+ if (!network)
+ return -ENOMEM;
+
+ *network = (Network) {
+ .ifname = TAKE_PTR(ifname),
+ .dhcp_type = _DHCP_TYPE_INVALID,
+ .dhcp_use_dns = -1,
+ };
+
+ r = hashmap_ensure_put(&context->networks_by_name, &string_hash_ops, network->ifname, network);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = network;
+
+ TAKE_PTR(network);
+ return 0;
+}
+
+Network *network_get(Context *context, const char *ifname) {
+ return hashmap_get(context->networks_by_name, ifname);
+}
+
+static NetDev *netdev_free(NetDev *netdev) {
+ if (!netdev)
+ return NULL;
+
+ free(netdev->ifname);
+ free(netdev->kind);
+ return mfree(netdev);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(NetDev*, netdev_free);
+
+static int netdev_new(Context *context, const char *_kind, const char *_ifname, NetDev **ret) {
+ _cleanup_(netdev_freep) NetDev *netdev = NULL;
+ _cleanup_free_ char *kind = NULL, *ifname = NULL;
+ int r;
+
+ assert(context);
+ assert(_kind);
+
+ if (!ifname_valid(_ifname))
+ return -EINVAL;
+
+ kind = strdup(_kind);
+ if (!kind)
+ return -ENOMEM;
+
+ ifname = strdup(_ifname);
+ if (!ifname)
+ return -ENOMEM;
+
+ netdev = new(NetDev, 1);
+ if (!netdev)
+ return -ENOMEM;
+
+ *netdev = (NetDev) {
+ .kind = TAKE_PTR(kind),
+ .ifname = TAKE_PTR(ifname),
+ };
+
+ r = hashmap_ensure_put(&context->netdevs_by_name, &string_hash_ops, netdev->ifname, netdev);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = netdev;
+
+ TAKE_PTR(netdev);
+ return 0;
+}
+
+NetDev *netdev_get(Context *context, const char *ifname) {
+ return hashmap_get(context->netdevs_by_name, ifname);
+}
+
+static Link *link_free(Link *link) {
+ if (!link)
+ return NULL;
+
+ free(link->filename);
+ free(link->ifname);
+ strv_free(link->policies);
+ strv_free(link->alt_policies);
+ return mfree(link);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free);
+
+static int link_new(
+ Context *context,
+ const char *name,
+ const struct hw_addr_data *mac,
+ Link **ret) {
+
+ _cleanup_(link_freep) Link *link = NULL;
+ _cleanup_free_ char *ifname = NULL, *filename = NULL;
+ int r;
+
+ assert(context);
+ assert(mac);
+
+ if (name) {
+ if (!ifname_valid(name))
+ return -EINVAL;
+
+ ifname = strdup(name);
+ if (!ifname)
+ return -ENOMEM;
+
+ filename = strdup(name);
+ if (!filename)
+ return -ENOMEM;
+ }
+
+ if (!filename) {
+ filename = strdup(hw_addr_is_null(mac) ? "default" :
+ HW_ADDR_TO_STR_FULL(mac, HW_ADDR_TO_STRING_NO_COLON));
+ if (!filename)
+ return -ENOMEM;
+ }
+
+ link = new(Link, 1);
+ if (!link)
+ return -ENOMEM;
+
+ *link = (Link) {
+ .filename = TAKE_PTR(filename),
+ .ifname = TAKE_PTR(ifname),
+ .mac = *mac,
+ };
+
+ r = hashmap_ensure_put(&context->links_by_filename, &string_hash_ops, link->filename, link);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = link;
+
+ TAKE_PTR(link);
+ return 0;
+}
+
+Link *link_get(Context *context, const char *filename) {
+ assert(context);
+ assert(filename);
+ return hashmap_get(context->links_by_filename, filename);
+}
+
+static int network_set_dhcp_type(Context *context, const char *ifname, const char *dhcp_type) {
+ Network *network;
+ DHCPType t;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(dhcp_type);
+
+ t = dracut_dhcp_type_from_string(dhcp_type);
+ if (t < 0)
+ return t;
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ network->dhcp_type = t;
+ return 0;
+}
+
+static int network_set_hostname(Context *context, const char *ifname, const char *hostname) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return free_and_strdup(&network->hostname, hostname);
+}
+
+static int network_set_mtu(Context *context, const char *ifname, const char *mtu) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+
+ if (isempty(mtu))
+ return 0;
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return parse_mtu(AF_UNSPEC, mtu, &network->mtu);
+}
+
+static int network_set_mac_address(Context *context, const char *ifname, const char *mac) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+ assert(mac);
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return parse_ether_addr(mac, &network->mac);
+}
+
+static int network_set_address(Context *context, const char *ifname, int family, unsigned char prefixlen,
+ union in_addr_union *addr, union in_addr_union *peer) {
+ Network *network;
+
+ assert(context);
+ assert(ifname);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(addr);
+
+ if (!in_addr_is_set(family, addr))
+ return 0;
+
+ network = network_get(context, ifname);
+ if (!network)
+ return -ENODEV;
+
+ return address_new(network, family, prefixlen, addr, peer, NULL);
+}
+
+static int network_set_route(Context *context, const char *ifname, int family, unsigned char prefixlen,
+ union in_addr_union *dest, union in_addr_union *gateway) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+
+ if (!(dest && in_addr_is_set(family, dest)) &&
+ !(gateway && in_addr_is_set(family, gateway)))
+ return 0;
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return route_new(network, family, prefixlen, dest, gateway, NULL);
+}
+
+static int network_set_dns(Context *context, const char *ifname, int family, const char *dns) {
+ union in_addr_union a;
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6));
+ assert(dns);
+
+ if (family == AF_UNSPEC)
+ r = in_addr_from_string_auto(dns, &family, &a);
+ else
+ r = in_addr_from_string(family, dns, &a);
+ if (r < 0)
+ return r;
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return strv_extend(&network->dns, dns);
+}
+
+static int network_set_dhcp_use_dns(Context *context, const char *ifname, bool value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ network->dhcp_use_dns = value;
+
+ return 0;
+}
+
+static int network_set_vlan(Context *context, const char *ifname, const char *value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return free_and_strdup(&network->vlan, value);
+}
+
+static int network_set_bridge(Context *context, const char *ifname, const char *value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return free_and_strdup(&network->bridge, value);
+}
+
+static int network_set_bond(Context *context, const char *ifname, const char *value) {
+ Network *network;
+ int r;
+
+ assert(context);
+ assert(ifname);
+
+ network = network_get(context, ifname);
+ if (!network) {
+ r = network_new(context, ifname, &network);
+ if (r < 0)
+ return r;
+ }
+
+ return free_and_strdup(&network->bond, value);
+}
+
+static int parse_cmdline_ip_mtu_mac(Context *context, const char *ifname, const char *value) {
+ const char *mtu, *p;
+ int r;
+
+ assert(context);
+ assert(ifname);
+ assert(value);
+
+ /* [<mtu>][:<macaddr>] */
+
+ p = strchr(value, ':');
+ if (!p)
+ mtu = value;
+ else
+ mtu = strndupa_safe(value, p - value);
+
+ r = network_set_mtu(context, ifname, mtu);
+ if (r < 0)
+ return r;
+
+ if (!p || isempty(p + 1))
+ return 0;
+
+ r = network_set_mac_address(context, ifname, p + 1);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int parse_ip_address_one(int family, const char **value, union in_addr_union *ret) {
+ const char *p, *q, *buf;
+ int r;
+
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(value);
+ assert(ret);
+
+ p = ASSERT_PTR(*value);
+
+ if (p[0] == ':') {
+ *value = p + 1;
+ return 0;
+ }
+
+ if (family == AF_INET6) {
+ if (p[0] != '[')
+ return -EINVAL;
+
+ q = strchr(p + 1, ']');
+ if (!q)
+ return -EINVAL;
+
+ if (q[1] != ':')
+ return -EINVAL;
+
+ buf = strndupa_safe(p + 1, q - p - 1);
+ p = q + 2;
+ } else {
+ q = strchr(p, ':');
+ if (!q)
+ return -EINVAL;
+
+ buf = strndupa_safe(p, q - p);
+ p = q + 1;
+ }
+
+ r = in_addr_from_string(family, buf, ret);
+ if (r < 0)
+ return r;
+
+ *value = p;
+ return 1;
+}
+
+static int parse_netmask_or_prefixlen(int family, const char **value, unsigned char *ret) {
+ union in_addr_union netmask;
+ const char *p, *q;
+ int r;
+
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(value);
+ assert(*value);
+ assert(ret);
+
+ r = parse_ip_address_one(family, value, &netmask);
+ if (r > 0) {
+ if (family == AF_INET6)
+ /* TODO: Not supported yet. */
+ return -EINVAL;
+
+ *ret = in4_addr_netmask_to_prefixlen(&netmask.in);
+ } else if (r == 0)
+ *ret = family == AF_INET6 ? 128 : 32;
+ else {
+ p = strchr(*value, ':');
+ if (!p)
+ return -EINVAL;
+
+ q = strndupa_safe(*value, p - *value);
+ r = safe_atou8(q, ret);
+ if (r < 0)
+ return r;
+
+ *value = p + 1;
+ }
+
+ return 0;
+}
+
+static int parse_ip_dns_address_one(Context *context, const char *ifname, const char **value) {
+ const char *p, *q, *buf;
+ int r, family;
+
+ assert(context);
+ assert(ifname);
+ assert(value);
+
+ p = ASSERT_PTR(*value);
+
+ if (isempty(p))
+ return 0;
+
+ if (p[0] == '[') {
+ q = strchr(p + 1, ']');
+ if (!q)
+ return -EINVAL;
+ if (!IN_SET(q[1], ':', '\0'))
+ return -EINVAL;
+
+ buf = strndupa_safe(p + 1, q - p - 1);
+ p = q + 1;
+ family = AF_INET6;
+ } else {
+ q = strchr(p, ':');
+ if (!q)
+ buf = *value;
+ else
+ buf = strndupa_safe(*value, q - *value);
+
+ p += strlen(buf);
+ family = AF_INET;
+ }
+
+ r = network_set_dns(context, ifname, family, buf);
+ if (r < 0)
+ return r;
+
+ *value = p;
+ return 0;
+}
+
+static int parse_cmdline_ip_address(Context *context, int family, const char *value) {
+ union in_addr_union addr = {}, peer = {}, gateway = {};
+ const char *hostname = NULL, *ifname, *dhcp_type, *p;
+ unsigned char prefixlen;
+ int r;
+
+ assert(context);
+ assert(IN_SET(family, AF_INET, AF_INET6));
+ assert(value);
+
+ /* ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft|link6}[:[<mtu>][:<macaddr>]]
+ * ip=<client-IP>:[<peer>]:<gateway-IP>:<netmask>:<client_hostname>:<interface>:{none|off|dhcp|on|any|dhcp6|auto6|ibft|link6}[:[<dns1>][:<dns2>]] */
+
+ r = parse_ip_address_one(family, &value, &addr);
+ if (r < 0)
+ return r;
+ r = parse_ip_address_one(family, &value, &peer);
+ if (r < 0)
+ return r;
+ r = parse_ip_address_one(family, &value, &gateway);
+ if (r < 0)
+ return r;
+ r = parse_netmask_or_prefixlen(family, &value, &prefixlen);
+ if (r < 0)
+ return r;
+
+ /* hostname */
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ if (p != value) {
+ hostname = strndupa_safe(value, p - value);
+ if (!hostname_is_valid(hostname, 0))
+ return -EINVAL;
+ }
+
+ value = p + 1;
+
+ /* ifname */
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ ifname = strndupa_safe(value, p - value);
+
+ value = p + 1;
+
+ /* dhcp_type */
+ p = strchr(value, ':');
+ if (!p)
+ dhcp_type = value;
+ else
+ dhcp_type = strndupa_safe(value, p - value);
+
+ r = network_set_dhcp_type(context, ifname, dhcp_type);
+ if (r < 0)
+ return r;
+
+ /* set values */
+ r = network_set_hostname(context, ifname, hostname);
+ if (r < 0)
+ return r;
+
+ r = network_set_address(context, ifname, family, prefixlen, &addr, &peer);
+ if (r < 0)
+ return r;
+
+ r = network_set_route(context, ifname, family, 0, NULL, &gateway);
+ if (r < 0)
+ return r;
+
+ if (!p)
+ return 0;
+
+ /* First, try [<mtu>][:<macaddr>] */
+ r = parse_cmdline_ip_mtu_mac(context, ifname, p + 1);
+ if (r >= 0)
+ return 0;
+
+ /* Next, try [<dns1>][:<dns2>] */
+ value = p + 1;
+ r = parse_ip_dns_address_one(context, ifname, &value);
+ if (r < 0)
+ return r;
+
+ value += *value == ':';
+ r = parse_ip_dns_address_one(context, ifname, &value);
+ if (r < 0)
+ return r;
+
+ /* refuse unexpected trailing strings */
+ if (!isempty(value))
+ return -EINVAL;
+
+ return 0;
+}
+
+static int parse_cmdline_ip_interface(Context *context, const char *value) {
+ const char *ifname, *dhcp_type, *p;
+ int r;
+
+ assert(context);
+ assert(value);
+
+ /* ip=<interface>:{dhcp|on|any|dhcp6|auto6|link6}[:[<mtu>][:<macaddr>]] */
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ ifname = strndupa_safe(value, p - value);
+
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p)
+ dhcp_type = value;
+ else
+ dhcp_type = strndupa_safe(value, p - value);
+
+ r = network_set_dhcp_type(context, ifname, dhcp_type);
+ if (r < 0)
+ return r;
+
+ if (!p)
+ return 0;
+
+ return parse_cmdline_ip_mtu_mac(context, ifname, p + 1);
+}
+
+static int parse_cmdline_ip(Context *context, const char *key, const char *value) {
+ const char *p;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ /* ip={dhcp|on|any|dhcp6|auto6|either6|link6} */
+ return network_set_dhcp_type(context, "", value);
+
+ if (value[0] == '[')
+ return parse_cmdline_ip_address(context, AF_INET6, value);
+
+ r = parse_cmdline_ip_address(context, AF_INET, value);
+ if (r < 0)
+ return parse_cmdline_ip_interface(context, value);
+
+ return 0;
+}
+
+static int parse_cmdline_rd_route(Context *context, const char *key, const char *value) {
+ union in_addr_union addr = {}, gateway = {};
+ unsigned char prefixlen;
+ const char *buf, *p;
+ int family, r;
+
+ assert(context);
+ assert(key);
+
+ /* rd.route=<net>/<netmask>:<gateway>[:<interface>] */
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ if (value[0] == '[') {
+ p = strchr(value, ']');
+ if (!p)
+ return -EINVAL;
+
+ if (p[1] != ':')
+ return -EINVAL;
+
+ buf = strndupa_safe(value + 1, p - value - 1);
+ value = p + 2;
+ family = AF_INET6;
+ } else {
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ buf = strndupa_safe(value, p - value);
+ value = p + 1;
+ family = AF_INET;
+ }
+
+ r = in_addr_prefix_from_string(buf, family, &addr, &prefixlen);
+ if (r < 0)
+ return r;
+
+ p = strchr(value, ':');
+ if (!p)
+ value = strjoina(value, ":");
+
+ r = parse_ip_address_one(family, &value, &gateway);
+ if (r < 0)
+ return r;
+
+ return network_set_route(context, value, family, prefixlen, &addr, &gateway);
+}
+
+static int parse_cmdline_nameserver(Context *context, const char *key, const char *value) {
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ return network_set_dns(context, "", AF_UNSPEC, value);
+}
+
+static int parse_cmdline_rd_peerdns(Context *context, const char *key, const char *value) {
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return network_set_dhcp_use_dns(context, "", true);
+
+ r = parse_boolean(value);
+ if (r < 0)
+ return r;
+
+ return network_set_dhcp_use_dns(context, "", r);
+}
+
+static int parse_cmdline_vlan(Context *context, const char *key, const char *value) {
+ const char *name, *p;
+ NetDev *netdev;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ netdev = netdev_get(context, name);
+ if (!netdev) {
+ r = netdev_new(context, "vlan", name, &netdev);
+ if (r < 0)
+ return r;
+ }
+
+ return network_set_vlan(context, p + 1, name);
+}
+
+static int parse_cmdline_bridge(Context *context, const char *key, const char *value) {
+ const char *name, *p;
+ NetDev *netdev;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ netdev = netdev_get(context, name);
+ if (!netdev) {
+ r = netdev_new(context, "bridge", name, &netdev);
+ if (r < 0)
+ return r;
+ }
+
+ p++;
+ if (isempty(p))
+ return -EINVAL;
+
+ for (;;) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&p, &word, ",", 0);
+ if (r <= 0)
+ return r;
+
+ r = network_set_bridge(context, word, name);
+ if (r < 0)
+ return r;
+ }
+}
+
+static int parse_cmdline_bond(Context *context, const char *key, const char *value) {
+ const char *name, *slaves, *p;
+ NetDev *netdev;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ netdev = netdev_get(context, name);
+ if (!netdev) {
+ r = netdev_new(context, "bond", name, &netdev);
+ if (r < 0)
+ return r;
+ }
+
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p)
+ slaves = value;
+ else
+ slaves = strndupa_safe(value, p - value);
+
+ if (isempty(slaves))
+ return -EINVAL;
+
+ for (const char *q = slaves; ; ) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&q, &word, ",", 0);
+ if (r == 0)
+ break;
+ if (r < 0)
+ return r;
+
+ r = network_set_bond(context, word, name);
+ if (r < 0)
+ return r;
+ }
+
+ if (!p)
+ return 0;
+
+ value = p + 1;
+ p = strchr(value, ':');
+ if (!p)
+ /* TODO: set bonding options */
+ return 0;
+
+ return parse_mtu(AF_UNSPEC, p + 1, &netdev->mtu);
+}
+
+static int parse_cmdline_ifname(Context *context, const char *key, const char *value) {
+ struct hw_addr_data mac;
+ const char *name, *p;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ /* ifname=<interface>:<MAC> */
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ p = strchr(value, ':');
+ if (!p)
+ return -EINVAL;
+
+ name = strndupa_safe(value, p - value);
+
+ r = parse_hw_addr(p + 1, &mac);
+ if (r < 0)
+ return r;
+
+ return link_new(context, name, &mac, NULL);
+}
+
+static int parse_cmdline_ifname_policy(Context *context, const char *key, const char *value) {
+ _cleanup_strv_free_ char **policies = NULL, **alt_policies = NULL;
+ struct hw_addr_data mac = HW_ADDR_NULL;
+ Link *link;
+ int r;
+
+ assert(context);
+ assert(key);
+
+ /* net.ifname-policy=policy1[,policy2,...][,<MAC>] */
+
+ if (proc_cmdline_value_missing(key, value))
+ return -EINVAL;
+
+ for (const char *q = value; ; ) {
+ _cleanup_free_ char *word = NULL;
+ NamePolicy p;
+
+ r = extract_first_word(&q, &word, ",", 0);
+ if (r == 0)
+ break;
+ if (r < 0)
+ return r;
+
+ p = name_policy_from_string(word);
+ if (p < 0) {
+ r = parse_hw_addr(word, &mac);
+ if (r < 0)
+ return r;
+
+ if (hw_addr_is_null(&mac))
+ return -EINVAL;
+
+ if (!isempty(q))
+ return -EINVAL;
+
+ break;
+ }
+
+ if (alternative_names_policy_from_string(word) >= 0) {
+ r = strv_extend(&alt_policies, word);
+ if (r < 0)
+ return r;
+ }
+
+ r = strv_consume(&policies, TAKE_PTR(word));
+ if (r < 0)
+ return r;
+ }
+
+ if (strv_isempty(policies))
+ return -EINVAL;
+
+ r = link_new(context, NULL, &mac, &link);
+ if (r < 0)
+ return r;
+
+ link->policies = TAKE_PTR(policies);
+ link->alt_policies = TAKE_PTR(alt_policies);
+ return 0;
+}
+
+int parse_cmdline_item(const char *key, const char *value, void *data) {
+ Context *context = ASSERT_PTR(data);
+
+ assert(key);
+
+ if (streq(key, "ip"))
+ return parse_cmdline_ip(context, key, value);
+ if (streq(key, "rd.route"))
+ return parse_cmdline_rd_route(context, key, value);
+ if (streq(key, "nameserver"))
+ return parse_cmdline_nameserver(context, key, value);
+ if (streq(key, "rd.peerdns"))
+ return parse_cmdline_rd_peerdns(context, key, value);
+ if (streq(key, "vlan"))
+ return parse_cmdline_vlan(context, key, value);
+ if (streq(key, "bridge"))
+ return parse_cmdline_bridge(context, key, value);
+ if (streq(key, "bond"))
+ return parse_cmdline_bond(context, key, value);
+ if (streq(key, "ifname"))
+ return parse_cmdline_ifname(context, key, value);
+ if (streq(key, "net.ifname-policy"))
+ return parse_cmdline_ifname_policy(context, key, value);
+
+ return 0;
+}
+
+int context_merge_networks(Context *context) {
+ Network *all, *network;
+ int r;
+
+ assert(context);
+
+ /* Copy settings about the following options
+ rd.route=<net>/<netmask>:<gateway>[:<interface>]
+ nameserver=<IP> [nameserver=<IP> ...]
+ rd.peerdns=0 */
+
+ all = network_get(context, "");
+ if (!all)
+ return 0;
+
+ if (hashmap_size(context->networks_by_name) <= 1)
+ return 0;
+
+ HASHMAP_FOREACH(network, context->networks_by_name) {
+ if (network == all)
+ continue;
+
+ network->dhcp_use_dns = all->dhcp_use_dns;
+
+ r = strv_extend_strv(&network->dns, all->dns, false);
+ if (r < 0)
+ return r;
+
+ LIST_FOREACH(routes, route, all->routes) {
+ r = route_new(network, route->family, route->prefixlen, &route->dest, &route->gateway, NULL);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ assert_se(hashmap_remove(context->networks_by_name, "") == all);
+ network_free(all);
+ return 0;
+}
+
+void context_clear(Context *context) {
+ if (!context)
+ return;
+
+ hashmap_free_with_destructor(context->networks_by_name, network_free);
+ hashmap_free_with_destructor(context->netdevs_by_name, netdev_free);
+ hashmap_free_with_destructor(context->links_by_filename, link_free);
+}
+
+static int address_dump(Address *address, FILE *f) {
+ assert(address);
+ assert(f);
+
+ fprintf(f,
+ "\n[Address]\n"
+ "Address=%s\n",
+ IN_ADDR_PREFIX_TO_STRING(address->family, &address->address, address->prefixlen));
+ if (in_addr_is_set(address->family, &address->peer))
+ fprintf(f, "Peer=%s\n",
+ IN_ADDR_TO_STRING(address->family, &address->peer));
+ return 0;
+}
+
+static int route_dump(Route *route, FILE *f) {
+ assert(route);
+ assert(f);
+
+ fputs("\n[Route]\n", f);
+ if (in_addr_is_set(route->family, &route->dest))
+ fprintf(f, "Destination=%s\n",
+ IN_ADDR_PREFIX_TO_STRING(route->family, &route->dest, route->prefixlen));
+ if (in_addr_is_set(route->family, &route->gateway))
+ fprintf(f, "Gateway=%s\n",
+ IN_ADDR_TO_STRING(route->family, &route->gateway));
+
+ return 0;
+}
+
+void network_dump(Network *network, FILE *f) {
+ const char *dhcp;
+
+ assert(network);
+ assert(f);
+
+ fputs("[Match]\n", f);
+
+ if (isempty(network->ifname))
+ /* If the interface name is not specified, then let's make the .network file match the all
+ * physical interfaces. */
+ fputs("Kind=!*\n"
+ "Type=!loopback\n", f);
+ else
+ fprintf(f, "Name=%s\n", network->ifname);
+
+ fputs("\n[Link]\n", f);
+
+ if (!ether_addr_is_null(&network->mac))
+ fprintf(f, "MACAddress=%s\n", ETHER_ADDR_TO_STR(&network->mac));
+ if (network->mtu > 0)
+ fprintf(f, "MTUBytes=%" PRIu32 "\n", network->mtu);
+
+ fputs("\n[Network]\n", f);
+
+ dhcp = networkd_dhcp_type_to_string(network->dhcp_type);
+ if (dhcp)
+ fprintf(f, "DHCP=%s\n", dhcp);
+
+ const char *ll;
+ ll = networkd_link_local_type_to_string(network->dhcp_type);
+ if (ll)
+ fprintf(f, "LinkLocalAddressing=%s\n", ll);
+
+ const char *ra;
+ ra = networkd_ipv6ra_type_to_string(network->dhcp_type);
+ if (ra)
+ fprintf(f, "IPv6AcceptRA=%s\n", ra);
+
+ if (!strv_isempty(network->dns))
+ STRV_FOREACH(dns, network->dns)
+ fprintf(f, "DNS=%s\n", *dns);
+
+ if (network->vlan)
+ fprintf(f, "VLAN=%s\n", network->vlan);
+
+ if (network->bridge)
+ fprintf(f, "Bridge=%s\n", network->bridge);
+
+ if (network->bond)
+ fprintf(f, "Bond=%s\n", network->bond);
+
+ fputs("\n[DHCP]\n", f);
+
+ if (!isempty(network->hostname))
+ fprintf(f, "Hostname=%s\n", network->hostname);
+
+ if (network->dhcp_use_dns >= 0)
+ fprintf(f, "UseDNS=%s\n", yes_no(network->dhcp_use_dns));
+
+ LIST_FOREACH(addresses, address, network->addresses)
+ (void) address_dump(address, f);
+
+ LIST_FOREACH(routes, route, network->routes)
+ (void) route_dump(route, f);
+}
+
+void netdev_dump(NetDev *netdev, FILE *f) {
+ assert(netdev);
+ assert(f);
+
+ fprintf(f,
+ "[NetDev]\n"
+ "Kind=%s\n"
+ "Name=%s\n",
+ netdev->kind,
+ netdev->ifname);
+
+ if (netdev->mtu > 0)
+ fprintf(f, "MTUBytes=%" PRIu32 "\n", netdev->mtu);
+}
+
+void link_dump(Link *link, FILE *f) {
+ assert(link);
+ assert(f);
+
+ fputs("[Match]\n", f);
+
+ if (!hw_addr_is_null(&link->mac))
+ fprintf(f, "MACAddress=%s\n", HW_ADDR_TO_STR(&link->mac));
+ else
+ fputs("OriginalName=*\n", f);
+
+ fputs("\n[Link]\n", f);
+
+ if (!isempty(link->ifname))
+ fprintf(f, "Name=%s\n", link->ifname);
+
+ if (!strv_isempty(link->policies)) {
+ fputs("NamePolicy=", f);
+ fputstrv(f, link->policies, " ", NULL);
+ fputc('\n', f);
+ }
+
+ if (!strv_isempty(link->alt_policies)) {
+ fputs("AlternativeNamesPolicy=", f);
+ fputstrv(f, link->alt_policies, " ", NULL);
+ fputc('\n', f);
+ }
+}
+
+int network_format(Network *network, char **ret) {
+ _cleanup_(memstream_done) MemStream m = {};
+ FILE *f;
+
+ assert(network);
+ assert(ret);
+
+ f = memstream_init(&m);
+ if (!f)
+ return -ENOMEM;
+
+ network_dump(network, f);
+
+ return memstream_finalize(&m, ret, NULL);
+}
+
+int netdev_format(NetDev *netdev, char **ret) {
+ _cleanup_(memstream_done) MemStream m = {};
+ FILE *f;
+
+ assert(netdev);
+ assert(ret);
+
+ f = memstream_init(&m);
+ if (!f)
+ return -ENOMEM;
+
+ netdev_dump(netdev, f);
+
+ return memstream_finalize(&m, ret, NULL);
+}
+
+int link_format(Link *link, char **ret) {
+ _cleanup_(memstream_done) MemStream m = {};
+ FILE *f;
+
+ assert(link);
+ assert(ret);
+
+ f = memstream_init(&m);
+ if (!f)
+ return -ENOMEM;
+
+ link_dump(link, f);
+
+ return memstream_finalize(&m, ret, NULL);
+}
diff --git a/src/network/generator/network-generator.h b/src/network/generator/network-generator.h
new file mode 100644
index 0000000..aa5ca9d
--- /dev/null
+++ b/src/network/generator/network-generator.h
@@ -0,0 +1,116 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdio.h>
+
+#include "ether-addr-util.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "list.h"
+
+typedef enum DHCPType {
+ DHCP_TYPE_NONE,
+ DHCP_TYPE_OFF, /* Same as DHCP_TYPE_NONE */
+ DHCP_TYPE_ON,
+ DHCP_TYPE_ANY, /* Same as DHCP_TYPE_ON */
+ DHCP_TYPE_DHCP, /* Actually means: DHCPv4 */
+ DHCP_TYPE_DHCP6,
+ DHCP_TYPE_AUTO6,
+ DHCP_TYPE_EITHER6,
+ DHCP_TYPE_IBFT,
+ DHCP_TYPE_LINK6,
+ DHCP_TYPE_LINK_LOCAL,
+ _DHCP_TYPE_MAX,
+ _DHCP_TYPE_INVALID = -EINVAL,
+} DHCPType;
+
+typedef struct Address Address;
+typedef struct Link Link;
+typedef struct NetDev NetDev;
+typedef struct Network Network;
+typedef struct Route Route;
+typedef struct Context Context;
+
+struct Address {
+ Network *network;
+
+ union in_addr_union address, peer;
+ unsigned char prefixlen;
+ int family;
+
+ LIST_FIELDS(Address, addresses);
+};
+
+struct Route {
+ Network *network;
+
+ union in_addr_union dest, gateway;
+ unsigned char prefixlen;
+ int family;
+
+ LIST_FIELDS(Route, routes);
+};
+
+struct Network {
+ /* [Match] */
+ char *ifname;
+
+ /* [Link] */
+ struct ether_addr mac;
+ uint32_t mtu;
+
+ /* [Network] */
+ DHCPType dhcp_type;
+ char **dns;
+ char *vlan;
+ char *bridge;
+ char *bond;
+
+ /* [DHCP] */
+ char *hostname;
+ int dhcp_use_dns;
+
+ LIST_HEAD(Address, addresses);
+ LIST_HEAD(Route, routes);
+};
+
+struct NetDev {
+ /* [NetDev] */
+ char *ifname;
+ char *kind;
+ uint32_t mtu;
+};
+
+struct Link {
+ char *filename;
+
+ /* [Match] */
+ struct hw_addr_data mac;
+
+ /* [Link] */
+ char *ifname;
+ char **policies;
+ char **alt_policies;
+};
+
+typedef struct Context {
+ Hashmap *networks_by_name;
+ Hashmap *netdevs_by_name;
+ Hashmap *links_by_filename;
+} Context;
+
+int parse_cmdline_item(const char *key, const char *value, void *data);
+int context_merge_networks(Context *context);
+void context_clear(Context *context);
+
+Network *network_get(Context *context, const char *ifname);
+void network_dump(Network *network, FILE *f);
+int network_format(Network *network, char **ret);
+
+NetDev *netdev_get(Context *context, const char *ifname);
+void netdev_dump(NetDev *netdev, FILE *f);
+int netdev_format(NetDev *netdev, char **ret);
+
+Link *link_get(Context *context, const char *filename);
+void link_dump(Link *link, FILE *f);
+int link_format(Link *link, char **ret);
diff --git a/src/network/generator/test-network-generator.c b/src/network/generator/test-network-generator.c
new file mode 100644
index 0000000..7850da9
--- /dev/null
+++ b/src/network/generator/test-network-generator.c
@@ -0,0 +1,462 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "macro.h"
+#include "network-generator.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void test_network_one(const char *ifname, const char *key, const char *value, const char *expected) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_free_ char *output = NULL;
+ Network *network;
+
+ printf("# %s=%s\n", key, value);
+ assert_se(parse_cmdline_item(key, value, &context) >= 0);
+ assert_se(network = network_get(&context, ifname));
+ assert_se(network_format(network, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+static void test_network_two(const char *ifname,
+ const char *key1, const char *value1,
+ const char *key2, const char *value2,
+ const char *expected) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_free_ char *output = NULL;
+ Network *network;
+
+ printf("# %s=%s\n", key1, value1);
+ printf("# %s=%s\n", key2, value2);
+ assert_se(parse_cmdline_item(key1, value1, &context) >= 0);
+ assert_se(parse_cmdline_item(key2, value2, &context) >= 0);
+ assert_se(context_merge_networks(&context) >= 0);
+ assert_se(network = network_get(&context, ifname));
+ assert_se(network_format(network, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+static void test_netdev_one(const char *ifname, const char *key, const char *value, const char *expected) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_free_ char *output = NULL;
+ NetDev *netdev;
+
+ printf("# %s=%s\n", key, value);
+ assert_se(parse_cmdline_item(key, value, &context) >= 0);
+ assert_se(netdev = netdev_get(&context, ifname));
+ assert_se(netdev_format(netdev, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+static void test_link_one(const char *filename, const char *key, const char *value, const char *expected) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_free_ char *output = NULL;
+ Link *link;
+
+ printf("# %s=%s\n", key, value);
+ assert_se(parse_cmdline_item(key, value, &context) >= 0);
+ assert_se(link = link_get(&context, filename));
+ assert_se(link_format(link, &output) >= 0);
+ puts(output);
+ assert_se(streq(output, expected));
+}
+
+int main(int argc, char *argv[]) {
+ test_setup_logging(LOG_DEBUG);
+
+ test_network_one("", "ip", "dhcp6",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=ipv6\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "eth0:dhcp",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=ipv4\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "eth0:dhcp:1530",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=ipv4\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "eth0:dhcp:1530:00:11:22:33:44:55",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=ipv4\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10::192.168.0.1:255.255.255.0:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:1530",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:1530:00:11:22:33:44:55",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "MTUBytes=1530\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_one("eth0", "ip", "[2001:1234:56:8f63::10]::[2001:1234:56:8f63::1]:64:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=2001:1234:56:8f63::10/64\n"
+ "\n[Route]\n"
+ "Gateway=2001:1234:56:8f63::1\n"
+ );
+
+ test_network_one("eth0", "ip", "[2001:1234:56:8f63::10]:[2001:1234:56:8f63::2]:[2001:1234:56:8f63::1]:64:hogehoge:eth0:on",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=2001:1234:56:8f63::10/64\n"
+ "Peer=2001:1234:56:8f63::2\n"
+ "\n[Route]\n"
+ "Gateway=2001:1234:56:8f63::1\n"
+ );
+
+ test_network_one("", "rd.route", "10.1.2.3/16:10.0.2.3",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "\n[Route]\n"
+ "Destination=10.1.2.3/16\n"
+ "Gateway=10.0.2.3\n"
+ );
+
+ test_network_one("eth0", "rd.route", "10.1.2.3/16:10.0.2.3:eth0",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "\n[Route]\n"
+ "Destination=10.1.2.3/16\n"
+ "Gateway=10.0.2.3\n"
+ );
+
+ test_network_one("", "nameserver", "10.1.2.3",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DNS=10.1.2.3\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("", "rd.peerdns", "0",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "UseDNS=no\n"
+ );
+
+ test_network_one("", "rd.peerdns", "1",
+ "[Match]\n"
+ "Kind=!*\n"
+ "Type=!loopback\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "\n[DHCP]\n"
+ "UseDNS=yes\n"
+ );
+
+ test_network_one("eth0", "vlan", "vlan99:eth0",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "VLAN=vlan99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "bridge", "bridge99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bridge=bridge99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth1", "bridge", "bridge99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth1\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bridge=bridge99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth0", "bond", "bond99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bond=bond99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_network_one("eth1", "bond", "bond99:eth0,eth1::1530",
+ "[Match]\n"
+ "Name=eth1\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "Bond=bond99\n"
+ "\n[DHCP]\n"
+ );
+
+ test_netdev_one("bond99", "bond", "bond99:eth0,eth1::1530",
+ "[NetDev]\n"
+ "Kind=bond\n"
+ "Name=bond99\n"
+ "MTUBytes=1530\n"
+ );
+
+ test_link_one("hogehoge", "ifname", "hogehoge:00:11:22:33:44:55",
+ "[Match]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "\n[Link]\n"
+ "Name=hogehoge\n"
+ );
+
+ test_link_one("001122334455", "net.ifname-policy", "keep,kernel,database,onboard,slot,path,mac,00:11:22:33:44:55",
+ "[Match]\n"
+ "MACAddress=00:11:22:33:44:55\n"
+ "\n[Link]\n"
+ "NamePolicy=keep kernel database onboard slot path mac\n"
+ "AlternativeNamesPolicy=database onboard slot path mac\n"
+ );
+
+ test_link_one("default", "net.ifname-policy", "keep,kernel,database,onboard,slot,path,mac",
+ "[Match]\n"
+ "OriginalName=*\n"
+ "\n[Link]\n"
+ "NamePolicy=keep kernel database onboard slot path mac\n"
+ "AlternativeNamesPolicy=database onboard slot path mac\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "rd.route", "10.1.2.3/16:10.0.2.3",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Destination=10.1.2.3/16\n"
+ "Gateway=10.0.2.3\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on",
+ "nameserver", "10.1.2.3",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.1.2.3\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "nameserver", "10.1.2.3",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "DNS=10.1.2.3\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "rd.peerdns", "1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "UseDNS=yes\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ test_network_two("eth0",
+ "ip", "192.168.0.10:192.168.0.2:192.168.0.1:255.255.255.0:hogehoge:eth0:on:10.10.10.10:10.10.10.11",
+ "bridge", "bridge99:eth0,eth1",
+ "[Match]\n"
+ "Name=eth0\n"
+ "\n[Link]\n"
+ "\n[Network]\n"
+ "DHCP=yes\n"
+ "DNS=10.10.10.10\n"
+ "DNS=10.10.10.11\n"
+ "Bridge=bridge99\n"
+ "\n[DHCP]\n"
+ "Hostname=hogehoge\n"
+ "\n[Address]\n"
+ "Address=192.168.0.10/24\n"
+ "Peer=192.168.0.2\n"
+ "\n[Route]\n"
+ "Gateway=192.168.0.1\n"
+ );
+
+ return 0;
+}