123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751 |
- // SPDX-License-Identifier: GPL-2.0-or-later
- /*
- * Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
- */
- #define _GNU_SOURCE
- #include <arpa/inet.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <sys/wait.h>
- #include <net/if.h>
- #include <libubox/avl-cmp.h>
- #include <libubox/utils.h>
- #include <libubox/blobmsg_json.h>
- #include "unetd.h"
- enum {
- NETDATA_ATTR_CONFIG,
- NETDATA_ATTR_HOSTS,
- NETDATA_ATTR_GROUPS,
- NETDATA_ATTR_SERVICES,
- __NETDATA_ATTR_MAX,
- };
- static const struct blobmsg_policy netdata_policy[__NETDATA_ATTR_MAX] = {
- [NETDATA_ATTR_CONFIG] = { "config", BLOBMSG_TYPE_TABLE },
- [NETDATA_ATTR_HOSTS] = { "hosts", BLOBMSG_TYPE_TABLE },
- [NETDATA_ATTR_SERVICES] = { "services", BLOBMSG_TYPE_TABLE },
- };
- enum {
- NETCONF_ATTR_ID,
- NETCONF_ATTR_PORT,
- NETCONF_ATTR_PEX_PORT,
- NETCONF_ATTR_KEEPALIVE,
- NETCONF_ATTR_STUN_SERVERS,
- __NETCONF_ATTR_MAX
- };
- static const struct blobmsg_policy netconf_policy[__NETCONF_ATTR_MAX] = {
- [NETCONF_ATTR_ID] = { "id", BLOBMSG_TYPE_STRING },
- [NETCONF_ATTR_PORT] = { "port", BLOBMSG_TYPE_INT32 },
- [NETCONF_ATTR_PEX_PORT] = { "peer-exchange-port", BLOBMSG_TYPE_INT32 },
- [NETCONF_ATTR_KEEPALIVE] = { "keepalive", BLOBMSG_TYPE_INT32 },
- [NETCONF_ATTR_STUN_SERVERS] = { "stun-servers", BLOBMSG_TYPE_ARRAY },
- };
- const struct blobmsg_policy network_policy[__NETWORK_ATTR_MAX] = {
- [NETWORK_ATTR_NAME] = { "name", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_TYPE] = { "type", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_AUTH_KEY] = { "auth_key", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_KEY] = { "key", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_FILE] = { "file", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_DATA] = { "data", BLOBMSG_TYPE_TABLE },
- [NETWORK_ATTR_INTERFACE] = { "interface", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_KEEPALIVE] = { "keepalive", BLOBMSG_TYPE_INT32 },
- [NETWORK_ATTR_DOMAIN] = { "domain", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_UPDATE_CMD] = { "update-cmd", BLOBMSG_TYPE_STRING },
- [NETWORK_ATTR_TUNNELS] = { "tunnels", BLOBMSG_TYPE_TABLE },
- [NETWORK_ATTR_AUTH_CONNECT] = { "auth_connect", BLOBMSG_TYPE_ARRAY },
- [NETWORK_ATTR_PEER_DATA] = { "peer_data", BLOBMSG_TYPE_ARRAY },
- };
- AVL_TREE(networks, avl_strcmp, false, NULL);
- static struct blob_buf b;
- static void network_load_stun_servers(struct network *net, struct blob_attr *data)
- {
- struct blob_attr *cur;
- int rem;
- blobmsg_for_each_attr(cur, data, rem)
- network_stun_server_add(net, blobmsg_get_string(cur));
- }
- static void network_load_config_data(struct network *net, struct blob_attr *data)
- {
- struct blob_attr *tb[__NETCONF_ATTR_MAX];
- struct blob_attr *cur;
- siphash_key_t key = {};
- blobmsg_parse(netconf_policy, __NETCONF_ATTR_MAX, tb,
- blobmsg_data(data), blobmsg_len(data));
- if ((cur = tb[NETCONF_ATTR_PORT]) != NULL)
- net->net_config.port = blobmsg_get_u32(cur);
- else
- net->net_config.port = 51820;
- if ((cur = tb[NETCONF_ATTR_PEX_PORT]) != NULL)
- net->net_config.pex_port = blobmsg_get_u32(cur);
- if ((cur = tb[NETCONF_ATTR_ID]) != NULL) {
- const char *id = blobmsg_get_string(cur);
- siphash_to_le64(&net->net_config.addr.network_id, id, strlen(id), &key);
- } else {
- uint32_t port = cpu_to_le32(net->net_config.port);
- siphash_to_le64(&net->net_config.addr.network_id, &port, sizeof(port), &key);
- }
- net->net_config.addr.network_id[0] = 0xfd;
- network_fill_host_addr(&net->net_config.addr, net->config.pubkey);
- if (net->config.keepalive >= 0)
- net->net_config.keepalive = net->config.keepalive;
- else if ((cur = tb[NETCONF_ATTR_KEEPALIVE]) != NULL)
- net->net_config.keepalive = blobmsg_get_u32(cur);
- else
- net->net_config.keepalive = 0;
- if ((cur = tb[NETCONF_ATTR_STUN_SERVERS]) != NULL &&
- blobmsg_check_array(cur, BLOBMSG_TYPE_STRING) > 0)
- network_load_stun_servers(net, cur);
- }
- static int network_load_data(struct network *net, struct blob_attr *data)
- {
- struct blob_attr *tb[__NETDATA_ATTR_MAX];
- siphash_key_t key = {};
- net->net_config.hash = siphash(data, blob_raw_len(data), &key);
- blobmsg_parse(netdata_policy, __NETDATA_ATTR_MAX, tb,
- blobmsg_data(data), blobmsg_len(data));
- network_load_config_data(net, tb[NETDATA_ATTR_CONFIG]);
- network_hosts_add(net, tb[NETDATA_ATTR_HOSTS]);
- network_services_add(net, tb[NETDATA_ATTR_SERVICES]);
- return 0;
- }
- static int network_load_file(struct network *net)
- {
- blob_buf_init(&b, 0);
- if (!blobmsg_add_json_from_file(&b, net->config.file))
- return -1;
- return network_load_data(net, b.head);
- }
- static int network_load_dynamic(struct network *net)
- {
- const char *json = NULL;
- char *fname = NULL;
- struct stat st;
- FILE *f = NULL;
- int ret = -1;
- if (asprintf(&fname, "%s/%s.bin", data_dir, network_name(net)) < 0)
- return -1;
- f = fopen(fname, "r");
- free(fname);
- if (!f) {
- D_NET(net, "failed to open %s/%s.bin\n", data_dir, network_name(net));
- return -1;
- }
- if (fstat(fileno(f), &st) < 0)
- goto out;
- net->net_data_len = st.st_size;
- net->net_data = realloc(net->net_data, net->net_data_len + 1);
- memset(net->net_data + net->net_data_len, 0, 1);
- if (fread(net->net_data, 1, net->net_data_len, f) != net->net_data_len ||
- unet_auth_data_validate(net->config.auth_key, net->net_data,
- net->net_data_len, &net->net_data_version, &json)) {
- net->net_data_len = 0;
- goto out;
- }
- fclose(f);
- blob_buf_init(&b, 0);
- if (!blobmsg_add_json_from_string(&b, json)) {
- net->net_data_len = 0;
- return -1;
- }
- return network_load_data(net, b.head);
- out:
- fclose(f);
- return ret;
- }
- int network_save_dynamic(struct network *net)
- {
- char *fname = NULL, *fname2;
- size_t len;
- FILE *f;
- int fd, ret;
- if (net->config.type != NETWORK_TYPE_DYNAMIC ||
- !net->net_data_len)
- return -1;
- if (asprintf(&fname, "%s/%s.bin.XXXXXXXX", data_dir, network_name(net)) < 0)
- return -1;
- fd = mkstemp(fname);
- if (fd < 0)
- goto error;
- f = fdopen(fd, "w");
- if (!f) {
- close(fd);
- goto error;
- }
- len = fwrite(net->net_data, 1, net->net_data_len, f);
- fflush(f);
- fdatasync(fd);
- fclose(f);
- if (len != net->net_data_len)
- goto error;
- fname2 = strdup(fname);
- *strrchr(fname2, '.') = 0;
- ret = rename(fname, fname2);
- free(fname2);
- if (ret)
- unlink(fname);
- free(fname);
- return ret;
- error:
- free(fname);
- return -1;
- }
- static void
- network_fill_ip(struct blob_buf *buf, int af, union network_addr *addr, int mask)
- {
- char *str;
- void *c;
- c = blobmsg_open_table(buf, NULL);
- blobmsg_printf(buf, "mask", "%d", mask);
- str = blobmsg_alloc_string_buffer(buf, "ipaddr", INET6_ADDRSTRLEN);
- inet_ntop(af, addr, str, INET6_ADDRSTRLEN);
- blobmsg_add_string_buffer(buf);
- blobmsg_close_table(buf, c);
- }
- static void
- network_fill_ipaddr_list(struct network_host *host, struct blob_buf *b, bool ipv6)
- {
- union network_addr addr = {};
- struct blob_attr *cur;
- void *c;
- int rem;
- int af;
- af = ipv6 ? AF_INET6 : AF_INET;
- blobmsg_for_each_attr(cur, host->peer.ipaddr, rem) {
- const char *str = blobmsg_get_string(cur);
- if (!!strchr(str, ':') != ipv6)
- continue;
- if (inet_pton(af, str, &addr) != 1)
- continue;
- c = blobmsg_open_table(b, NULL);
- blobmsg_add_string(b, "ipaddr", str);
- blobmsg_add_string(b, "mask", ipv6 ? "128" : "32");
- blobmsg_close_table(b, c);
- }
- }
- static void
- network_fill_ip_settings(struct network *net, struct blob_buf *buf)
- {
- struct network_host *host = net->net_config.local_host;
- void *c;
- c = blobmsg_open_array(buf, "ipaddr");
- network_fill_ipaddr_list(host, buf, false);
- blobmsg_close_array(buf, c);
- c = blobmsg_open_array(buf, "ip6addr");
- network_fill_ip(buf, AF_INET6, &host->peer.local_addr, 64);
- network_fill_ipaddr_list(host, buf, true);
- blobmsg_close_array(buf, c);
- }
- static void
- __network_fill_host_subnets(struct network_host *host, struct blob_buf *b, bool ipv6)
- {
- union network_addr addr = {};
- struct blob_attr *cur;
- void *c;
- int af;
- int mask;
- int rem;
- af = ipv6 ? AF_INET6 : AF_INET;
- blobmsg_for_each_attr(cur, host->peer.subnet, rem) {
- const char *str = blobmsg_get_string(cur);
- char *buf;
- if (!!strchr(str, ':') != ipv6)
- continue;
- if (network_get_subnet(af, &addr, &mask, str))
- continue;
- c = blobmsg_open_table(b, NULL);
- buf = blobmsg_alloc_string_buffer(b, "target", INET6_ADDRSTRLEN);
- inet_ntop(af, &addr, buf, INET6_ADDRSTRLEN);
- blobmsg_add_string_buffer(b);
- blobmsg_printf(b, "netmask", "%d", mask);
- blobmsg_close_table(b, c);
- }
- blobmsg_for_each_attr(cur, host->peer.ipaddr, rem) {
- const char *str = blobmsg_get_string(cur);
- if (!!strchr(str, ':') != ipv6)
- continue;
- if (inet_pton(af, str, &addr) != 1)
- continue;
- c = blobmsg_open_table(b, NULL);
- blobmsg_add_string(b, "target", str);
- blobmsg_add_string(b, "netmask", ipv6 ? "128" : "32");
- blobmsg_close_table(b, c);
- }
- }
- static void
- __network_fill_subnets(struct network *net, struct blob_buf *buf, bool ipv6)
- {
- struct network_host *host;
- void *c;
- c = blobmsg_open_array(buf, ipv6 ? "routes6": "routes");
- avl_for_each_element(&net->hosts, host, node) {
- if (host == net->net_config.local_host)
- continue;
- __network_fill_host_subnets(host, buf, ipv6);
- }
- blobmsg_close_array(buf, c);
- }
- static void
- network_fill_subnets(struct network *net, struct blob_buf *buf)
- {
- __network_fill_subnets(net, buf, false);
- __network_fill_subnets(net, buf, true);
- }
- static bool
- __network_skip_endpoint_route(struct network *net, struct network_host *host,
- union network_endpoint *ep)
- {
- bool ipv6 = ep->sa.sa_family == AF_INET6;
- uint32_t *subnet32, *addr32, mask32;
- union network_addr addr = {};
- struct blob_attr *cur;
- int mask, rem;
- blobmsg_for_each_attr(cur, host->peer.ipaddr, rem) {
- const char *str = blobmsg_get_string(cur);
- if (!!strchr(str, ':') != ipv6)
- continue;
- if (inet_pton(ep->sa.sa_family, str, &addr) != 1)
- continue;
- if (ipv6) {
- if (!memcmp(&addr.in6, &ep->in6.sin6_addr, sizeof(addr.in6)))
- return true;
- } else {
- if (!memcmp(&addr.in, &ep->in.sin_addr, sizeof(addr.in)))
- return true;
- }
- }
- if (ipv6)
- addr32 = (uint32_t *)&ep->in6.sin6_addr;
- else
- addr32 = (uint32_t *)&ep->in.sin_addr;
- subnet32 = (uint32_t *)&addr;
- blobmsg_for_each_attr(cur, host->peer.subnet, rem) {
- const char *str = blobmsg_get_string(cur);
- int i;
- if (!!strchr(str, ':') != ipv6)
- continue;
- if (network_get_subnet(ep->sa.sa_family, &addr, &mask, str))
- continue;
- if (mask <= 1)
- continue;
- for (i = 0; i < (ipv6 ? 4 : 1); i++) {
- int cur_mask = mask > 32 ? 32 : mask;
- if (mask > 32)
- mask -= 32;
- else
- mask = 0;
- mask32 = ~0ULL << (32 - cur_mask);
- if (ntohl(subnet32[i] ^ addr32[i]) & mask32)
- continue;
- }
- return true;
- }
- return false;
- }
- bool network_skip_endpoint_route(struct network *net, union network_endpoint *ep)
- {
- struct network_host *host;
- avl_for_each_element(&net->hosts, host, node)
- if (__network_skip_endpoint_route(net, host, ep))
- return true;
- return false;
- }
- static void
- network_do_update(struct network *net, bool up)
- {
- if (!net->net_config.local_host)
- up = false;
- blob_buf_init(&b, 0);
- blobmsg_add_u32(&b, "action", 0);
- blobmsg_add_string(&b, "ifname", network_name(net));
- blobmsg_add_u8(&b, "link-up", up);
- if (up) {
- network_fill_ip_settings(net, &b);
- network_fill_subnets(net, &b);
- }
- if (debug) {
- char *s = blobmsg_format_json(b.head, true);
- D_NET(net, "update: %s", s);
- free(s);
- }
- if (net->config.update_cmd) {
- const char *argv[] = { net->config.update_cmd, NULL, NULL };
- int pid, stat;
- pid = fork();
- if (pid == 0) {
- argv[1] = blobmsg_format_json(b.head, true);
- execvp(argv[0], (char **)argv);
- exit(1);
- }
- waitpid(pid, &stat, 0);
- }
- if (!net->config.interface)
- return;
- blobmsg_add_string(&b, "interface", net->config.interface);
- unetd_ubus_netifd_update(b.head);
- }
- static void network_reload(struct uloop_timeout *t)
- {
- struct network *net = container_of(t, struct network, reload_timer);
- net->prev_local_host = net->net_config.local_host;
- memset(&net->net_config, 0, sizeof(net->net_config));
- network_stun_free(net);
- network_pex_close(net);
- network_services_free(net);
- network_hosts_update_start(net);
- network_services_update_start(net);
- switch (net->config.type) {
- case NETWORK_TYPE_FILE:
- network_load_file(net);
- break;
- case NETWORK_TYPE_INLINE:
- network_load_data(net, net->config.net_data);
- break;
- case NETWORK_TYPE_DYNAMIC:
- network_load_dynamic(net);
- break;
- }
- network_services_update_done(net);
- network_hosts_update_done(net);
- uloop_timeout_set(&net->connect_timer, 10);
- net->prev_local_host = NULL;
- unetd_write_hosts();
- network_do_update(net, true);
- network_pex_open(net);
- network_stun_start(net);
- unetd_ubus_notify(net);
- }
- void network_soft_reload(struct network *net)
- {
- siphash_key_t key = {};
- uint64_t hash;
- if (net->config.type == NETWORK_TYPE_FILE) {
- blob_buf_init(&b, 0);
- if (!blobmsg_add_json_from_file(&b, net->config.file))
- return;
- hash = siphash(b.head, blob_raw_len(b.head), &key);
- if (hash != net->net_config.hash) {
- uloop_timeout_set(&net->reload_timer, 1);
- return;
- }
- }
- network_hosts_reload_dynamic_peers(net);
- }
- static int network_setup(struct network *net)
- {
- if (wg_init_network(net)) {
- fprintf(stderr, "Setup failed for network %s\n", network_name(net));
- return -1;
- }
- net->ifindex = if_nametoindex(network_name(net));
- if (!net->ifindex) {
- fprintf(stderr, "Could not get ifindex for network %s\n", network_name(net));
- return -1;
- }
- return 0;
- }
- static void network_teardown(struct network *net)
- {
- uloop_timeout_cancel(&net->connect_timer);
- uloop_timeout_cancel(&net->reload_timer);
- network_do_update(net, false);
- network_stun_free(net);
- network_pex_close(net);
- network_pex_free(net);
- network_hosts_free(net);
- network_services_free(net);
- wg_cleanup_network(net);
- }
- static void
- network_destroy(struct network *net)
- {
- network_teardown(net);
- avl_delete(&networks, &net->node);
- free(net->net_data);
- free(net->config.data);
- free(net);
- }
- static int
- network_set_config(struct network *net, struct blob_attr *config)
- {
- struct blob_attr *tb[__NETWORK_ATTR_MAX];
- struct blob_attr *cur;
- if (net->config.data && blob_attr_equal(net->config.data, config))
- goto reload;
- network_teardown(net);
- free(net->config.data);
- memset(&net->config, 0, sizeof(net->config));
- net->config.data = blob_memdup(config);
- blobmsg_parse(network_policy, __NETWORK_ATTR_MAX, tb,
- blobmsg_data(net->config.data),
- blobmsg_len(net->config.data));
- if ((cur = tb[NETWORK_ATTR_TYPE]) == NULL ||
- !strlen(blobmsg_get_string(cur)) ||
- !strcmp(blobmsg_get_string(cur), "dynamic"))
- net->config.type = NETWORK_TYPE_DYNAMIC;
- else if (!strcmp(blobmsg_get_string(cur), "file"))
- net->config.type = NETWORK_TYPE_FILE;
- else if (!strcmp(blobmsg_get_string(cur), "inline"))
- net->config.type = NETWORK_TYPE_INLINE;
- else
- goto invalid;
- if ((cur = tb[NETWORK_ATTR_KEEPALIVE]) != NULL)
- net->config.keepalive = blobmsg_get_u32(cur);
- else
- net->config.keepalive = -1;
- switch (net->config.type) {
- case NETWORK_TYPE_FILE:
- if ((cur = tb[NETWORK_ATTR_FILE]) != NULL)
- net->config.file = blobmsg_get_string(cur);
- else
- goto invalid;
- break;
- case NETWORK_TYPE_INLINE:
- net->config.net_data = tb[NETWORK_ATTR_DATA];
- if (!net->config.net_data)
- goto invalid;
- break;
- case NETWORK_TYPE_DYNAMIC:
- if ((cur = tb[NETWORK_ATTR_AUTH_KEY]) == NULL)
- goto invalid;
- if (b64_decode(blobmsg_get_string(cur), net->config.auth_key,
- sizeof(net->config.auth_key)) != sizeof(net->config.auth_key))
- goto invalid;
- break;
- }
- if ((cur = tb[NETWORK_ATTR_INTERFACE]) != NULL &&
- strlen(blobmsg_get_string(cur)) > 0)
- net->config.interface = blobmsg_get_string(cur);
- if ((cur = tb[NETWORK_ATTR_UPDATE_CMD]) != NULL &&
- strlen(blobmsg_get_string(cur)) > 0)
- net->config.update_cmd = blobmsg_get_string(cur);
- if ((cur = tb[NETWORK_ATTR_DOMAIN]) != NULL &&
- strlen(blobmsg_get_string(cur)) > 0)
- net->config.domain = blobmsg_get_string(cur);
- if ((cur = tb[NETWORK_ATTR_TUNNELS]) != NULL)
- net->config.tunnels = cur;
- if ((cur = tb[NETWORK_ATTR_AUTH_CONNECT]) != NULL &&
- blobmsg_check_array(cur, BLOBMSG_TYPE_STRING) > 0)
- net->config.auth_connect = cur;
- if ((cur = tb[NETWORK_ATTR_PEER_DATA]) != NULL &&
- blobmsg_check_array(cur, BLOBMSG_TYPE_STRING) > 0)
- net->config.peer_data = cur;
- if ((cur = tb[NETWORK_ATTR_KEY]) == NULL)
- goto invalid;
- if (b64_decode(blobmsg_get_string(cur), net->config.key, sizeof(net->config.key)) !=
- sizeof(net->config.key))
- goto invalid;
- curve25519_generate_public(net->config.pubkey, net->config.key);
- if (network_setup(net))
- goto invalid;
- reload:
- network_reload(&net->reload_timer);
- return 0;
- invalid:
- network_destroy(net);
- return -1;
- }
- static struct network *
- network_alloc(const char *name)
- {
- struct network *net;
- char *name_buf;
- net = calloc_a(sizeof(*net), &name_buf, strlen(name) + 1);
- net->node.key = strcpy(name_buf, name);
- net->reload_timer.cb = network_reload;
- avl_insert(&networks, &net->node);
- network_pex_init(net);
- network_stun_init(net);
- network_hosts_init(net);
- network_services_init(net);
- return net;
- }
- void network_fill_host_addr(union network_addr *addr, uint8_t *pubkey)
- {
- siphash_key_t key = {
- .key = {
- get_unaligned_le64(addr->network_id),
- get_unaligned_le64(addr->network_id)
- }
- };
- siphash_to_le64(&addr->host_addr, pubkey, CURVE25519_KEY_SIZE, &key);
- }
- int unetd_network_add(const char *name, struct blob_attr *config)
- {
- struct network *net;
- if (strchr(name, '/'))
- return -1;
- net = avl_find_element(&networks, name, net, node);
- if (!net)
- net = network_alloc(name);
- return network_set_config(net, config);
- }
- int unetd_network_remove(const char *name)
- {
- struct network *net;
- net = avl_find_element(&networks, name, net, node);
- if (!net)
- return -1;
- network_destroy(net);
- return 0;
- }
- void network_free_all(void)
- {
- struct network *net, *tmp;
- avl_for_each_element_safe(&networks, net, node, tmp)
- network_destroy(net);
- }
|