From d7df17009e7fe0828a38ce9f6ad107d75c04f5f4 Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Wed, 15 Jun 2022 15:12:25 +0200 Subject: [PATCH] service: add vxlan tunnel support Signed-off-by: Felix Fietkau --- CMakeLists.txt | 2 +- examples/net0.json | 2 + network.c | 13 ++ network.h | 3 + service.c | 36 +++- service.h | 28 +++ unetd.h | 1 + utils.h | 27 +++ vxlan.c | 466 +++++++++++++++++++++++++++++++++++++++++++++ wg.c | 16 +- 10 files changed, 587 insertions(+), 7 deletions(-) create mode 100644 vxlan.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 02fadbd..73c4cfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ FIND_LIBRARY(libjson NAMES json-c json) OPTION(UBUS_SUPPORT "enable ubus support" ON) IF(CMAKE_SYSTEM_NAME STREQUAL "Linux") FIND_LIBRARY(nl nl-tiny) - SET(SOURCES ${SOURCES} wg-linux.c) + SET(SOURCES ${SOURCES} wg-linux.c vxlan.c) ELSE() SET(nl "") ENDIF() diff --git a/examples/net0.json b/examples/net0.json index 567d508..1b96987 100644 --- a/examples/net0.json +++ b/examples/net0.json @@ -31,6 +31,8 @@ "services": { "l2-tunnel": { "type": "vxlan", + "config": { + }, "members": [ "master", "@ap" ] }, "usteer": { diff --git a/network.c b/network.c index 9e9485c..e422954 100644 --- a/network.c +++ b/network.c @@ -3,7 +3,10 @@ * Copyright (C) 2022 Felix Fietkau */ #include +#include +#include #include +#include #include #include #include @@ -48,6 +51,7 @@ const struct blobmsg_policy network_policy[__NETWORK_ATTR_MAX] = { [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 }, }; AVL_TREE(networks, avl_strcmp, false, NULL); @@ -327,6 +331,12 @@ static int network_setup(struct network *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; } @@ -405,6 +415,9 @@ network_set_config(struct network *net, struct blob_attr *config) if ((cur = tb[NETWORK_ATTR_DOMAIN]) != NULL) net->config.domain = blobmsg_get_string(cur); + if ((cur = tb[NETWORK_ATTR_TUNNELS]) != NULL) + net->config.tunnels = cur; + if ((cur = tb[NETWORK_ATTR_KEY]) == NULL) goto invalid; diff --git a/network.h b/network.h index da0a281..ea52be3 100644 --- a/network.h +++ b/network.h @@ -33,6 +33,7 @@ struct network { const char *interface; const char *update_cmd; const char *domain; + struct blob_attr *tunnels; struct blob_attr *net_data; } config; @@ -45,6 +46,7 @@ struct network { bool local_host_changed; } net_config; + int ifindex; struct network_host *prev_local_host; struct avl_tree hosts; struct vlist_tree peers; @@ -67,6 +69,7 @@ enum { NETWORK_ATTR_UPDATE_CMD, NETWORK_ATTR_KEEPALIVE, NETWORK_ATTR_DOMAIN, + NETWORK_ATTR_TUNNELS, __NETWORK_ATTR_MAX, }; diff --git a/service.c b/service.c index f7daacd..29b41a2 100644 --- a/service.c +++ b/service.c @@ -104,6 +104,10 @@ service_add(struct network *net, struct blob_attr *data) s->type = strcpy(type_buf, type); if (config) s->config = memcpy(config_buf, config, blob_pad_len(config)); +#ifdef linux + if (type && !strcmp(type, "vxlan")) + s->ops = &vxlan_ops; +#endif service_parse_members(net, s, tb[SERVICE_ATTR_MEMBERS]); vlist_add(&net->services, &s->node, name_buf); @@ -122,11 +126,37 @@ static void service_update(struct vlist_tree *tree, struct vlist_node *node_new, struct vlist_node *node_old) { - struct network_service *s_old; + struct network *net = container_of(tree, struct network, services); + struct network_service *s_old, *s_new; + s_new = container_of_safe(node_new, struct network_service, node); s_old = container_of_safe(node_old, struct network_service, node); - if (s_old) - free(s_old); + + if (s_new && s_old && s_new->ops && s_new->ops == s_old->ops) { + s_new->ops->init(net, s_new, s_old); + goto out; + } + + if (s_new && s_new->ops) + s_new->ops->init(net, s_new, NULL); + + if (s_old && s_old->ops) + s_old->ops->free(net, s_old); + +out: + free(s_old); +} + +void network_services_peer_update(struct network *net, struct network_peer *peer) +{ + struct network_service *s; + + vlist_for_each_element(&net->services, s, node) { + if (!s->ops || !s->ops->peer_update) + continue; + + s->ops->peer_update(net, s, peer); + } } void network_services_init(struct network *net) diff --git a/service.h b/service.h index b85fbe6..09041d8 100644 --- a/service.h +++ b/service.h @@ -5,19 +5,47 @@ #ifndef __UNETD_SERVICE_H #define __UNETD_SERVICE_H +struct vxlan_tunnel; +struct service_ops; + struct network_service { struct vlist_node node; struct blob_attr *config; + const char *type; + const struct service_ops *ops; + union { + struct vxlan_tunnel *vxlan; + void *priv; + }; + int n_members; struct network_host *members[]; }; +struct service_ops { + void (*init)(struct network *net, + struct network_service *s_new, + struct network_service *s_old); + void (*peer_update)(struct network *net, struct network_service *s, + struct network_peer *peer); + void (*free)(struct network *net, struct network_service *s); +}; + +extern const struct service_ops vxlan_ops; + +static inline const char * +network_service_name(struct network_service *s) +{ + return s->node.avl.key; +} + void network_services_init(struct network *net); void network_services_free(struct network *net); void network_services_add(struct network *net, struct blob_attr *data); +void network_services_peer_update(struct network *net, struct network_peer *peer); static inline void network_services_update_start(struct network *net) { diff --git a/unetd.h b/unetd.h index e7d1360..b6fd437 100644 --- a/unetd.h +++ b/unetd.h @@ -32,6 +32,7 @@ extern bool debug; #define D_NET(net, format, ...) D("network %s " format, network_name(net), ##__VA_ARGS__) #define D_HOST(net, host, format, ...) D_NET(net, "host %s " format, network_host_name(host), ##__VA_ARGS__) #define D_PEER(net, peer, format, ...) D_NET(net, "host %s " format, network_peer_name(peer), ##__VA_ARGS__) +#define D_SERVICE(net, service, format, ...) D_NET(net, "service %s " format, network_service_name(service), ##__VA_ARGS__) void unetd_write_hosts(void); diff --git a/utils.h b/utils.h index eac083e..b024374 100644 --- a/utils.h +++ b/utils.h @@ -55,4 +55,31 @@ int network_get_subnet(int af, union network_addr *addr, int *mask, const char *str); int network_get_local_addr(void *local, const union network_endpoint *target); +#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) + +#define bitmask_size(len) (4 * DIV_ROUND_UP(len, 32)) + +static inline bool bitmask_test(uint32_t *mask, unsigned int i) +{ + return mask[i / 32] & (1 << (i % 32)); +} + +static inline void bitmask_set(uint32_t *mask, unsigned int i) +{ + mask[i / 32] |= 1 << (i % 32); +} + +static inline void bitmask_clear(uint32_t *mask, unsigned int i) +{ + mask[i / 32] &= ~(1 << (i % 32)); +} + +static inline void bitmask_set_val(uint32_t *mask, unsigned int i, bool val) +{ + if (val) + bitmask_set(mask, i); + else + bitmask_clear(mask, i); +} + #endif diff --git a/vxlan.c b/vxlan.c new file mode 100644 index 0000000..38db876 --- /dev/null +++ b/vxlan.c @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2022 Felix Fietkau + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include "unetd.h" + +struct vxlan_tunnel { + struct network *net; + struct network_service *s; + char ifname[IFNAMSIZ + 1]; + int ifindex; + uint16_t mtu; + uint16_t port; + uint32_t vni; + uint32_t *forward_ports; + uint32_t *cur_forward_ports; + bool active; +}; + +static struct nl_sock *rtnl; +static bool ignore_errors; + +static int +unetd_nl_error_cb(struct sockaddr_nl *nla, struct nlmsgerr *err, + void *arg) +{ + struct nlmsghdr *nlh = (struct nlmsghdr *) err - 1; + struct nlattr *tb[NLMSGERR_ATTR_MAX + 1]; + struct nlattr *attrs; + int ack_len = sizeof(*nlh) + sizeof(int) + sizeof(*nlh); + int len = nlh->nlmsg_len; + const char *errstr = "(unknown)"; + + if (ignore_errors) + return NL_STOP; + + if (!(nlh->nlmsg_flags & NLM_F_ACK_TLVS)) + return NL_STOP; + + if (!(nlh->nlmsg_flags & NLM_F_CAPPED)) + ack_len += err->msg.nlmsg_len - sizeof(*nlh); + + attrs = (void *) ((unsigned char *) nlh + ack_len); + len -= ack_len; + + nla_parse(tb, NLMSGERR_ATTR_MAX, attrs, len, NULL); + if (tb[NLMSGERR_ATTR_MSG]) + errstr = nla_data(tb[NLMSGERR_ATTR_MSG]); + + D("Netlink error(%d): %s\n", err->error, errstr); + + return NL_STOP; +} + +static struct nl_msg *vxlan_rtnl_msg(const char *ifname, int type, int flags) +{ + struct ifinfomsg iim = { + .ifi_family = AF_UNSPEC, + }; + struct nl_msg *msg; + + msg = nlmsg_alloc_simple(type, flags | NLM_F_REQUEST); + if (!msg) + return NULL; + + nlmsg_append(msg, &iim, sizeof(iim), 0); + nla_put_string(msg, IFLA_IFNAME, ifname); + + return msg; +} + +static int vxlan_rtnl_call(struct nl_msg *msg) +{ + int ret; + + ret = nl_send_auto_complete(rtnl, msg); + nlmsg_free(msg); + + if (ret < 0) + return ret; + + return nl_wait_for_ack(rtnl); +} + +static int +vxlan_rtnl_init(void) +{ + int fd, opt; + + if (rtnl) + return 0; + + rtnl = nl_socket_alloc(); + if (!rtnl) + return -1; + + if (nl_connect(rtnl, NETLINK_ROUTE)) + goto free; + + nl_socket_disable_seq_check(rtnl); + nl_socket_set_buffer_size(rtnl, 65536, 0); + nl_cb_err(nl_socket_get_cb(rtnl), NL_CB_CUSTOM, unetd_nl_error_cb, NULL); + + fd = nl_socket_get_fd(rtnl); + + opt = 1; + setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, &opt, sizeof(opt)); + + opt = 1; + setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &opt, sizeof(opt)); + + return 0; + +free: + nl_socket_free(rtnl); + rtnl = NULL; + return -1; +} + +static uint32_t +vxlan_tunnel_id(struct vxlan_tunnel *vt) +{ + siphash_key_t key = {}; + const char *name = network_service_name(vt->s); + uint64_t val; + + if (vt->vni != ~0) + return vt->vni; + + siphash_to_le64(&val, name, strlen(name), &key); + + return val & 0x00ffffff; +} + +static int +vxlan_update_host_fdb_entry(struct vxlan_tunnel *vt, struct network_host *host, bool add) +{ + struct ndmsg ndmsg = { + .ndm_family = PF_BRIDGE, + .ndm_state = NUD_NOARP | NUD_PERMANENT, + .ndm_flags = NTF_SELF, + .ndm_ifindex = vt->ifindex, + }; + unsigned int flags = NLM_F_REQUEST; + uint8_t lladdr[ETH_ALEN] = {}; + struct nl_msg *msg; + + if (add) + flags |= NLM_F_CREATE | NLM_F_APPEND; + + msg = nlmsg_alloc_simple(add ? RTM_NEWNEIGH : RTM_DELNEIGH, flags); + nlmsg_append(msg, &ndmsg, sizeof(ndmsg), 0); + nla_put(msg, NDA_LLADDR, ETH_ALEN, lladdr); + nla_put(msg, NDA_DST, sizeof(struct in6_addr), &host->peer.local_addr); + nla_put_u32(msg, NDA_IFINDEX, vt->net->ifindex); + + return vxlan_rtnl_call(msg); +} + +static void +vxlan_update_fdb_hosts(struct vxlan_tunnel *vt) +{ + struct network_service *s = vt->s; + bool active; + int i; + + if (!vt->active) + return; + + for (i = 0; i < s->n_members; i++) { + if (s->members[i] == vt->net->net_config.local_host) + continue; + + if (vt->forward_ports && !bitmask_test(vt->forward_ports, i)) + continue; + + active = s->members[i]->peer.state.connected; + if (active == bitmask_test(vt->cur_forward_ports, i)) + continue; + + if (!vxlan_update_host_fdb_entry(vt, s->members[i], active)) + bitmask_set_val(vt->cur_forward_ports, i, active); + } +} + +static void +vxlan_peer_update(struct network *net, struct network_service *s, struct network_peer *peer) +{ + if (!s->vxlan) + return; + + vxlan_update_fdb_hosts(s->vxlan); +} + +static void +vxlan_tunnel_init(struct vxlan_tunnel *vt) +{ + struct network_peer *local = &vt->net->net_config.local_host->peer; + struct nlattr *linkinfo, *data; + struct nl_msg *msg; + + if (vxlan_rtnl_init()) + return; + + msg = vxlan_rtnl_msg(vt->ifname, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL); + + linkinfo = nla_nest_start(msg, IFLA_LINKINFO); + nla_put_string(msg, IFLA_INFO_KIND, "vxlan"); + nla_put_u32(msg, IFLA_MTU, vt->mtu); + + data = nla_nest_start(msg, IFLA_INFO_DATA); + nla_put_u32(msg, IFLA_VXLAN_ID, vxlan_tunnel_id(vt)); + nla_put(msg, IFLA_VXLAN_LOCAL6, sizeof(struct in6_addr), &local->local_addr); + nla_put_u16(msg, IFLA_VXLAN_PORT, vt->port); + nla_put_u8(msg, IFLA_VXLAN_LEARNING, 1); + nla_nest_end(msg, data); + + nla_nest_end(msg, linkinfo); + + if (vxlan_rtnl_call(msg) < 0) + return; + + vt->ifindex = if_nametoindex(vt->ifname); + if (!vt->ifindex) { + D_SERVICE(vt->net, vt->s, "failed to get ifindex for device %s", vt->ifname); + return; + } + + vt->active = true; + vxlan_update_fdb_hosts(vt); +} + +static void +vxlan_tunnel_teardown(struct vxlan_tunnel *vt) +{ + struct nl_msg *msg; + + if (!rtnl) + return; + + vt->active = false; + msg = vxlan_rtnl_msg(vt->ifname, RTM_DELLINK, 0); + vxlan_rtnl_call(msg); +} + +static const char * +vxlan_find_ifname(struct network *net, const char *service) +{ + struct blob_attr *cur; + int rem; + + if (!net->config.tunnels) + return NULL; + + blobmsg_for_each_attr(cur, net->config.tunnels, rem) { + const char *name; + + if (!blobmsg_check_attr(cur, true) || + blobmsg_type(cur) != BLOBMSG_TYPE_STRING) + continue; + + if (strcmp(blobmsg_get_string(cur), service) != 0) + continue; + + name = blobmsg_name(cur); + if (strlen(name) > IFNAMSIZ) + break; + + return name; + } + + return NULL; +} + +static void +__vxlan_mark_forward_host(struct vxlan_tunnel *vt, struct network_host *host) +{ + struct network_service *s = vt->s; + unsigned int i; + + for (i = 0; i < s->n_members; i++) { + if (s->members[i] != host) + continue; + + bitmask_set(vt->forward_ports, i); + break; + } +} + +static void +vxlan_mark_forward_host(struct vxlan_tunnel *vt, const char *name) +{ + struct network *net = vt->net; + struct network_host *host; + + host = avl_find_element(&net->hosts, name, host, node); + if (!host) + return; + + __vxlan_mark_forward_host(vt, host); +} + +static void +vxlan_mark_forward_group(struct vxlan_tunnel *vt, const char *name) +{ + struct network *net = vt->net; + struct network_group *group; + int i; + + group = avl_find_element(&net->groups, name, group, node); + if (!group) + return; + + for (i = 0; i < group->n_members; i++) + __vxlan_mark_forward_host(vt, group->members[i]); +} + +static void +vxlan_init_forward_ports(struct vxlan_tunnel *vt, struct blob_attr *data) +{ + unsigned int len = bitmask_size(vt->s->n_members); + struct blob_attr *cur; + int rem; + + vt->cur_forward_ports = realloc(vt->cur_forward_ports, len); + memset(vt->cur_forward_ports, 0, len); + + if (!data || blobmsg_check_array(data, BLOBMSG_TYPE_STRING) <= 0) { + free(vt->forward_ports); + vt->forward_ports = NULL; + return; + } + + vt->forward_ports = realloc(vt->forward_ports, len); + memset(vt->forward_ports, 0, len); + blobmsg_for_each_attr(cur, data, rem) { + const char *name = blobmsg_get_string(cur); + + if (name[0] == '@') + vxlan_mark_forward_group(vt, name + 1); + else + vxlan_mark_forward_host(vt, name); + } +} + +static bool +vxlan_config_equal(struct network_service *s1, struct network_service *s2) +{ + int i; + + if (!blob_attr_equal(s1->config, s2->config)) + return false; + + if (s1->n_members != s2->n_members) + return false; + + for (i = 0; i < s1->n_members; i++) + if (memcmp(s1->members[i]->peer.key, s2->members[i]->peer.key, + CURVE25519_KEY_SIZE) != 0) + return false; + + return true; +} + +static void +vxlan_init(struct network *net, struct network_service *s, + struct network_service *s_old) +{ + enum { + VXCFG_ATTR_FWD_PORTS, + VXCFG_ATTR_ID, + VXCFG_ATTR_PORT, + VXCFG_ATTR_MTU, + __VXCFG_ATTR_MAX + }; + static const struct blobmsg_policy policy[__VXCFG_ATTR_MAX] = { + [VXCFG_ATTR_FWD_PORTS] = { "forward_ports", BLOBMSG_TYPE_ARRAY }, + [VXCFG_ATTR_ID] = { "id", BLOBMSG_TYPE_INT32 }, + [VXCFG_ATTR_PORT] = { "port", BLOBMSG_TYPE_INT32 }, + [VXCFG_ATTR_MTU] = { "mtu", BLOBMSG_TYPE_INT32 }, + }; + struct blob_attr *tb[__VXCFG_ATTR_MAX] = {}; + struct blob_attr *cur; + struct vxlan_tunnel *vt = s->vxlan; + const char *name; + + if (s_old) { + vt = s_old->vxlan; + s_old->vxlan = NULL; + if (!vt) + return; + + if (vxlan_config_equal(s, s_old)) { + s->vxlan = vt; + vt->s = s; + return; + } + + vxlan_tunnel_teardown(vt); + goto init; + } + + name = vxlan_find_ifname(net, network_service_name(s)); + if (!name) { + D_SERVICE(net, s, "no configured tunnel ifname"); + return; + } + + vt = calloc(1, sizeof(*s->vxlan)); + snprintf(vt->ifname, sizeof(vt->ifname), "%s", name); + vt->net = net; + +init: + s->vxlan = vt; + vt->s = s; + if (s->config) + blobmsg_parse(policy, __VXCFG_ATTR_MAX, tb, blobmsg_data(s->config), + blobmsg_len(s->config)); + + vxlan_init_forward_ports(vt, tb[VXCFG_ATTR_FWD_PORTS]); + if ((cur = tb[VXCFG_ATTR_ID]) != NULL) + vt->vni = blobmsg_get_u32(cur) & 0x00ffffff; + else + vt->vni = ~0; + + if ((cur = tb[VXCFG_ATTR_PORT]) != NULL) + vt->port = blobmsg_get_u32(cur); + else + vt->port = 4789; + + if ((cur = tb[VXCFG_ATTR_MTU]) != NULL) + vt->mtu = blobmsg_get_u32(cur); + else + vt->mtu = 1500; + + vxlan_tunnel_init(vt); +} + +static void +vxlan_free(struct network *net, struct network_service *s) +{ + struct vxlan_tunnel *vt = s->vxlan; + + if (!vt) + return; + + vxlan_tunnel_teardown(vt); + s->vxlan = NULL; + free(vt->forward_ports); + free(vt); +} + +const struct service_ops vxlan_ops = { + .init = vxlan_init, + .free = vxlan_free, + .peer_update = vxlan_peer_update, +}; diff --git a/wg.c b/wg.c index be8e0cb..ea51990 100644 --- a/wg.c +++ b/wg.c @@ -35,6 +35,16 @@ void wg_cleanup_network(struct network *net) net->wg.ops->cleanup(net); } +static void +wg_peer_set_connected(struct network *net, struct network_peer *peer, bool val) +{ + if (peer->state.connected == val) + return; + + peer->state.connected = val; + network_services_peer_update(net, peer); +} + struct network_peer *wg_peer_update_start(struct network *net, const uint8_t *key) { struct network_peer *peer; @@ -46,7 +56,7 @@ struct network_peer *wg_peer_update_start(struct network *net, const uint8_t *ke peer->state.handshake = false; peer->state.idle++; if (peer->state.idle >= 2 * net->net_config.keepalive) - peer->state.connected = false; + wg_peer_set_connected(net, peer, false); if (peer->state.idle > net->net_config.keepalive) network_pex_event(net, peer, PEX_EV_PING); @@ -69,9 +79,9 @@ void wg_peer_set_last_handshake(struct network *net, struct network_peer *peer, peer->state.last_handshake = sec; sec = now - sec; if (sec <= net->net_config.keepalive) { - peer->state.connected = true; if (peer->state.idle > sec) peer->state.idle = sec; + wg_peer_set_connected(net, peer, true); } } @@ -83,7 +93,7 @@ void wg_peer_set_rx_bytes(struct network *net, struct network_peer *peer, peer->state.rx_bytes = bytes; if (diff > 0) { peer->state.idle = 0; - peer->state.connected = true; + wg_peer_set_connected(net, peer, true); } } -- 2.30.2