diff options
author | Steven Barth <steven@midlink.org> | 2013-10-14 08:37:38 +0200 |
---|---|---|
committer | Steven Barth <steven@midlink.org> | 2013-10-14 08:37:38 +0200 |
commit | 8b19de666e02f24280728938dd985ecb3673b83e (patch) | |
tree | f378f10e87dea4d9b143f8e3f983c2c01efba7e5 /src |
Initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/config.c | 561 | ||||
-rw-r--r-- | src/dhcpv4.c | 508 | ||||
-rw-r--r-- | src/dhcpv4.h | 93 | ||||
-rw-r--r-- | src/dhcpv6-ia.c | 893 | ||||
-rw-r--r-- | src/dhcpv6.c | 452 | ||||
-rw-r--r-- | src/dhcpv6.h | 162 | ||||
-rw-r--r-- | src/md5.c | 242 | ||||
-rw-r--r-- | src/md5.h | 17 | ||||
-rw-r--r-- | src/ndp.c | 532 | ||||
-rw-r--r-- | src/ndp.h | 31 | ||||
-rw-r--r-- | src/odhcpd.c | 423 | ||||
-rw-r--r-- | src/odhcpd.h | 205 | ||||
-rw-r--r-- | src/router.c | 502 | ||||
-rw-r--r-- | src/router.h | 40 | ||||
-rw-r--r-- | src/ubus.c | 314 |
15 files changed, 4975 insertions, 0 deletions
diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..bc85603 --- /dev/null +++ b/src/config.c @@ -0,0 +1,561 @@ +#include <resolv.h> +#include <signal.h> +#include <arpa/inet.h> + +#include <uci.h> +#include <uci_blob.h> + +#include "odhcpd.h" + +static struct blob_buf b; +struct list_head leases = LIST_HEAD_INIT(leases); +struct list_head interfaces = LIST_HEAD_INIT(interfaces); +struct config config = {false, NULL, NULL}; + +enum { + IFACE_ATTR_INTERFACE, + IFACE_ATTR_IFNAME, + IFACE_ATTR_DYNAMICDHCP, + IFACE_ATTR_IGNORE, + IFACE_ATTR_LEASETIME, + IFACE_ATTR_LIMIT, + IFACE_ATTR_START, + IFACE_ATTR_MASTER, + IFACE_ATTR_UPSTREAM, + IFACE_ATTR_RA, + IFACE_ATTR_DHCPV4, + IFACE_ATTR_DHCPV6, + IFACE_ATTR_NDPROXY, + IFACE_ATTR_DNS, + IFACE_ATTR_DOMAIN, + IFACE_ATTR_ULA_COMPAT, + IFACE_ATTR_RA_DEFAULT, + IFACE_ATTR_RA_MANAGEMENT, + IFACE_ATTR_RA_OFFLINK, + IFACE_ATTR_RA_PREFERENCE, + IFACE_ATTR_NDPROXY_ROUTING, + IFACE_ATTR_NDPROXY_SLAVE, + IFACE_ATTR_NDPROXY_STATIC, + IFACE_ATTR_MAX +}; + +static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = { + [IFACE_ATTR_INTERFACE] = { .name = "interface", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_IFNAME] = { .name = "ifname", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_DYNAMICDHCP] = { .name = "dynamicdhcp", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_IGNORE] = { .name = "ignore", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_LEASETIME] = { .name = "leasetime", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_START] = { .name = "start", .type = BLOBMSG_TYPE_INT32 }, + [IFACE_ATTR_LIMIT] = { .name = "limit", .type = BLOBMSG_TYPE_INT32 }, + [IFACE_ATTR_MASTER] = { .name = "master", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_UPSTREAM] = { .name = "upstream", .type = BLOBMSG_TYPE_ARRAY }, + [IFACE_ATTR_RA] = { .name = "ra", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_DHCPV4] = { .name = "dhcpv4", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_DHCPV6] = { .name = "dhcpv6", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_NDPROXY] = { .name = "ndproxy", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_DNS] = { .name = "dns", .type = BLOBMSG_TYPE_ARRAY }, + [IFACE_ATTR_DOMAIN] = { .name = "domain", .type = BLOBMSG_TYPE_ARRAY }, + [IFACE_ATTR_ULA_COMPAT] = { .name = "ula_compat", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_RA_DEFAULT] = { .name = "ra_default", .type = BLOBMSG_TYPE_INT32 }, + [IFACE_ATTR_RA_MANAGEMENT] = { .name = "ra_management", .type = BLOBMSG_TYPE_INT32 }, + [IFACE_ATTR_RA_OFFLINK] = { .name = "ra_offlink", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_RA_PREFERENCE] = { .name = "ra_preference", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_NDPROXY_ROUTING] = { .name = "ndproxy_routing", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_NDPROXY_SLAVE] = { .name = "ndproxy_slave", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_NDPROXY_STATIC] = { .name = "ndproxy_static", .type = BLOBMSG_TYPE_ARRAY }, +}; + +static const struct uci_blob_param_info iface_attr_info[IFACE_ATTR_MAX] = { + [IFACE_ATTR_UPSTREAM] = { .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_DNS] = { .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_DOMAIN] = { .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_NDPROXY_STATIC] = { .type = BLOBMSG_TYPE_STRING }, +}; + +const struct uci_blob_param_list interface_attr_list = { + .n_params = IFACE_ATTR_MAX, + .params = iface_attrs, + .info = iface_attr_info, +}; + + +enum { + LEASE_ATTR_IP, + LEASE_ATTR_MAC, + LEASE_ATTR_DUID, + LEASE_ATTR_HOSTID, + LEASE_ATTR_HOSTNAME, + LEASE_ATTR_MAX +}; + + +static const struct blobmsg_policy lease_attrs[LEASE_ATTR_MAX] = { + [LEASE_ATTR_IP] = { .name = "ip", .type = BLOBMSG_TYPE_STRING }, + [LEASE_ATTR_MAC] = { .name = "mac", .type = BLOBMSG_TYPE_STRING }, + [LEASE_ATTR_DUID] = { .name = "duid", .type = BLOBMSG_TYPE_STRING }, + [LEASE_ATTR_HOSTID] = { .name = "hostid", .type = BLOBMSG_TYPE_STRING }, + [LEASE_ATTR_HOSTNAME] = { .name = "hostname", .type = BLOBMSG_TYPE_STRING }, +}; + + +const struct uci_blob_param_list lease_attr_list = { + .n_params = LEASE_ATTR_MAX, + .params = lease_attrs, +}; + + +enum { + ODHCPD_ATTR_LEGACY, + ODHCPD_ATTR_LEASEFILE, + ODHCPD_ATTR_LEASETRIGGER, + ODHCPD_ATTR_MAX +}; + + +static const struct blobmsg_policy odhcpd_attrs[LEASE_ATTR_MAX] = { + [ODHCPD_ATTR_LEGACY] = { .name = "legacy", .type = BLOBMSG_TYPE_BOOL }, + [ODHCPD_ATTR_LEASEFILE] = { .name = "leasefile", .type = BLOBMSG_TYPE_STRING }, + [ODHCPD_ATTR_LEASETRIGGER] = { .name = "leasetrigger", .type = BLOBMSG_TYPE_STRING }, +}; + + +const struct uci_blob_param_list odhcpd_attr_list = { + .n_params = ODHCPD_ATTR_MAX, + .params = odhcpd_attrs, +}; + + +static struct interface* get_interface(const char *name) +{ + struct interface *c; + list_for_each_entry(c, &interfaces, head) + if (!strcmp(c->name, name)) + return c; + return NULL; +} + + +static void clean_interface(struct interface *iface) +{ + free(iface->dns); + free(iface->search); + free(iface->upstream); + free(iface->static_ndp); + free(iface->dhcpv4_dns); + memset(&iface->ra, 0, sizeof(*iface) - offsetof(struct interface, ra)); +} + + +static void close_interface(struct interface *iface) +{ + if (iface->head.next) + list_del(&iface->head); + + setup_router_interface(iface, false); + setup_dhcpv6_interface(iface, false); + setup_ndp_interface(iface, false); + setup_dhcpv4_interface(iface, false); + + clean_interface(iface); + free(iface); +} + + +static int parse_mode(const char *mode) +{ + if (!strcmp(mode, "disabled")) { + return RELAYD_DISABLED; + } else if (!strcmp(mode, "server")) { + return RELAYD_SERVER; + } else if (!strcmp(mode, "relay")) { + return RELAYD_RELAY; + } else if (!strcmp(mode, "hybrid")) { + return RELAYD_HYBRID; + } else { + return -1; + } +} + + +static void set_config(struct uci_section *s) +{ + struct blob_attr *tb[ODHCPD_ATTR_MAX], *c; + + blob_buf_init(&b, 0); + uci_to_blob(&b, s, &lease_attr_list); + blobmsg_parse(lease_attrs, ODHCPD_ATTR_MAX, tb, blob_data(b.head), blob_len(b.head)); + + if ((c = tb[ODHCPD_ATTR_LEGACY])) + config.legacy = blobmsg_get_bool(c); + + if ((c = tb[ODHCPD_ATTR_LEASEFILE])) { + free(config.dhcp_statefile); + config.dhcp_statefile = strdup(blobmsg_get_string(c)); + } + + if ((c = tb[ODHCPD_ATTR_LEASETRIGGER])) { + free(config.dhcp_cb); + config.dhcp_cb = strdup(blobmsg_get_string(c)); + } +} + + +static int set_lease(struct uci_section *s) +{ + struct blob_attr *tb[LEASE_ATTR_MAX], *c; + + blob_buf_init(&b, 0); + uci_to_blob(&b, s, &lease_attr_list); + blobmsg_parse(lease_attrs, LEASE_ATTR_MAX, tb, blob_data(b.head), blob_len(b.head)); + + size_t hostlen = 1; + if ((c = tb[LEASE_ATTR_HOSTNAME])) + hostlen = blobmsg_data_len(c); + + struct lease *lease = calloc(1, sizeof(*lease) + hostlen); + + if (hostlen > 1) + memcpy(lease->hostname, blobmsg_get_string(c), hostlen); + + if ((c = tb[LEASE_ATTR_IP])) + if (inet_pton(AF_INET, blobmsg_get_string(c), &lease->ipaddr) < 0) + goto err; + + if ((c = tb[LEASE_ATTR_MAC])) + if (!ether_aton_r(blobmsg_get_string(c), &lease->mac)) + goto err; + + if ((c = tb[LEASE_ATTR_DUID])) { + size_t duidlen = (blobmsg_data_len(c) - 1) / 2; + lease->duid = malloc(duidlen); + ssize_t len = odhcpd_unhexlify(lease->duid, + duidlen, blobmsg_get_string(c)); + + if (len < 0) + goto err; + + lease->duid_len = len; + } + + if ((c = tb[LEASE_ATTR_HOSTID])) + if (odhcpd_unhexlify((uint8_t*)&lease->hostid, sizeof(lease->hostid), + blobmsg_get_string(c)) < 0) + goto err; + + list_add(&lease->head, &leases); + return 0; + +err: + free(lease->duid); + free(lease); + return -1; +} + + +int config_parse_interface(struct blob_attr *b, const char *name) +{ + bool overwrite = !!name; + struct blob_attr *tb[IFACE_ATTR_MAX], *c; + blobmsg_parse(iface_attrs, IFACE_ATTR_MAX, tb, blob_data(b), blob_len(b)); + + if (tb[IFACE_ATTR_INTERFACE]) + name = blobmsg_data(tb[IFACE_ATTR_INTERFACE]); + + struct interface *iface = get_interface(name); + if (!iface) { + iface = calloc(1, sizeof(*iface)); + strncpy(iface->name, name, sizeof(iface->name) - 1); + list_add(&iface->head, &interfaces); + } else { + clean_interface(iface); + } + + const char *ifname = NULL; +#ifdef WITH_UBUS + if (overwrite) + ifname = ubus_get_ifname(name); +#endif + if ((c = tb[IFACE_ATTR_IFNAME])) + ifname = blobmsg_get_string(c); + + strncpy(iface->ifname, ifname, sizeof(iface->ifname) - 1); + iface->inuse = true; + + if (overwrite) + clean_interface(iface); + + if ((c = tb[IFACE_ATTR_DYNAMICDHCP])) + iface->no_dynamic_dhcp = !blobmsg_get_bool(c); + + if ((c = tb[IFACE_ATTR_IGNORE])) + iface->ignore = blobmsg_get_bool(c); + + if ((c = tb[IFACE_ATTR_LEASETIME])) { + char *val = blobmsg_get_string(c), *endptr; + double time = strtod(val, &endptr); + if (time && endptr[0]) { + if (endptr[0] == 's') + time *= 1; + else if (endptr[0] == 'm') + time *= 60; + else if (endptr[0] == 'h') + time *= 3600; + else if (endptr[0] == 'd') + time *= 24 * 3600; + else if (endptr[0] == 'w') + time *= 7 * 24 * 3600; + else + goto err; + } + + if (time >= 60) + iface->dhcpv4_leasetime = time; + } + + if ((c = tb[IFACE_ATTR_START])) { + iface->dhcpv4_start.s_addr = htonl(blobmsg_get_u32(c)); + + if (config.legacy) + iface->dhcpv4 = RELAYD_SERVER; + } + + if ((c = tb[IFACE_ATTR_LIMIT])) + iface->dhcpv4_end.s_addr = htonl( + ntohl(iface->dhcpv4_start.s_addr) + blobmsg_get_u32(c)); + + if ((c = tb[IFACE_ATTR_MASTER])) + iface->master = blobmsg_get_bool(c); + + if ((c = tb[IFACE_ATTR_UPSTREAM])) { + struct blob_attr *cur; + int rem; + + blobmsg_for_each_attr(cur, c, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL)) + continue; + + iface->upstream = realloc(iface->upstream, + iface->upstream_len + blobmsg_data_len(cur)); + memcpy(iface->upstream + iface->upstream_len, blobmsg_get_string(cur), blobmsg_data_len(cur)); + iface->upstream_len += blobmsg_data_len(cur); + } + } + + if ((c = tb[IFACE_ATTR_RA])) + if ((iface->ra = parse_mode(blobmsg_get_string(c))) < 0) + goto err; + + if ((c = tb[IFACE_ATTR_DHCPV4])) + if ((iface->dhcpv4 = parse_mode(blobmsg_get_string(c))) < 0) + goto err; + + if ((c = tb[IFACE_ATTR_DHCPV6])) + if ((iface->dhcpv6 = parse_mode(blobmsg_get_string(c))) < 0) + goto err; + + if ((c = tb[IFACE_ATTR_NDPROXY])) + iface->ndp = blobmsg_get_bool(c) ? RELAYD_RELAY : RELAYD_DISABLED; + + if ((c = tb[IFACE_ATTR_DNS])) { + struct blob_attr *cur; + int rem; + + blobmsg_for_each_attr(cur, c, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL)) + continue; + + struct in_addr addr4; + struct in6_addr addr6; + if (inet_pton(AF_INET, blobmsg_get_string(cur), &addr4) == 1) { + iface->dhcpv4_dns = realloc(iface->dhcpv4_dns, + (++iface->dhcpv4_dns_cnt) * sizeof(*iface->dhcpv4_dns)); + iface->dhcpv4_dns[iface->dhcpv4_dns_cnt - 1] = addr4; + } else if (inet_pton(AF_INET6, blobmsg_get_string(cur), &addr6) == 1) { + iface->dns = realloc(iface->dns, + (++iface->dns_cnt) * sizeof(*iface->dns)); + iface->dns[iface->dns_cnt - 1] = addr6; + } else { + goto err; + } + } + } + + if ((c = tb[IFACE_ATTR_DOMAIN])) { + struct blob_attr *cur; + int rem; + + blobmsg_for_each_attr(cur, c, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL)) + continue; + + uint8_t buf[256]; + int len = dn_comp(blobmsg_get_string(cur), buf, sizeof(buf), NULL, NULL); + if (len <= 0) + goto err; + + iface->search = realloc(iface->search, iface->search_len + len); + memcpy(&iface->search[iface->search_len], buf, len); + iface->search_len += len; + } + } + + if ((c = tb[IFACE_ATTR_ULA_COMPAT])) + iface->deprecate_ula_if_public_avail = blobmsg_get_bool(c); + + if ((c = tb[IFACE_ATTR_RA_DEFAULT])) + iface->default_router = blobmsg_get_u32(c); + + if ((c = tb[IFACE_ATTR_RA_MANAGEMENT])) + iface->managed = blobmsg_get_u32(c); + + if ((c = tb[IFACE_ATTR_RA_OFFLINK])) + iface->ra_not_onlink = blobmsg_get_bool(c); + + if ((c = tb[IFACE_ATTR_RA_PREFERENCE])) { + const char *prio = blobmsg_get_string(c); + + if (!strcmp(prio, "high")) + iface->route_preference = 1; + else if (!strcmp(prio, "low")) + iface->route_preference = -1; + else if (!strcmp(prio, "medium") || !strcmp(prio, "default")) + iface->route_preference = 0; + else + goto err; + } + + if ((c = tb[IFACE_ATTR_NDPROXY_ROUTING])) + iface->learn_routes = blobmsg_get_bool(c); + + if ((c = tb[IFACE_ATTR_NDPROXY_SLAVE])) + iface->external = blobmsg_get_bool(c); + + if ((c = tb[IFACE_ATTR_NDPROXY_STATIC])) { + struct blob_attr *cur; + int rem; + + blobmsg_for_each_attr(cur, c, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL)) + continue; + + int len = blobmsg_data_len(cur); + iface->static_ndp = realloc(iface->static_ndp, iface->static_ndp_len + len); + memcpy(&iface->static_ndp[iface->static_ndp_len], blobmsg_get_string(cur), len); + iface->static_ndp_len += len; + } + } + + iface->ignore = (iface->ifindex = if_nametoindex(iface->ifname)) < 0; + return 0; + +err: + close_interface(iface); + return -1; +} + +static int set_interface(struct uci_section *s) +{ + blob_buf_init(&b, 0); + uci_to_blob(&b, s, &interface_attr_list); + return config_parse_interface(b.head, s->e.name); +} + + +static volatile bool do_reload = false; +static void set_stop(int signal) +{ + uloop_end(); + do_reload = (signal == SIGHUP); +} + +void odhcpd_run(void) +{ + struct uci_context *uci = uci_alloc_context(); + signal(SIGTERM, set_stop); + signal(SIGHUP, set_stop); + signal(SIGINT, set_stop); + + do { + do_reload = false; + + struct lease *l; + list_for_each_entry(l, &leases, head) { + list_del(&l->head); + free(l->duid); + free(l); + } + + struct uci_package *dhcp = NULL; + if (!uci_load(uci, "dhcp", &dhcp)) { + struct uci_element *e; + uci_foreach_element(&dhcp->sections, e) { + struct uci_section *s = uci_to_section(e); + if (!strcmp(s->type, "lease")) + set_lease(s); + else if (!strcmp(s->type, "odhcpd")) + set_config(s); + } + + uci_foreach_element(&dhcp->sections, e) { + struct uci_section *s = uci_to_section(e); + if (!strcmp(s->type, "dhcp")) + set_interface(s); + } + } + +#ifdef WITH_UBUS + ubus_apply_network(); +#endif + + // Evaluate hybrid mode for master + struct interface *master = NULL, *i; + list_for_each_entry(i, &interfaces, head) { + if (!i->master) + continue; + + enum odhcpd_mode hybrid_mode = RELAYD_DISABLED; + if (i->dhcpv6 == RELAYD_HYBRID) + i->dhcpv6 = hybrid_mode; + + if (i->ra == RELAYD_HYBRID) + i->ra = hybrid_mode; + + if (i->ndp == RELAYD_HYBRID) + i->ndp = hybrid_mode; + + if (i->dhcpv6 == RELAYD_RELAY || i->ra == RELAYD_RELAY || i->ndp == RELAYD_RELAY) + master = i; + } + + + list_for_each_entry(i, &interfaces, head) { + if (i->inuse && !i->ignore) { + // Resolve hybrid mode + if (i->dhcpv6 == RELAYD_HYBRID) + i->dhcpv6 = (master && master->dhcpv6 == RELAYD_RELAY) ? + RELAYD_RELAY : RELAYD_SERVER; + + if (i->ra == RELAYD_HYBRID) + i->ra = (master && master->ra == RELAYD_RELAY) ? + RELAYD_RELAY : RELAYD_SERVER; + + if (i->ndp == RELAYD_HYBRID) + i->ndp = (master && master->ndp == RELAYD_RELAY) ? + RELAYD_RELAY : RELAYD_SERVER; + + setup_router_interface(i, true); + setup_dhcpv6_interface(i, true); + setup_ndp_interface(i, true); + setup_dhcpv4_interface(i, true); + } else { + close_interface(i); + } + } + + uloop_run(); + + if (dhcp) + uci_unload(uci, dhcp); + } while (do_reload); +} + diff --git a/src/dhcpv4.c b/src/dhcpv4.c new file mode 100644 index 0000000..38b4f9b --- /dev/null +++ b/src/dhcpv4.c @@ -0,0 +1,508 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * + */ + +#include <time.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <stddef.h> +#include <stdlib.h> +#include <resolv.h> +#include <net/if.h> +#include <net/if_arp.h> +#include <netinet/ip.h> +#include <sys/ioctl.h> +#include <sys/timerfd.h> +#include <arpa/inet.h> + +#include "odhcpd.h" +#include "dhcpv4.h" +#include "dhcpv6.h" + + +static void handle_dhcpv4(void *addr, void *data, size_t len, + struct interface *iface); +static struct dhcpv4_assignment* dhcpv4_lease(struct interface *iface, + enum dhcpv4_msg msg, const uint8_t *mac, struct in_addr reqaddr, + const char *hostname); + + +// Create socket and register events +int init_dhcpv4(void) +{ + return 0; +} + + +int setup_dhcpv4_interface(struct interface *iface, bool enable) +{ + if (iface->dhcpv4_event.uloop.fd > 0) { + close(iface->dhcpv4_event.uloop.fd); + iface->dhcpv4_event.uloop.fd = -1; + } + + if (iface->dhcpv4 && enable) { + if (!iface->dhcpv4_assignments.next) + INIT_LIST_HEAD(&iface->dhcpv4_assignments); + + int sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP); + + // Basic IPv6 configuration + int val = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); + setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val)); + setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &val, sizeof(val)); + + val = IPTOS_CLASS_CS6; + setsockopt(sock, IPPROTO_IP, IP_TOS, &val, sizeof(val)); + + val = IP_PMTUDISC_DONT; + setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); + + setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, + iface->ifname, strlen(iface->ifname)); + + struct sockaddr_in bind_addr = {AF_INET, htons(DHCPV4_SERVER_PORT), + {INADDR_ANY}, {0}}; + + if (bind(sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr))) { + syslog(LOG_ERR, "Failed to open DHCPv4 server socket: %s", + strerror(errno)); + return -1; + } + + + if (ntohl(iface->dhcpv4_start.s_addr) > ntohl(iface->dhcpv4_end.s_addr)) { + syslog(LOG_ERR, "Invalid DHCP range"); + return -1; + } + + // Create a range if not specified + struct ifreq ifreq; + strncpy(ifreq.ifr_name, iface->ifname, sizeof(ifreq.ifr_name)); + + struct sockaddr_in *saddr = (struct sockaddr_in*)&ifreq.ifr_addr; + struct sockaddr_in *smask = (struct sockaddr_in*)&ifreq.ifr_netmask; + if (!(iface->dhcpv4_start.s_addr & htonl(0xffff0000)) && + !(iface->dhcpv4_end.s_addr & htonl(0xffff0000)) && + !ioctl(sock, SIOCGIFADDR, &ifreq)) { + struct in_addr addr = saddr->sin_addr; + + ioctl(sock, SIOCGIFNETMASK, &ifreq); + struct in_addr mask = smask->sin_addr; + + uint32_t start = ntohl(iface->dhcpv4_start.s_addr); + uint32_t end = ntohl(iface->dhcpv4_end.s_addr); + + if (start && end && start < end && + start > ntohl(addr.s_addr & ~mask.s_addr) && + (start & ntohl(mask.s_addr)) == start && + (end & ntohl(mask.s_addr)) == end) { + iface->dhcpv4_start.s_addr = htonl(start) | + (addr.s_addr & mask.s_addr); + iface->dhcpv4_end.s_addr = htonl(end) | + (addr.s_addr & mask.s_addr); + } else if (ntohl(mask.s_addr) <= 0xffffffc0) { + start = addr.s_addr & mask.s_addr; + end = addr.s_addr & mask.s_addr; + + if (ntohl(mask.s_addr) <= 0xffffff00) { + iface->dhcpv4_start.s_addr = start | htonl(20); + iface->dhcpv4_end.s_addr = end | htonl(199); + } else { + iface->dhcpv4_start.s_addr = start | htonl(10); + iface->dhcpv4_end.s_addr = end | htonl(59); + } + } + + + } + + // Parse static entries + struct lease *lease; + list_for_each_entry(lease, &leases, head) { + // Construct entry + size_t hostlen = strlen(lease->hostname) + 1; + struct dhcpv4_assignment *a = calloc(1, sizeof(*a) + hostlen); + + a->addr = ntohl(lease->ipaddr.s_addr); + memcpy(a->hwaddr, lease->mac.ether_addr_octet, sizeof(a->hwaddr)); + memcpy(a->hostname, lease->hostname, hostlen); + + // Assign to all interfaces + struct dhcpv4_assignment *c; + list_for_each_entry(c, &iface->dhcpv4_assignments, head) { + if (c->addr > a->addr) { + list_add_tail(&a->head, &c->head); + } else if (c->addr == a->addr) { + // Already an assignment with that number + break; + } + } + + if (!a->head.next) + free(a); + } + + // Clean invalid assignments + struct dhcpv4_assignment *a, *n; + list_for_each_entry_safe(a, n, &iface->dhcpv4_assignments, head) { + if ((htonl(a->addr) & smask->sin_addr.s_addr) != + (saddr->sin_addr.s_addr & smask->sin_addr.s_addr)) { + list_del(&a->head); + free(a); + } + } + + + if (iface->dhcpv4_leasetime < 60) + iface->dhcpv4_leasetime = 1800; + + iface->dhcpv4_event.uloop.fd = sock; + iface->dhcpv4_event.handle_dgram = handle_dhcpv4; + odhcpd_register(&iface->dhcpv4_event); + } else if (iface->dhcpv4_assignments.next) { + while (!list_empty(&iface->dhcpv4_assignments)) { + struct dhcpv4_assignment *a = list_first_entry(&iface->dhcpv4_assignments, + struct dhcpv4_assignment, head); + list_del(&a->head); + free(a->hostname); + free(a); + } + + } + return 0; +} + + +static void dhcpv4_put(struct dhcpv4_message *msg, uint8_t **cookie, + uint8_t type, uint8_t len, const void *data) +{ + uint8_t *c = *cookie; + if (*cookie + 2 + len > (uint8_t*)&msg[1]) + return; + + *c++ = type; + *c++ = len; + memcpy(c, data, len); + + *cookie = c + len; +} + + +// Simple DHCPv6-server for information requests +static void handle_dhcpv4(void *addr, void *data, size_t len, + struct interface *iface) +{ + if (!iface->dhcpv4) + return; + + struct dhcpv4_message *req = data; + if (len < offsetof(struct dhcpv4_message, options) + 4 || + req->op != DHCPV4_BOOTREQUEST || req->hlen != 6) + return; + + int sock = iface->dhcpv4_event.uloop.fd; + struct sockaddr_in ifaddr; + struct sockaddr_in ifnetmask; + + syslog(LOG_NOTICE, "Got DHCPv4 request"); + + struct ifreq ifreq; + memcpy(ifreq.ifr_name, iface->ifname, sizeof(ifreq.ifr_name)); + if (ioctl(sock, SIOCGIFADDR, &ifreq)) { + syslog(LOG_WARNING, "DHCPv4 failed to detect address: %s", strerror(errno)); + return; + } + + memcpy(&ifaddr, &ifreq.ifr_addr, sizeof(ifaddr)); + if (ioctl(sock, SIOCGIFNETMASK, &ifreq)) + return; + + memcpy(&ifnetmask, &ifreq.ifr_netmask, sizeof(ifnetmask)); + uint32_t network = ifaddr.sin_addr.s_addr & ifnetmask.sin_addr.s_addr; + + if ((iface->dhcpv4_start.s_addr & ifnetmask.sin_addr.s_addr) != network || + (iface->dhcpv4_end.s_addr & ifnetmask.sin_addr.s_addr) != network) { + syslog(LOG_WARNING, "DHCPv4 range out of assigned network"); + return; + } + + struct ifreq ifr = {.ifr_name = ""}; + strncpy(ifr.ifr_name, iface->ifname, sizeof(ifr.ifr_name)); + + struct dhcpv4_message reply = { + .op = DHCPV4_BOOTREPLY, + .htype = 1, + .hlen = 6, + .hops = 0, + .xid = req->xid, + .secs = 0, + .flags = req->flags, + .ciaddr = {INADDR_ANY}, + .giaddr = req->giaddr, + .siaddr = ifaddr.sin_addr, + }; + memcpy(reply.chaddr, req->chaddr, sizeof(reply.chaddr)); + + reply.options[0] = 0x63; + reply.options[1] = 0x82; + reply.options[2] = 0x53; + reply.options[3] = 0x63; + + uint8_t *cookie = &reply.options[4]; + uint8_t reqmsg = DHCPV4_MSG_REQUEST; + uint8_t msg = DHCPV4_MSG_ACK; + + struct in_addr reqaddr = {INADDR_ANY}; + char hostname[256]; + hostname[0] = 0; + + uint8_t *start = &req->options[4]; + uint8_t *end = ((uint8_t*)data) + len; + struct dhcpv4_option *opt; + dhcpv4_for_each_option(start, end, opt) { + if (opt->type == DHCPV4_OPT_MESSAGE && opt->len == 1) { + reqmsg = opt->data[0]; + } else if (opt->type == DHCPV4_OPT_HOSTNAME && opt->len > 0) { + memcpy(hostname, opt->data, opt->len); + hostname[opt->len] = 0; + } else if (opt->type == DHCPV4_OPT_IPADDRESS && opt->len == 4) { + memcpy(&reqaddr, opt->data, 4); + } else if (opt->type == DHCPV4_OPT_SERVERID && opt->len == 4) { + if (memcmp(opt->data, &ifaddr.sin_addr, 4)) + return; + } + } + + if (reqmsg != DHCPV4_MSG_DISCOVER && reqmsg != DHCPV4_MSG_REQUEST && + reqmsg != DHCPV4_MSG_INFORM && reqmsg != DHCPV4_MSG_DECLINE && + reqmsg != DHCPV4_MSG_RELEASE) + return; + + struct dhcpv4_assignment *lease = NULL; + if (reqmsg != DHCPV4_MSG_INFORM) + lease = dhcpv4_lease(iface, reqmsg, req->chaddr, reqaddr, hostname); + + if (!lease) { + if (reqmsg == DHCPV4_MSG_REQUEST) + msg = DHCPV4_MSG_NAK; + else if (reqmsg == DHCPV4_MSG_DISCOVER) + return; + } else if (reqmsg == DHCPV4_MSG_DISCOVER) { + msg = DHCPV4_MSG_OFFER; + } + + if (reqmsg == DHCPV4_MSG_DECLINE || reqmsg == DHCPV4_MSG_RELEASE) + return; + + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_MESSAGE, 1, &msg); + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_SERVERID, 4, &ifaddr.sin_addr); + + if (lease) { + reply.yiaddr.s_addr = htonl(lease->addr); + + uint32_t val = htonl(iface->dhcpv4_leasetime); + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_LEASETIME, 4, &val); + + val = htonl(500 * iface->dhcpv4_leasetime / 1000); + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_RENEW, 4, &val); + + val = htonl(875 * iface->dhcpv4_leasetime / 1000); + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_REBIND, 4, &val); + + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_NETMASK, 4, &ifnetmask.sin_addr); + + if (lease->hostname[0]) + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_HOSTNAME, + strlen(lease->hostname), lease->hostname); + + if (!ioctl(sock, SIOCGIFBRDADDR, &ifr)) { + struct sockaddr_in *ina = (struct sockaddr_in*)&ifr.ifr_broadaddr; + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_BROADCAST, 4, &ina->sin_addr); + } + } + + if (!ioctl(sock, SIOCGIFMTU, &ifr)) { + uint16_t mtu = htons(ifr.ifr_mtu); + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_MTU, 2, &mtu); + } + + if (iface->search) { + char b[256]; + if (dn_expand(iface->search, iface->search + iface->search_len, + iface->search, b, sizeof(b)) > 0) + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DOMAIN, strlen(b), b); + } else if (!res_init() && _res.dnsrch[0] && _res.dnsrch[0][0]) { + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DOMAIN, + strlen(_res.dnsrch[0]), _res.dnsrch[0]); + } + + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_ROUTER, 4, &ifaddr.sin_addr); + + + + if (iface->dhcpv4_dns_cnt == 0) + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DNSSERVER, 4, &ifaddr.sin_addr); + else + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DNSSERVER, + 4 * iface->dhcpv4_dns_cnt, iface->dhcpv4_dns); + + + dhcpv4_put(&reply, &cookie, DHCPV4_OPT_END, 0, NULL); + + struct sockaddr_in dest = *((struct sockaddr_in*)addr); + if (req->giaddr.s_addr) { + dest.sin_addr = req->giaddr; + dest.sin_port = htons(DHCPV4_SERVER_PORT); + } else if (req->ciaddr.s_addr && req->ciaddr.s_addr != dest.sin_addr.s_addr) { + dest.sin_addr = req->ciaddr; + dest.sin_port = htons(DHCPV4_CLIENT_PORT); + } else if ((ntohs(req->flags) & DHCPV4_FLAG_BROADCAST) || + req->hlen != reply.hlen) { + dest.sin_addr.s_addr = INADDR_BROADCAST; + dest.sin_port = htons(DHCPV4_CLIENT_PORT); + } else { + dest.sin_addr = reply.yiaddr; + dest.sin_port = htons(DHCPV4_CLIENT_PORT); + + struct arpreq arp = {.arp_flags = ATF_COM}; + memcpy(arp.arp_ha.sa_data, req->chaddr, 6); + memcpy(&arp.arp_pa, &dest, sizeof(arp.arp_pa)); + memcpy(arp.arp_dev, iface->ifname, sizeof(arp.arp_dev)); + ioctl(sock, SIOCSARP, &arp); + } + + sendto(sock, &reply, sizeof(reply), MSG_DONTWAIT, + (struct sockaddr*)&dest, sizeof(dest)); +} + + +static bool dhcpv4_assign(struct interface *iface, + struct dhcpv4_assignment *assign, uint32_t raddr) +{ + const unsigned tries = 10; + uint32_t start = ntohl(iface->dhcpv4_start.s_addr); + uint32_t end = ntohl(iface->dhcpv4_end.s_addr); + uint32_t count = end - start + 1; + + // Seed RNG with checksum of DUID + uint32_t seed = 0; + for (size_t i = 0; i < sizeof(assign->hwaddr); ++i) + seed += assign->hwaddr[i]; + srand(seed); + + // Try to assign up to 100x + for (unsigned i = 0; i < tries; ++i) { + uint32_t try = (((uint32_t)rand()) % count) + start; + if (i == 0 && raddr >= start && raddr <= end) + try = raddr; + else if (i == tries - 1) + try = start; + + if (list_empty(&iface->dhcpv4_assignments)) { + assign->addr = try; + list_add(&assign->head, &iface->dhcpv4_assignments); + return true; + } + + struct dhcpv4_assignment *c; + list_for_each_entry(c, &iface->dhcpv4_assignments, head) { + if (c->addr > try) { + assign->addr = try; + list_add_tail(&assign->head, &c->head); + return true; + } else if (c->addr == try) { + if (i < tries - 1) + break; + else + ++try; + } + } + } + + return false; +} + + +static struct dhcpv4_assignment* dhcpv4_lease(struct interface *iface, + enum dhcpv4_msg msg, const uint8_t *mac, struct in_addr reqaddr, + const char *hostname) +{ + struct dhcpv4_assignment *lease = NULL; + uint32_t raddr = ntohl(reqaddr.s_addr); + time_t now = odhcpd_time(); + + struct dhcpv4_assignment *c, *n, *a = NULL; + list_for_each_entry_safe(c, n, &iface->dhcpv4_assignments, head) { + if (c->addr == raddr && !memcmp(c->hwaddr, mac, 6)) { + a = c; + break; + } else if (c->valid_until < now) { + list_del(&c->head); + free(c); + } + } + + bool update_state = false; + if (msg == DHCPV4_MSG_DISCOVER || msg == DHCPV4_MSG_REQUEST) { + bool assigned = !!a; + size_t hostlen = strlen(hostname) + 1; + + if (!a && !iface->no_dynamic_dhcp) { // Create new binding + a = calloc(1, sizeof(*a) + hostlen); + memcpy(a->hwaddr, mac, sizeof(a->hwaddr)); + memcpy(a->hostname, hostname, hostlen); + + assigned = dhcpv4_assign(iface, a, raddr); + } + + if (assigned && !a->hostname[0] && hostname) { + a = realloc(a, sizeof(*a) + hostlen); + memcpy(a->hostname, hostname, hostlen); + + // Fixup list + a->head.next->prev = &a->head; + a->head.prev->next = &a->head; + } + + // Was only a solicitation: mark binding for removal + if (assigned && a->valid_until < now) { + a->valid_until = (msg == DHCPV4_MSG_DISCOVER) ? 0 : + (now + iface->dhcpv4_leasetime); + } else if (!assigned && a) { // Cleanup failed assignment + free(a); + a = NULL; + } + + if (assigned && a) + lease = a; + } else if (msg == DHCPV4_MSG_RELEASE) { + if (a) { + a->valid_until = 0; + update_state = true; + } + } else if (msg == DHCPV4_MSG_DECLINE) { + memset(a->hwaddr, 0, sizeof(a->hwaddr)); + a->valid_until = now + 3600; // Block address for 1h + update_state = true; + } + + if (update_state) + dhcpv6_write_statefile(); + + return lease; +} + diff --git a/src/dhcpv4.h b/src/dhcpv4.h new file mode 100644 index 0000000..308cc53 --- /dev/null +++ b/src/dhcpv4.h @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2012 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License version 2 for more details. + * + */ +#pragma once + +#define DHCPV4_CLIENT_PORT 68 +#define DHCPV4_SERVER_PORT 67 + +#define DHCPV4_FLAG_BROADCAST 0x8000 + +enum dhcpv4_op { + DHCPV4_BOOTREQUEST = 1, + DHCPV4_BOOTREPLY = 2 +}; + +enum dhcpv4_msg { + DHCPV4_MSG_DISCOVER = 1, + DHCPV4_MSG_OFFER = 2, + DHCPV4_MSG_REQUEST = 3, + DHCPV4_MSG_DECLINE = 4, + DHCPV4_MSG_ACK = 5, + DHCPV4_MSG_NAK = 6, + DHCPV4_MSG_RELEASE = 7, + DHCPV4_MSG_INFORM = 8, +}; + +enum dhcpv4_opt { + DHCPV4_OPT_NETMASK = 1, + DHCPV4_OPT_ROUTER = 3, + DHCPV4_OPT_DNSSERVER = 6, + DHCPV4_OPT_DOMAIN = 15, + DHCPV4_OPT_MTU = 26, + DHCPV4_OPT_BROADCAST = 28, + DHCPV4_OPT_NTPSERVER = 42, + DHCPV4_OPT_LEASETIME = 51, + DHCPV4_OPT_MESSAGE = 53, + DHCPV4_OPT_SERVERID = 54, + DHCPV4_OPT_RENEW = 58, + DHCPV4_OPT_REBIND = 59, + DHCPV4_OPT_IPADDRESS = 50, + DHCPV4_OPT_HOSTNAME = 10, + DHCPV4_OPT_REQUEST = 17, + DHCPV4_OPT_END = 255, +}; + +struct dhcpv4_message { + uint8_t op; + uint8_t htype; + uint8_t hlen; + uint8_t hops; + uint32_t xid; + uint16_t secs; + uint16_t flags; + struct in_addr ciaddr; + struct in_addr yiaddr; + struct in_addr siaddr; + struct in_addr giaddr; + uint8_t chaddr[16]; + char sname[64]; + char file[128]; + uint8_t options[312]; +}; + +struct dhcpv4_assignment { + struct list_head head; + uint32_t addr; + time_t valid_until; + uint8_t hwaddr[6]; + char hostname[]; +}; + +struct dhcpv4_option { + uint8_t type; + uint8_t len; + uint8_t data[]; +}; + + +#define dhcpv4_for_each_option(start, end, opt)\ + for (opt = (struct dhcpv4_option*)(start); \ + &opt[1] <= (struct dhcpv4_option*)(end) && \ + &opt->data[opt->len] <= (end); \ + opt = (struct dhcpv4_option*)&opt->data[opt->len]) diff --git a/src/dhcpv6-ia.c b/src/dhcpv6-ia.c new file mode 100644 index 0000000..89d3a15 --- /dev/null +++ b/src/dhcpv6-ia.c @@ -0,0 +1,893 @@ +/** + * Copyright (C) 2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "odhcpd.h" +#include "dhcpv6.h" +#include "dhcpv4.h" +#include "md5.h" + +#include <time.h> +#include <errno.h> +#include <fcntl.h> +#include <alloca.h> +#include <resolv.h> +#include <limits.h> +#include <stdlib.h> +#include <unistd.h> +#include <arpa/inet.h> +#include <sys/timerfd.h> + + +static void update(struct interface *iface); +static void reconf_timer(struct uloop_timeout *event); +static struct uloop_timeout reconf_event = {.cb = reconf_timer}; +static int socket_fd = -1; +static uint32_t serial = 0; + + +int dhcpv6_ia_init(int dhcpv6_socket) +{ + socket_fd = dhcpv6_socket; + uloop_timeout_set(&reconf_event, 2000); + return 0; +} + + +int setup_dhcpv6_ia_interface(struct interface *iface, bool enable) +{ + if (!enable && iface->ia_assignments.next) { + struct dhcpv6_assignment *c; + while (!list_empty(&iface->ia_assignments)) { + c = list_first_entry(&iface->ia_assignments, struct dhcpv6_assignment, head); + list_del(&c->head); + free(c); + } + } + + if (iface->dhcpv6 == RELAYD_SERVER) { + if (!iface->ia_assignments.next) + INIT_LIST_HEAD(&iface->ia_assignments); + + if (list_empty(&iface->ia_assignments)) { + struct dhcpv6_assignment *border = calloc(1, sizeof(*border)); + border->length = 64; + list_add(&border->head, &iface->ia_assignments); + } + + update(iface); + + // Parse static entries + struct lease *lease; + list_for_each_entry(lease, &leases, head) { + // Construct entry + struct dhcpv6_assignment *a = calloc(1, sizeof(*a) + lease->duid_len); + a->clid_len = lease->duid_len; + a->length = 128; + a->assigned = lease->hostid; + odhcpd_urandom(a->key, sizeof(a->key)); + memcpy(a->clid_data, lease->duid, a->clid_len); + memcpy(a->mac, lease->mac.ether_addr_octet, sizeof(a->mac)); + + // Assign to all interfaces + struct dhcpv6_assignment *c; + list_for_each_entry(c, &iface->ia_assignments, head) { + if (c->length != 128 || c->assigned > a->assigned) { + list_add_tail(&a->head, &c->head); + } else if (c->assigned == a->assigned) { + // Already an assignment with that number + break; + } + } + + if (a->head.next) { + if (lease->hostname[0]) { + free(a->hostname); + a->hostname = strdup(lease->hostname); + } + } else { + free(a); + } + } + } + return 0; +} + + +static int send_reconf(struct interface *iface, struct dhcpv6_assignment *assign) +{ + struct { + struct dhcpv6_client_header hdr; + uint16_t srvid_type; + uint16_t srvid_len; + uint16_t duid_type; + uint16_t hardware_type; + uint8_t mac[6]; + uint16_t msg_type; + uint16_t msg_len; + uint8_t msg_id; + struct dhcpv6_auth_reconfigure auth; + uint16_t clid_type; + uint16_t clid_len; + uint8_t clid_data[128]; + } __attribute__((packed)) reconf_msg = { + .hdr = {DHCPV6_MSG_RECONFIGURE, {0, 0, 0}}, + .srvid_type = htons(DHCPV6_OPT_SERVERID), + .srvid_len = htons(10), + .duid_type = htons(3), + .hardware_type = htons(1), + .msg_type = htons(DHCPV6_OPT_RECONF_MSG), + .msg_len = htons(1), + .msg_id = DHCPV6_MSG_RENEW, + .auth = {htons(DHCPV6_OPT_AUTH), + htons(sizeof(reconf_msg.auth) - 4), 3, 1, 0, + {htonl(time(NULL)), htonl(++serial)}, 2, {0}}, + .clid_type = htons(DHCPV6_OPT_CLIENTID), + .clid_len = htons(assign->clid_len), + .clid_data = {0}, + }; + + odhcpd_get_mac(iface, reconf_msg.mac); + memcpy(reconf_msg.clid_data, assign->clid_data, assign->clid_len); + struct iovec iov = {&reconf_msg, sizeof(reconf_msg) - 128 + assign->clid_len}; + + md5_ctx_t md5; + uint8_t secretbytes[16]; + memcpy(secretbytes, assign->key, sizeof(secretbytes)); + + for (size_t i = 0; i < sizeof(secretbytes); ++i) + secretbytes[i] ^= 0x36; + + md5_begin(&md5); + md5_hash(secretbytes, sizeof(secretbytes), &md5); + md5_hash(iov.iov_base, iov.iov_len, &md5); + md5_end(reconf_msg.auth.key, &md5); + + for (size_t i = 0; i < sizeof(secretbytes); ++i) { + secretbytes[i] ^= 0x36; + secretbytes[i] ^= 0x5c; + } + + md5_begin(&md5); + md5_hash(secretbytes, sizeof(secretbytes), &md5); + md5_hash(reconf_msg.auth.key, 16, &md5); + md5_end(reconf_msg.auth.key, &md5); + + return odhcpd_send(socket_fd, &assign->peer, &iov, 1, iface); +} + + +void dhcpv6_write_statefile(void) +{ + if (config.dhcp_statefile) { + time_t now = odhcpd_time(), wall_time = time(NULL); + int fd = open(config.dhcp_statefile, O_CREAT | O_WRONLY | O_CLOEXEC, 0644); + if (fd < 0) + return; + + lockf(fd, F_LOCK, 0); + ftruncate(fd, 0); + + FILE *fp = fdopen(fd, "w"); + if (!fp) { + close(fd); + return; + } + + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) { + if (iface->dhcpv6 != RELAYD_SERVER && iface->dhcpv4 != RELAYD_SERVER) + continue; + + if (iface->dhcpv6 == RELAYD_SERVER) { + struct dhcpv6_assignment *c; + list_for_each_entry(c, &iface->ia_assignments, head) { + if (c->clid_len == 0) + continue; + + char ipbuf[INET6_ADDRSTRLEN]; + char leasebuf[512]; + char duidbuf[264]; + odhcpd_hexlify(duidbuf, c->clid_data, c->clid_len); + + // iface DUID iaid hostname lifetime assigned length [addrs...] + int l = snprintf(leasebuf, sizeof(leasebuf), "# %s %s %x %s %u %x %u ", + iface->ifname, duidbuf, ntohl(c->iaid), + (c->hostname ? c->hostname : "-"), + (unsigned)(c->valid_until > now ? + (c->valid_until - now + wall_time) : 0), + c->assigned, (unsigned)c->length); + + struct in6_addr addr; + for (size_t i = 0; i < iface->ia_addr_len; ++i) { + if (iface->ia_addr[i].prefix > 64) + continue; + + addr = iface->ia_addr[i].addr; + if (c->length == 128) + addr.s6_addr32[3] = htonl(c->assigned); + else + addr.s6_addr32[1] |= htonl(c->assigned); + inet_ntop(AF_INET6, &addr, ipbuf, sizeof(ipbuf) - 1); + + if (c->length == 128 && c->hostname && i == 0) + fprintf(fp, "%s\t%s\n", ipbuf, c->hostname); + + l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/%hhu ", ipbuf, c->length); + } + leasebuf[l - 1] = '\n'; + fwrite(leasebuf, 1, l, fp); + } + } + + if (iface->dhcpv4 == RELAYD_SERVER) { + struct dhcpv4_assignment *c; + list_for_each_entry(c, &iface->dhcpv4_assignments, head) { + char ipbuf[INET6_ADDRSTRLEN]; + char leasebuf[512]; + char duidbuf[16]; + odhcpd_hexlify(duidbuf, c->hwaddr, sizeof(c->hwaddr)); + + // iface DUID iaid hostname lifetime assigned length [addrs...] + int l = snprintf(leasebuf, sizeof(leasebuf), "# %s %s ipv4 %s %u %x 32 ", + iface->ifname, duidbuf, + (c->hostname ? c->hostname : "-"), + (unsigned)(c->valid_until > now ? + (c->valid_until - now + wall_time) : 0), + c->addr); + + struct in_addr addr = {htonl(c->addr)}; + inet_ntop(AF_INET, &addr, ipbuf, sizeof(ipbuf) - 1); + + if (c->hostname[0]) + fprintf(fp, "%s\t%s\n", ipbuf, c->hostname); + + l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/32 ", ipbuf); + leasebuf[l - 1] = '\n'; + fwrite(leasebuf, 1, l, fp); + } + } + } + + fclose(fp); + } + + if (config.dhcp_cb) { + char *argv[2] = {config.dhcp_cb, NULL}; + if (!vfork()) { + execv(argv[0], argv); + _exit(128); + } + } +} + + +static void apply_lease(struct interface *iface, struct dhcpv6_assignment *a, bool add) +{ + if (a->length > 64) + return; + + for (size_t i = 0; i < iface->ia_addr_len; ++i) { + struct in6_addr prefix = iface->ia_addr[i].addr; + prefix.s6_addr32[1] |= htonl(a->assigned); + odhcpd_setup_route(&prefix, a->length, iface, &a->peer.sin6_addr, add); + } +} + + +static bool assign_pd(struct interface *iface, struct dhcpv6_assignment *assign) +{ + struct dhcpv6_assignment *c; + if (iface->ia_addr_len < 1) + return false; + + // Try honoring the hint first + uint32_t current = 1, asize = (1 << (64 - assign->length)) - 1; + if (assign->assigned) { + list_for_each_entry(c, &iface->ia_assignments, head) { + if (c->length == 128) + continue; + + if (assign->assigned >= current && assign->assigned + asize < c->assigned) { + list_add_tail(&assign->head, &c->head); + apply_lease(iface, assign, true); + return true; + } + + if (c->assigned != 0) + current = (c->assigned + (1 << (64 - c->length))); + } + } + + // Fallback to a variable assignment + current = 1; + list_for_each_entry(c, &iface->ia_assignments, head) { + if (c->length == 128) + continue; + + current = (current + asize) & (~asize); + if (current + asize < c->assigned) { + assign->assigned = current; + list_add_tail(&assign->head, &c->head); + apply_lease(iface, assign, true); + return true; + } + + if (c->assigned != 0) + current = (c->assigned + (1 << (64 - c->length))); + } + + return false; +} + + +static bool assign_na(struct interface *iface, struct dhcpv6_assignment *assign) +{ + if (iface->ia_addr_len < 1) + return false; + + // Seed RNG with checksum of DUID + uint32_t seed = 0; + for (size_t i = 0; i < assign->clid_len; ++i) + seed += assign->clid_data[i]; + srand(seed); + + // Try to assign up to 100x + for (size_t i = 0; i < 100; ++i) { + uint32_t try; + do try = ((uint32_t)rand()) % 0x0fff; while (try < 0x100); + + struct dhcpv6_assignment *c; + list_for_each_entry(c, &iface->ia_assignments, head) { + if (c->assigned > try || c->length != 128) { + assign->assigned = try; + list_add_tail(&assign->head, &c->head); + return true; + } else if (c->assigned == try) { + break; + } + } + } + + return false; +} + + +static int prefixcmp(const void *va, const void *vb) +{ + const struct odhcpd_ipaddr *a = va, *b = vb; + uint32_t a_pref = ((a->addr.s6_addr[0] & 0xfe) != 0xfc) ? a->preferred : 1; + uint32_t b_pref = ((b->addr.s6_addr[0] & 0xfe) != 0xfc) ? b->preferred : 1; + return (a_pref < b_pref) ? 1 : (a_pref > b_pref) ? -1 : 0; +} + + +static void update(struct interface *iface) +{ + struct odhcpd_ipaddr addr[8]; + memset(addr, 0, sizeof(addr)); + int len = odhcpd_get_interface_addresses(iface->ifindex, addr, 8); + + if (len < 0) + return; + + qsort(addr, len, sizeof(*addr), prefixcmp); + + time_t now = odhcpd_time(); + int minprefix = -1; + + for (int i = 0; i < len; ++i) { + if (addr[i].prefix > minprefix) + minprefix = addr[i].prefix; + + addr[i].addr.s6_addr32[2] = 0; + addr[i].addr.s6_addr32[3] = 0; + + if (addr[i].preferred < UINT32_MAX - now) + addr[i].preferred += now; + + if (addr[i].valid < UINT32_MAX - now) + addr[i].valid += now; + } + + struct dhcpv6_assignment *border = list_last_entry(&iface->ia_assignments, struct dhcpv6_assignment, head); + border->assigned = 1 << (64 - minprefix); + + bool change = len != (int)iface->ia_addr_len; + for (int i = 0; !change && i < len; ++i) + if (addr[i].addr.s6_addr32[0] != iface->ia_addr[i].addr.s6_addr32[0] || + addr[i].addr.s6_addr32[1] != iface->ia_addr[i].addr.s6_addr32[1] || + (addr[i].preferred > 0) != (iface->ia_addr[i].preferred > 0) || + (addr[i].valid > now + 7200) != (iface->ia_addr[i].valid > now + 7200)) + change = true; + + if (change) { + struct dhcpv6_assignment *c; + list_for_each_entry(c, &iface->ia_assignments, head) + if (c != border) + apply_lease(iface, c, false); + } + + memcpy(iface->ia_addr, addr, len * sizeof(*addr)); + iface->ia_addr_len = len; + + if (change) { // Addresses / prefixes have changed + struct list_head reassign = LIST_HEAD_INIT(reassign); + struct dhcpv6_assignment *c, *d; + list_for_each_entry_safe(c, d, &iface->ia_assignments, head) { + if (c->clid_len == 0 || c->valid_until < now) + continue; + + if (c->length < 128 && c->assigned >= border->assigned && c != border) + list_move(&c->head, &reassign); + else if (c != border) + apply_lease(iface, c, true); + + if (c->accept_reconf && c->reconf_cnt == 0) { + c->reconf_cnt = 1; + c->reconf_sent = now; + send_reconf(iface, c); + + // Leave all other assignments of that client alone + struct dhcpv6_assignment *a; + list_for_each_entry(a, &iface->ia_assignments, head) + if (a != c && a->clid_len == c->clid_len && + !memcmp(a->clid_data, c->clid_data, a->clid_len)) + c->reconf_cnt = INT_MAX; + } + } + + while (!list_empty(&reassign)) { + c = list_first_entry(&reassign, struct dhcpv6_assignment, head); + list_del(&c->head); + if (!assign_pd(iface, c)) { + c->assigned = 0; + list_add(&c->head, &iface->ia_assignments); + } + } + + dhcpv6_write_statefile(); + } +} + + +static void reconf_timer(struct uloop_timeout *event) +{ + time_t now = odhcpd_time(); + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) { + if (iface->dhcpv6 != RELAYD_SERVER || iface->ia_assignments.next == NULL) + continue; + + struct dhcpv6_assignment *a, *n; + list_for_each_entry_safe(a, n, &iface->ia_assignments, head) { + if (a->valid_until < now) { + if ((a->length < 128 && a->clid_len > 0) || + (a->length == 128 && a->clid_len == 0)) { + list_del(&a->head); + free(a->hostname); + free(a); + } + } else if (a->reconf_cnt > 0 && a->reconf_cnt < 8 && + now > a->reconf_sent + (1 << a->reconf_cnt)) { + ++a->reconf_cnt; + a->reconf_sent = now; + send_reconf(iface, a); + } + } + + if (iface->ia_reconf) { + update(iface); + iface->ia_reconf = false; + } + } + + uloop_timeout_set(event, 2000); +} + + +static size_t append_reply(uint8_t *buf, size_t buflen, uint16_t status, + const struct dhcpv6_ia_hdr *ia, struct dhcpv6_assignment *a, + struct interface *iface, bool request) +{ + if (buflen < sizeof(*ia) + sizeof(struct dhcpv6_ia_prefix)) + return 0; + + struct dhcpv6_ia_hdr out = {ia->type, 0, ia->iaid, 0, 0}; + size_t datalen = sizeof(out); + time_t now = odhcpd_time(); + + if (status) { + struct __attribute__((packed)) { + uint16_t type; + uint16_t len; + uint16_t value; + } stat = {htons(DHCPV6_OPT_STATUS), htons(sizeof(stat) - 4), + htons(status)}; + + memcpy(buf + datalen, &stat, sizeof(stat)); + datalen += sizeof(stat); + } else { + if (a) { + uint32_t pref = 3600; + uint32_t valid = 3600; + bool have_non_ula = false; + for (size_t i = 0; i < iface->ia_addr_len; ++i) + if ((iface->ia_addr[i].addr.s6_addr[0] & 0xfe) != 0xfc) + have_non_ula = true; + + for (size_t i = 0; i < iface->ia_addr_len; ++i) { + uint32_t prefix_pref = iface->ia_addr[i].preferred - now; + uint32_t prefix_valid = iface->ia_addr[i].valid - now; + + if (iface->ia_addr[i].prefix > 64 || + iface->ia_addr[i].preferred <= (uint32_t)now) + continue; + + // ULA-deprecation compatibility workaround + if ((iface->ia_addr[i].addr.s6_addr[0] & 0xfe) == 0xfc && + a->length == 128 && have_non_ula && + iface->deprecate_ula_if_public_avail) + continue; + + if (prefix_pref > 86400) + prefix_pref = 86400; + + if (prefix_valid > 86400) + prefix_valid = 86400; + + if (a->length < 128) { + struct dhcpv6_ia_prefix p = { + .type = htons(DHCPV6_OPT_IA_PREFIX), + .len = htons(sizeof(p) - 4), + .preferred = htonl(prefix_pref), + .valid = htonl(prefix_valid), + .prefix = a->length, + .addr = iface->ia_addr[i].addr + }; + p.addr.s6_addr32[1] |= htonl(a->assigned); + + if (datalen + sizeof(p) > buflen || a->assigned == 0) + continue; + + memcpy(buf + datalen, &p, sizeof(p)); + datalen += sizeof(p); + } else { + struct dhcpv6_ia_addr n = { + .type = htons(DHCPV6_OPT_IA_ADDR), + .len = htons(sizeof(n) - 4), + .addr = iface->ia_addr[i].addr, + .preferred = htonl(prefix_pref), + .valid = htonl(prefix_valid) + }; + n.addr.s6_addr32[3] = htonl(a->assigned); + + if (datalen + sizeof(n) > buflen || a->assigned == 0) + continue; + + memcpy(buf + datalen, &n, sizeof(n)); + datalen += sizeof(n); + } + + // Calculate T1 / T2 based on non-deprecated addresses + if (prefix_pref > 0) { + if (prefix_pref < pref) + pref = prefix_pref; + + if (prefix_valid < valid) + valid = prefix_valid; + } + } + + a->valid_until = valid + now; + out.t1 = htonl(pref * 5 / 10); + out.t2 = htonl(pref * 8 / 10); + + if (!out.t1) + out.t1 = htonl(1); + + if (!out.t2) + out.t2 = htonl(1); + } + + if (!request) { + uint8_t *odata, *end = ((uint8_t*)ia) + htons(ia->len) + 4; + uint16_t otype, olen; + dhcpv6_for_each_option((uint8_t*)&ia[1], end, otype, olen, odata) { + struct dhcpv6_ia_prefix *p = (struct dhcpv6_ia_prefix*)&odata[-4]; + struct dhcpv6_ia_addr *n = (struct dhcpv6_ia_addr*)&odata[-4]; + if ((otype != DHCPV6_OPT_IA_PREFIX || olen < sizeof(*p) - 4) && + (otype != DHCPV6_OPT_IA_ADDR || olen < sizeof(*n) - 4)) + continue; + + bool found = false; + if (a) { + for (size_t i = 0; i < iface->ia_addr_len; ++i) { + if (iface->ia_addr[i].prefix > 64 || + iface->ia_addr[i].preferred <= (uint32_t)now) + continue; + + struct in6_addr addr = iface->ia_addr[i].addr; + if (ia->type == htons(DHCPV6_OPT_IA_PD)) { + addr.s6_addr32[1] |= htonl(a->assigned); + + if (IN6_ARE_ADDR_EQUAL(&p->addr, &addr) && + p->prefix == a->length) + found = true; + } else { + addr.s6_addr32[3] = htonl(a->assigned); + + if (IN6_ARE_ADDR_EQUAL(&n->addr, &addr)) + found = true; + } + } + } + + if (!found) { + if (otype == DHCPV6_OPT_IA_PREFIX) { + struct dhcpv6_ia_prefix inv = { + .type = htons(DHCPV6_OPT_IA_PREFIX), + .len = htons(sizeof(inv) - 4), + .preferred = 0, + .valid = 0, + .prefix = p->prefix, + .addr = p->addr + }; + + if (datalen + sizeof(inv) > buflen) + continue; + + memcpy(buf + datalen, &inv, sizeof(inv)); + datalen += sizeof(inv); + } else { + struct dhcpv6_ia_addr inv = { + .type = htons(DHCPV6_OPT_IA_ADDR), + .len = htons(sizeof(inv) - 4), + .addr = n->addr, + .preferred = 0, + .valid = 0 + }; + + if (datalen + sizeof(inv) > buflen) + continue; + + memcpy(buf + datalen, &inv, sizeof(inv)); + datalen += sizeof(inv); + } + } + } + } + } + + out.len = htons(datalen - 4); + memcpy(buf, &out, sizeof(out)); + return datalen; +} + + +size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface, + const struct sockaddr_in6 *addr, const void *data, const uint8_t *end) +{ + time_t now = odhcpd_time(); + size_t response_len = 0; + const struct dhcpv6_client_header *hdr = data; + uint8_t *start = (uint8_t*)&hdr[1], *odata; + uint16_t otype, olen; + + // Find and parse client-id and hostname + bool accept_reconf = false; + uint8_t *clid_data = NULL, clid_len = 0, mac[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + char hostname[256]; + size_t hostname_len = 0; + dhcpv6_for_each_option(start, end, otype, olen, odata) { + if (otype == DHCPV6_OPT_CLIENTID) { + clid_data = odata; + clid_len = olen; + + if (olen == 14 && odata[0] == 0 && odata[1] == 1) + memcpy(mac, &odata[8], sizeof(mac)); + else if (olen == 10 && odata[0] == 0 && odata[1] == 3) + memcpy(mac, &odata[4], sizeof(mac)); + } else if (otype == DHCPV6_OPT_FQDN && olen >= 2 && olen <= 255) { + uint8_t fqdn_buf[256]; + memcpy(fqdn_buf, odata, olen); + fqdn_buf[olen++] = 0; + + if (dn_expand(&fqdn_buf[1], &fqdn_buf[olen], &fqdn_buf[1], hostname, sizeof(hostname)) > 0) + hostname_len = strcspn(hostname, "."); + } else if (otype == DHCPV6_OPT_RECONF_ACCEPT) { + accept_reconf = true; + } + } + + if (!clid_data || !clid_len || clid_len > 130) + goto out; + + update(iface); + bool update_state = false; + + struct dhcpv6_assignment *first = NULL; + dhcpv6_for_each_option(start, end, otype, olen, odata) { + bool is_pd = (otype == DHCPV6_OPT_IA_PD); + bool is_na = (otype == DHCPV6_OPT_IA_NA); + if (!is_pd && !is_na) + continue; + + struct dhcpv6_ia_hdr *ia = (struct dhcpv6_ia_hdr*)&odata[-4]; + size_t ia_response_len = 0; + uint8_t reqlen = (is_pd) ? 62 : 128; + uint32_t reqhint = 0; + + // Parse request hint for IA-PD + if (is_pd) { + uint8_t *sdata; + uint16_t stype, slen; + dhcpv6_for_each_option(&ia[1], odata + olen, stype, slen, sdata) { + if (stype == DHCPV6_OPT_IA_PREFIX && slen >= sizeof(struct dhcpv6_ia_prefix) - 4) { + struct dhcpv6_ia_prefix *p = (struct dhcpv6_ia_prefix*)&sdata[-4]; + if (p->prefix) { + reqlen = p->prefix; + reqhint = ntohl(p->addr.s6_addr32[1]); + if (reqlen > 32 && reqlen <= 64) + reqhint &= (1U << (64 - reqlen)) - 1; + } + break; + } + } + + if (reqlen > 64) + reqlen = 64; + } + + // Find assignment + struct dhcpv6_assignment *c, *a = NULL; + list_for_each_entry(c, &iface->ia_assignments, head) { + if (((c->clid_len == clid_len && !memcmp(c->clid_data, clid_data, clid_len)) || + (c->clid_len >= clid_len && !c->clid_data[0] && !c->clid_data[1] + && !memcmp(c->mac, mac, sizeof(mac)))) && + (c->iaid == ia->iaid || c->valid_until < now) && + ((is_pd && c->length <= 64) || (is_na && c->length == 128))) { + a = c; + + // Reset state + apply_lease(iface, a, false); + memcpy(a->clid_data, clid_data, clid_len); + a->clid_len = clid_len; + a->iaid = ia->iaid; + a->peer = *addr; + a->reconf_cnt = 0; + a->reconf_sent = 0; + break; + } + } + + // Generic message handling + uint16_t status = DHCPV6_STATUS_OK; + if (hdr->msg_type == DHCPV6_MSG_SOLICIT || hdr->msg_type == DHCPV6_MSG_REQUEST) { + bool assigned = !!a; + + if (!a && !iface->no_dynamic_dhcp) { // Create new binding + a = calloc(1, sizeof(*a) + clid_len); + a->clid_len = clid_len; + a->iaid = ia->iaid; + a->length = reqlen; + a->peer = *addr; + a->assigned = reqhint; + if (first) + memcpy(a->key, first->key, sizeof(a->key)); + else + odhcpd_urandom(a->key, sizeof(a->key)); + memcpy(a->clid_data, clid_data, clid_len); + + if (is_pd) + while (!(assigned = assign_pd(iface, a)) && ++a->length <= 64); + else + assigned = assign_na(iface, a); + } + + if (!assigned || iface->ia_addr_len == 0) { // Set error status + status = (is_pd) ? DHCPV6_STATUS_NOPREFIXAVAIL : DHCPV6_STATUS_NOADDRSAVAIL; + } else if (assigned && !first) { // + size_t handshake_len = 4; + buf[0] = 0; + buf[1] = DHCPV6_OPT_RECONF_ACCEPT; + buf[2] = 0; + buf[3] = 0; + + if (hdr->msg_type == DHCPV6_MSG_REQUEST) { + struct dhcpv6_auth_reconfigure auth = { + htons(DHCPV6_OPT_AUTH), + htons(sizeof(auth) - 4), + 3, 1, 0, + {htonl(time(NULL)), htonl(++serial)}, + 1, + {0} + }; + memcpy(auth.key, a->key, sizeof(a->key)); + memcpy(buf + handshake_len, &auth, sizeof(auth)); + handshake_len += sizeof(auth); + } + + buf += handshake_len; + buflen -= handshake_len; + response_len += handshake_len; + + first = a; + } + + ia_response_len = append_reply(buf, buflen, status, ia, a, iface, true); + + // Was only a solicitation: mark binding for removal + if (assigned && hdr->msg_type == DHCPV6_MSG_SOLICIT) { + a->valid_until = 0; + } else if (assigned && hdr->msg_type == DHCPV6_MSG_REQUEST) { + if (hostname_len > 0) { + a->hostname = realloc(a->hostname, hostname_len + 1); + memcpy(a->hostname, hostname, hostname_len); + a->hostname[hostname_len] = 0; + } + a->accept_reconf = accept_reconf; + apply_lease(iface, a, true); + update_state = true; + } else if (!assigned && a) { // Cleanup failed assignment + free(a->hostname); + free(a); + } + } else if (hdr->msg_type == DHCPV6_MSG_RENEW || + hdr->msg_type == DHCPV6_MSG_RELEASE || + hdr->msg_type == DHCPV6_MSG_REBIND || + hdr->msg_type == DHCPV6_MSG_DECLINE) { + if (!a && hdr->msg_type != DHCPV6_MSG_REBIND) { + status = DHCPV6_STATUS_NOBINDING; + ia_response_len = append_reply(buf, buflen, status, ia, a, iface, false); + } else if (hdr->msg_type == DHCPV6_MSG_RENEW || + hdr->msg_type == DHCPV6_MSG_REBIND) { + ia_response_len = append_reply(buf, buflen, status, ia, a, iface, false); + if (a) + apply_lease(iface, a, true); + } else if (hdr->msg_type == DHCPV6_MSG_RELEASE) { + a->valid_until = 0; + apply_lease(iface, a, false); + update_state = true; + } else if (hdr->msg_type == DHCPV6_MSG_DECLINE && a->length == 128) { + a->clid_len = 0; + a->valid_until = now + 3600; // Block address for 1h + update_state = true; + } + } else if (hdr->msg_type == DHCPV6_MSG_CONFIRM) { + // Always send NOTONLINK for CONFIRM so that clients restart connection + status = DHCPV6_STATUS_NOTONLINK; + ia_response_len = append_reply(buf, buflen, status, ia, a, iface, true); + } + + buf += ia_response_len; + buflen -= ia_response_len; + response_len += ia_response_len; + } + + if (hdr->msg_type == DHCPV6_MSG_RELEASE && response_len + 6 < buflen) { + buf[0] = 0; + buf[1] = DHCPV6_OPT_STATUS; + buf[2] = 0; + buf[3] = 2; + buf[4] = 0; + buf[5] = DHCPV6_STATUS_OK; + response_len += 6; + } + + if (update_state) + dhcpv6_write_statefile(); + +out: + return response_len; +} diff --git a/src/dhcpv6.c b/src/dhcpv6.c new file mode 100644 index 0000000..9515f40 --- /dev/null +++ b/src/dhcpv6.c @@ -0,0 +1,452 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * + */ + +#include <errno.h> +#include <unistd.h> +#include <stddef.h> +#include <resolv.h> +#include <sys/timerfd.h> + +#include "odhcpd.h" +#include "dhcpv6.h" + + +static void relay_client_request(struct sockaddr_in6 *source, + const void *data, size_t len, struct interface *iface); +static void relay_server_response(uint8_t *data, size_t len); + +static void handle_dhcpv6(void *addr, void *data, size_t len, + struct interface *iface); +static void handle_client_request(void *addr, void *data, size_t len, + struct interface *iface); + +static struct odhcpd_event dhcpv6_event = {{.fd = -1}, handle_dhcpv6}; + + + +// Create socket and register events +int init_dhcpv6(void) +{ + int sock = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP); + + // Basic IPv6 configuration + int val = 1; + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val)); + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); + setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, sizeof(val)); + + val = DHCPV6_HOP_COUNT_LIMIT; + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val)); + + val = 0; + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof(val)); + + struct sockaddr_in6 bind_addr = {AF_INET6, htons(DHCPV6_SERVER_PORT), + 0, IN6ADDR_ANY_INIT, 0}; + + if (bind(sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr))) { + syslog(LOG_ERR, "Failed to open DHCPv6 server socket: %s", + strerror(errno)); + return -1; + } + + dhcpv6_event.uloop.fd = sock; + odhcpd_register(&dhcpv6_event); + + dhcpv6_ia_init(dhcpv6_event.uloop.fd); + + return 0; +} + + +int setup_dhcpv6_interface(struct interface *iface, bool enable) +{ + // Configure multicast settings + struct ipv6_mreq relay = {ALL_DHCPV6_RELAYS, iface->ifindex}; + struct ipv6_mreq server = {ALL_DHCPV6_SERVERS, iface->ifindex}; + + setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6, + IPV6_DROP_MEMBERSHIP, &relay, sizeof(relay)); + setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6, + IPV6_DROP_MEMBERSHIP, &server, sizeof(server)); + + if (enable && iface->dhcpv6 && !iface->master) { + setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6, + IPV6_ADD_MEMBERSHIP, &relay, sizeof(relay)); + + if (iface->dhcpv6 == RELAYD_SERVER) + setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6, + IPV6_ADD_MEMBERSHIP, &server, sizeof(server)); + } + + setup_dhcpv6_ia_interface(iface, enable); + return 0; +} + + +static void handle_nested_message(uint8_t *data, size_t len, + uint8_t **opts, uint8_t **end, struct iovec iov[6]) +{ + struct dhcpv6_relay_header *hdr = (struct dhcpv6_relay_header*)data; + if (iov[0].iov_base == NULL) { + iov[0].iov_base = data; + iov[0].iov_len = len; + } + + if (len < sizeof(struct dhcpv6_client_header)) + return; + + if (hdr->msg_type != DHCPV6_MSG_RELAY_FORW) { + iov[0].iov_len = data - (uint8_t*)iov[0].iov_base; + struct dhcpv6_client_header *hdr = (void*)data; + *opts = (uint8_t*)&hdr[1]; + *end = data + len; + return; + } + + uint16_t otype, olen; + uint8_t *odata; + dhcpv6_for_each_option(hdr->options, data + len, otype, olen, odata) { + if (otype == DHCPV6_OPT_RELAY_MSG) { + iov[7].iov_base = odata + olen; + iov[7].iov_len = (((uint8_t*)iov[0].iov_base) + iov[0].iov_len) + - (odata + olen); + handle_nested_message(odata, olen, opts, end, iov); + return; + } + } +} + + +static void update_nested_message(uint8_t *data, size_t len, ssize_t pdiff) +{ + struct dhcpv6_relay_header *hdr = (struct dhcpv6_relay_header*)data; + if (hdr->msg_type != DHCPV6_MSG_RELAY_FORW) + return; + + hdr->msg_type = DHCPV6_MSG_RELAY_REPL; + + uint16_t otype, olen; + uint8_t *odata; + dhcpv6_for_each_option(hdr->options, data + len, otype, olen, odata) { + if (otype == DHCPV6_OPT_RELAY_MSG) { + olen += pdiff; + odata[-2] = (olen >> 8) & 0xff; + odata[-1] = olen & 0xff; + update_nested_message(odata, olen - pdiff, pdiff); + return; + } + } +} + + +// Simple DHCPv6-server for information requests +static void handle_client_request(void *addr, void *data, size_t len, + struct interface *iface) +{ + struct dhcpv6_client_header *hdr = data; + if (len < sizeof(*hdr)) + return; + + syslog(LOG_NOTICE, "Got DHCPv6 request"); + + // Construct reply message + struct __attribute__((packed)) { + uint8_t msg_type; + uint8_t tr_id[3]; + uint16_t serverid_type; + uint16_t serverid_length; + uint16_t duid_type; + uint16_t hardware_type; + uint8_t mac[6]; + uint16_t clientid_type; + uint16_t clientid_length; + uint8_t clientid_buf[130]; + } dest = { + .msg_type = DHCPV6_MSG_REPLY, + .serverid_type = htons(DHCPV6_OPT_SERVERID), + .serverid_length = htons(10), + .duid_type = htons(3), + .hardware_type = htons(1), + .clientid_type = htons(DHCPV6_OPT_CLIENTID), + .clientid_buf = {0} + }; + odhcpd_get_mac(iface, dest.mac); + + struct __attribute__((packed)) { + uint16_t type; + uint16_t len; + uint16_t value; + } stat = {htons(DHCPV6_OPT_STATUS), htons(sizeof(stat) - 4), + htons(DHCPV6_STATUS_NOADDRSAVAIL)}; + + struct __attribute__((packed)) { + uint16_t type; + uint16_t len; + uint32_t value; + } refresh = {htons(DHCPV6_OPT_INFO_REFRESH), htons(sizeof(uint32_t)), + htonl(600)}; + + struct odhcpd_ipaddr ipaddr; + struct in6_addr *dns_addr = iface->dns; + size_t dns_cnt = iface->dns_cnt; + + if (dns_cnt == 0 && odhcpd_get_interface_addresses(iface->ifindex, &ipaddr, 1) == 1) { + dns_addr = &ipaddr.addr; + dns_cnt = 1; + } + + struct { + uint16_t type; + uint16_t len; + } dns = {htons(DHCPV6_OPT_DNS_SERVERS), htons(dns_cnt * sizeof(*dns_addr))}; + + + + // DNS Search options + uint8_t search_buf[256], *search_domain = iface->search; + size_t search_len = iface->search_len; + + if (!search_domain && !res_init() && _res.dnsrch[0] && _res.dnsrch[0][0]) { + int len = dn_comp(_res.dnsrch[0], search_buf, + sizeof(search_buf), NULL, NULL); + if (len > 0) { + search_domain = search_buf; + search_len = len; + } + } + + struct { + uint16_t type; + uint16_t len; + } search = {htons(DHCPV6_OPT_DNS_DOMAIN), htons(search_len)}; + + + + uint8_t pdbuf[512]; + struct iovec iov[] = {{NULL, 0}, + {&dest, (uint8_t*)&dest.clientid_type - (uint8_t*)&dest}, + {&dns, (dns_cnt) ? sizeof(dns) : 0}, + {dns_addr, dns_cnt * sizeof(*dns_addr)}, + {&search, (search_len) ? sizeof(search) : 0}, + {search_domain, search_len}, + {pdbuf, 0}, + {NULL, 0}}; + + uint8_t *opts = (uint8_t*)&hdr[1], *opts_end = (uint8_t*)data + len; + if (hdr->msg_type == DHCPV6_MSG_RELAY_FORW) + handle_nested_message(data, len, &opts, &opts_end, iov); + + memcpy(dest.tr_id, &opts[-3], sizeof(dest.tr_id)); + + if (opts[-4] == DHCPV6_MSG_ADVERTISE || opts[-4] == DHCPV6_MSG_REPLY || opts[-4] == DHCPV6_MSG_RELAY_REPL) + return; + + if (opts[-4] == DHCPV6_MSG_SOLICIT) { + dest.msg_type = DHCPV6_MSG_ADVERTISE; + } else if (opts[-4] == DHCPV6_MSG_INFORMATION_REQUEST) { + iov[6].iov_base = &refresh; + iov[6].iov_len = sizeof(refresh); + } + + // Go through options and find what we need + uint16_t otype, olen; + uint8_t *odata; + dhcpv6_for_each_option(opts, opts_end, otype, olen, odata) { + if (otype == DHCPV6_OPT_CLIENTID && olen <= 130) { + dest.clientid_length = htons(olen); + memcpy(dest.clientid_buf, odata, olen); + iov[1].iov_len += 4 + olen; + } else if (otype == DHCPV6_OPT_SERVERID) { + if (olen != ntohs(dest.serverid_length) || + memcmp(odata, &dest.duid_type, olen)) + return; // Not for us + } + } + + if (opts[-4] != DHCPV6_MSG_INFORMATION_REQUEST) { + iov[6].iov_len = dhcpv6_handle_ia(pdbuf, sizeof(pdbuf), iface, addr, &opts[-4], opts_end); + if (iov[6].iov_len == 0 && opts[-4] == DHCPV6_MSG_REBIND) + return; + } + + if (iov[0].iov_len > 0) // Update length + update_nested_message(data, len, iov[1].iov_len + iov[2].iov_len + + iov[3].iov_len + iov[4].iov_len + iov[5].iov_len + + iov[6].iov_len - (4 + opts_end - opts)); + + odhcpd_send(dhcpv6_event.uloop.fd, addr, iov, ARRAY_SIZE(iov), iface); +} + + +// Central DHCPv6-relay handler +static void handle_dhcpv6(void *addr, void *data, size_t len, + struct interface *iface) +{ + if (iface->dhcpv6 == RELAYD_SERVER) { + handle_client_request(addr, data, len, iface); + } else if (iface->dhcpv6 == RELAYD_RELAY) { + if (iface->master) + relay_server_response(data, len); + else + relay_client_request(addr, data, len, iface); + } +} + + +// Relay server response (regular relay server handling) +static void relay_server_response(uint8_t *data, size_t len) +{ + // Information we need to gather + uint8_t *payload_data = NULL; + size_t payload_len = 0; + int32_t ifaceidx = 0; + struct sockaddr_in6 target = {AF_INET6, htons(DHCPV6_CLIENT_PORT), + 0, IN6ADDR_ANY_INIT, 0}; + + syslog(LOG_NOTICE, "Got a DHCPv6-reply"); + + int otype, olen; + uint8_t *odata, *end = data + len; + + // Relay DHCPv6 reply from server to client + struct dhcpv6_relay_header *h = (void*)data; + if (len < sizeof(*h) || h->msg_type != DHCPV6_MSG_RELAY_REPL) + return; + + memcpy(&target.sin6_addr, &h->peer_address, + sizeof(struct in6_addr)); + + // Go through options and find what we need + dhcpv6_for_each_option(h->options, end, otype, olen, odata) { + if (otype == DHCPV6_OPT_INTERFACE_ID + && olen == sizeof(ifaceidx)) { + memcpy(&ifaceidx, odata, sizeof(ifaceidx)); + } else if (otype == DHCPV6_OPT_RELAY_MSG) { + payload_data = odata; + payload_len = olen; + } + } + + // Invalid interface-id or basic payload + struct interface *iface = odhcpd_get_interface_by_index(ifaceidx); + if (!iface || iface->master || !payload_data || payload_len < 4) + return; + + bool is_authenticated = false; + struct in6_addr *dns_ptr = NULL; + size_t dns_count = 0; + + // If the payload is relay-reply we have to send to the server port + if (payload_data[0] == DHCPV6_MSG_RELAY_REPL) { + target.sin6_port = htons(DHCPV6_SERVER_PORT); + } else { // Go through the payload data + struct dhcpv6_client_header *h = (void*)payload_data; + end = payload_data + payload_len; + + dhcpv6_for_each_option(&h[1], end, otype, olen, odata) { + if (otype == DHCPV6_OPT_DNS_SERVERS && olen >= 16) { + dns_ptr = (struct in6_addr*)odata; + dns_count = olen / 16; + } else if (otype == DHCPV6_OPT_AUTH) { + is_authenticated = true; + } + } + } + + // Rewrite DNS servers if requested + if (iface->always_rewrite_dns && dns_ptr && dns_count > 0) { + if (is_authenticated) + return; // Impossible to rewrite + + struct odhcpd_ipaddr ip; + const struct in6_addr *rewrite = iface->dns; + size_t rewrite_cnt = iface->dns_cnt; + + if (rewrite_cnt == 0) { + if (odhcpd_get_interface_addresses(iface->ifindex, &ip, 1) < 1) + return; // Unable to get interface address + + rewrite = &ip.addr; + rewrite_cnt = 1; + } + + // Copy over any other addresses + for (size_t i = 0; i < dns_count; ++i) { + size_t j = (i < rewrite_cnt) ? i : rewrite_cnt - 1; + memcpy(&dns_ptr[i], &rewrite[j], sizeof(*rewrite)); + } + } + + struct iovec iov = {payload_data, payload_len}; + odhcpd_send(dhcpv6_event.uloop.fd, &target, &iov, 1, iface); +} + + +// Relay client request (regular DHCPv6-relay) +static void relay_client_request(struct sockaddr_in6 *source, + const void *data, size_t len, struct interface *iface) +{ + struct interface *master = odhcpd_get_master_interface(); + const struct dhcpv6_relay_header *h = data; + if (!master || master->dhcpv6 != RELAYD_RELAY || + h->msg_type == DHCPV6_MSG_RELAY_REPL || + h->msg_type == DHCPV6_MSG_RECONFIGURE || + h->msg_type == DHCPV6_MSG_REPLY || + h->msg_type == DHCPV6_MSG_ADVERTISE) + return; // Invalid message types for client + + syslog(LOG_NOTICE, "Got a DHCPv6-request"); + + // Construct our forwarding envelope + struct dhcpv6_relay_forward_envelope hdr = { + .msg_type = DHCPV6_MSG_RELAY_FORW, + .hop_count = 0, + .interface_id_type = htons(DHCPV6_OPT_INTERFACE_ID), + .interface_id_len = htons(sizeof(uint32_t)), + .relay_message_type = htons(DHCPV6_OPT_RELAY_MSG), + .relay_message_len = htons(len), + }; + + if (h->msg_type == DHCPV6_MSG_RELAY_FORW) { // handle relay-forward + if (h->hop_count >= DHCPV6_HOP_COUNT_LIMIT) + return; // Invalid hop count + else + hdr.hop_count = h->hop_count + 1; + } + + // use memcpy here as the destination fields are unaligned + uint32_t ifindex = iface->ifindex; + memcpy(&hdr.peer_address, &source->sin6_addr, sizeof(struct in6_addr)); + memcpy(&hdr.interface_id_data, &ifindex, sizeof(ifindex)); + + // Detect public IP of slave interface to use as link-address + struct odhcpd_ipaddr ip; + if (odhcpd_get_interface_addresses(iface->ifindex, &ip, 1) < 1) { + // No suitable address! Is the slave not configured yet? + // Detect public IP of master interface and use it instead + // This is WRONG and probably violates the RFC. However + // otherwise we have a hen and egg problem because the + // slave-interface cannot be auto-configured. + if (odhcpd_get_interface_addresses(master->ifindex, &ip, 1) < 1) + return; // Could not obtain a suitable address + } + memcpy(&hdr.link_address, &ip.addr, sizeof(hdr.link_address)); + + struct sockaddr_in6 dhcpv6_servers = {AF_INET6, + htons(DHCPV6_SERVER_PORT), 0, ALL_DHCPV6_SERVERS, 0}; + struct iovec iov[2] = {{&hdr, sizeof(hdr)}, {(void*)data, len}}; + odhcpd_send(dhcpv6_event.uloop.fd, &dhcpv6_servers, iov, 2, master); +} diff --git a/src/dhcpv6.h b/src/dhcpv6.h new file mode 100644 index 0000000..004c3cf --- /dev/null +++ b/src/dhcpv6.h @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2012 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License version 2 for more details. + * + */ +#pragma once + +#define ALL_DHCPV6_RELAYS {{{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02}}} + +#define ALL_DHCPV6_SERVERS {{{0xff, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03}}} + +#define DHCPV6_CLIENT_PORT 546 +#define DHCPV6_SERVER_PORT 547 + +#define DHCPV6_MSG_SOLICIT 1 +#define DHCPV6_MSG_ADVERTISE 2 +#define DHCPV6_MSG_REQUEST 3 +#define DHCPV6_MSG_CONFIRM 4 +#define DHCPV6_MSG_RENEW 5 +#define DHCPV6_MSG_REBIND 6 +#define DHCPV6_MSG_REPLY 7 +#define DHCPV6_MSG_RELEASE 8 +#define DHCPV6_MSG_DECLINE 9 +#define DHCPV6_MSG_RECONFIGURE 10 +#define DHCPV6_MSG_INFORMATION_REQUEST 11 +#define DHCPV6_MSG_RELAY_FORW 12 +#define DHCPV6_MSG_RELAY_REPL 13 + +#define DHCPV6_OPT_CLIENTID 1 +#define DHCPV6_OPT_SERVERID 2 +#define DHCPV6_OPT_IA_NA 3 +#define DHCPV6_OPT_IA_ADDR 5 +#define DHCPV6_OPT_STATUS 13 +#define DHCPV6_OPT_RELAY_MSG 9 +#define DHCPV6_OPT_AUTH 11 +#define DHCPV6_OPT_INTERFACE_ID 18 +#define DHCPV6_OPT_RECONF_MSG 19 +#define DHCPV6_OPT_RECONF_ACCEPT 20 +#define DHCPV6_OPT_DNS_SERVERS 23 +#define DHCPV6_OPT_DNS_DOMAIN 24 +#define DHCPV6_OPT_IA_PD 25 +#define DHCPV6_OPT_IA_PREFIX 26 +#define DHCPV6_OPT_INFO_REFRESH 32 +#define DHCPV6_OPT_FQDN 39 + +#define DHCPV6_DUID_VENDOR 2 + +#define DHCPV6_STATUS_OK 0 +#define DHCPV6_STATUS_NOADDRSAVAIL 2 +#define DHCPV6_STATUS_NOBINDING 3 +#define DHCPV6_STATUS_NOTONLINK 4 +#define DHCPV6_STATUS_NOPREFIXAVAIL 6 + +// I just remembered I have an old one lying around... +#define DHCPV6_ENT_NO 30462 +#define DHCPV6_ENT_TYPE 1 + + +#define DHCPV6_HOP_COUNT_LIMIT 32 + +struct dhcpv6_client_header { + uint8_t msg_type; + uint8_t transaction_id[3]; +} __attribute__((packed)); + +struct dhcpv6_relay_header { + uint8_t msg_type; + uint8_t hop_count; + struct in6_addr link_address; + struct in6_addr peer_address; + uint8_t options[]; +} __attribute__((packed)); + +struct dhcpv6_relay_forward_envelope { + uint8_t msg_type; + uint8_t hop_count; + struct in6_addr link_address; + struct in6_addr peer_address; + uint16_t interface_id_type; + uint16_t interface_id_len; + uint32_t interface_id_data; + uint16_t relay_message_type; + uint16_t relay_message_len; +} __attribute__((packed)); + +struct dhcpv6_auth_reconfigure { + uint16_t type; + uint16_t len; + uint8_t protocol; + uint8_t algorithm; + uint8_t rdm; + uint32_t replay[2]; + uint8_t reconf_type; + uint8_t key[16]; +} _packed; + +struct dhcpv6_ia_hdr { + uint16_t type; + uint16_t len; + uint32_t iaid; + uint32_t t1; + uint32_t t2; +} _packed; + +struct dhcpv6_ia_prefix { + uint16_t type; + uint16_t len; + uint32_t preferred; + uint32_t valid; + uint8_t prefix; + struct in6_addr addr; +} _packed; + +struct dhcpv6_ia_addr { + uint16_t type; + uint16_t len; + struct in6_addr addr; + uint32_t preferred; + uint32_t valid; +} _packed; + +struct dhcpv6_assignment { + struct list_head head; + struct sockaddr_in6 peer; + time_t valid_until; + time_t reconf_sent; + int reconf_cnt; + char *hostname; + uint8_t key[16]; + uint32_t assigned; + uint32_t iaid; + uint8_t mac[6]; + uint8_t length; // length == 128 -> IA_NA, length <= 64 -> IA_PD + bool accept_reconf; + uint8_t clid_len; + uint8_t clid_data[]; +}; + + + +#define dhcpv6_for_each_option(start, end, otype, olen, odata)\ + for (uint8_t *_o = (uint8_t*)(start); _o + 4 <= (end) &&\ + ((otype) = _o[0] << 8 | _o[1]) && ((odata) = (void*)&_o[4]) &&\ + ((olen) = _o[2] << 8 | _o[3]) + (odata) <= (end); \ + _o += 4 + (_o[2] << 8 | _o[3])) + +int dhcpv6_init_ia(struct interface *iface, int socket); +size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface, + const struct sockaddr_in6 *addr, const void *data, const uint8_t *end); +int dhcpv6_ia_init(int dhcpv6_socket); +int setup_dhcpv6_ia_interface(struct interface *iface, bool enable); +void dhcpv6_write_statefile(void); diff --git a/src/md5.c b/src/md5.c new file mode 100644 index 0000000..ec24dd2 --- /dev/null +++ b/src/md5.c @@ -0,0 +1,242 @@ +/* + * md5.c - Compute MD5 checksum of strings according to the + * definition of MD5 in RFC 1321 from April 1992. + * + * Written by Ulrich Drepper <drepper@gnu.ai.mit.edu>, 1995. + * + * Copyright (C) 1995-1999 Free Software Foundation, Inc. + * Copyright (C) 2001 Manuel Novoa III + * Copyright (C) 2003 Glenn L. McGrath + * Copyright (C) 2003 Erik Andersen + * + * Licensed under the GPL v2 or later, see the file LICENSE in this tarball. + */ + +#include <libubox/blob.h> /* TODO: better include for bswap_32 compat */ +#include "md5.h" + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define SWAP_LE32(x) (x) +#else +#define SWAP_LE32(x) bswap_32(x) +#endif + +/* Initialize structure containing state of computation. + * (RFC 1321, 3.3: Step 3) + */ +void md5_begin(md5_ctx_t *ctx) +{ + ctx->A = 0x67452301; + ctx->B = 0xefcdab89; + ctx->C = 0x98badcfe; + ctx->D = 0x10325476; + + ctx->total = 0; + ctx->buflen = 0; +} + +/* These are the four functions used in the four steps of the MD5 algorithm + * and defined in the RFC 1321. The first function is a little bit optimized + * (as found in Colin Plumbs public domain implementation). + * #define FF(b, c, d) ((b & c) | (~b & d)) + */ +# define FF(b, c, d) (d ^ (b & (c ^ d))) +# define FG(b, c, d) FF (d, b, c) +# define FH(b, c, d) (b ^ c ^ d) +# define FI(b, c, d) (c ^ (b | ~d)) + +/* Hash a single block, 64 bytes long and 4-byte aligned. */ +static void md5_hash_block(const void *buffer, md5_ctx_t *ctx) +{ + uint32_t correct_words[16]; + const uint32_t *words = buffer; + + static const uint32_t C_array[] = { + /* round 1 */ + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + /* round 2 */ + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + /* round 3 */ + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + /* round 4 */ + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 + }; + + static const char P_array[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, /* 1 */ + 1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, /* 2 */ + 5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2, /* 3 */ + 0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9 /* 4 */ + }; + + static const char S_array[] = { + 7, 12, 17, 22, + 5, 9, 14, 20, + 4, 11, 16, 23, + 6, 10, 15, 21 + }; + + uint32_t A = ctx->A; + uint32_t B = ctx->B; + uint32_t C = ctx->C; + uint32_t D = ctx->D; + + uint32_t *cwp = correct_words; + +# define CYCLIC(w, s) (w = (w << s) | (w >> (32 - s))) + + const uint32_t *pc; + const char *pp; + const char *ps; + int i; + uint32_t temp; + + for (i = 0; i < 16; i++) { + cwp[i] = SWAP_LE32(words[i]); + } + words += 16; + + pc = C_array; + pp = P_array; + ps = S_array; + + for (i = 0; i < 16; i++) { + temp = A + FF(B, C, D) + cwp[(int) (*pp++)] + *pc++; + CYCLIC(temp, ps[i & 3]); + temp += B; + A = D; + D = C; + C = B; + B = temp; + } + + ps += 4; + for (i = 0; i < 16; i++) { + temp = A + FG(B, C, D) + cwp[(int) (*pp++)] + *pc++; + CYCLIC(temp, ps[i & 3]); + temp += B; + A = D; + D = C; + C = B; + B = temp; + } + ps += 4; + for (i = 0; i < 16; i++) { + temp = A + FH(B, C, D) + cwp[(int) (*pp++)] + *pc++; + CYCLIC(temp, ps[i & 3]); + temp += B; + A = D; + D = C; + C = B; + B = temp; + } + ps += 4; + for (i = 0; i < 16; i++) { + temp = A + FI(B, C, D) + cwp[(int) (*pp++)] + *pc++; + CYCLIC(temp, ps[i & 3]); + temp += B; + A = D; + D = C; + C = B; + B = temp; + } + + + ctx->A += A; + ctx->B += B; + ctx->C += C; + ctx->D += D; +} + +/* Feed data through a temporary buffer to call md5_hash_aligned_block() + * with chunks of data that are 4-byte aligned and a multiple of 64 bytes. + * This function's internal buffer remembers previous data until it has 64 + * bytes worth to pass on. Call md5_end() to flush this buffer. */ + +void md5_hash(const void *buffer, size_t len, md5_ctx_t *ctx) +{ + char *buf = (char *)buffer; + + /* RFC 1321 specifies the possible length of the file up to 2^64 bits, + * Here we only track the number of bytes. */ + + ctx->total += len; + + // Process all input. + + while (len) { + unsigned i = 64 - ctx->buflen; + + // Copy data into aligned buffer. + + if (i > len) + i = len; + memcpy(ctx->buffer + ctx->buflen, buf, i); + len -= i; + ctx->buflen += i; + buf += i; + + // When buffer fills up, process it. + + if (ctx->buflen == 64) { + md5_hash_block(ctx->buffer, ctx); + ctx->buflen = 0; + } + } +} + +/* Process the remaining bytes in the buffer and put result from CTX + * in first 16 bytes following RESBUF. The result is always in little + * endian byte order, so that a byte-wise output yields to the wanted + * ASCII representation of the message digest. + * + * IMPORTANT: On some systems it is required that RESBUF is correctly + * aligned for a 32 bits value. + */ +void md5_end(void *resbuf, md5_ctx_t *ctx) +{ + char *buf = ctx->buffer; + int i; + + /* Pad data to block size. */ + + buf[ctx->buflen++] = 0x80; + memset(buf + ctx->buflen, 0, 128 - ctx->buflen); + + /* Put the 64-bit file length in *bits* at the end of the buffer. */ + ctx->total <<= 3; + if (ctx->buflen > 56) + buf += 64; + + for (i = 0; i < 8; i++) + buf[56 + i] = ctx->total >> (i*8); + + /* Process last bytes. */ + if (buf != ctx->buffer) + md5_hash_block(ctx->buffer, ctx); + md5_hash_block(buf, ctx); + + /* Put result from CTX in first 16 bytes following RESBUF. The result is + * always in little endian byte order, so that a byte-wise output yields + * to the wanted ASCII representation of the message digest. + * + * IMPORTANT: On some systems it is required that RESBUF is correctly + * aligned for a 32 bits value. + */ + ((uint32_t *) resbuf)[0] = SWAP_LE32(ctx->A); + ((uint32_t *) resbuf)[1] = SWAP_LE32(ctx->B); + ((uint32_t *) resbuf)[2] = SWAP_LE32(ctx->C); + ((uint32_t *) resbuf)[3] = SWAP_LE32(ctx->D); +} diff --git a/src/md5.h b/src/md5.h new file mode 100644 index 0000000..fb79ae0 --- /dev/null +++ b/src/md5.h @@ -0,0 +1,17 @@ +#pragma once +#include <stdint.h> +#include <stddef.h> + +typedef struct md5_ctx { + uint32_t A; + uint32_t B; + uint32_t C; + uint32_t D; + uint64_t total; + uint32_t buflen; + char buffer[128]; +} md5_ctx_t; + +void md5_begin(md5_ctx_t *ctx); +void md5_hash(const void *data, size_t length, md5_ctx_t *ctx); +void md5_end(void *resbuf, md5_ctx_t *ctx); diff --git a/src/ndp.c b/src/ndp.c new file mode 100644 index 0000000..89bcd3c --- /dev/null +++ b/src/ndp.c @@ -0,0 +1,532 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <stdio.h> +#include <stdlib.h> +#include <signal.h> +#include <errno.h> + +#include <arpa/inet.h> +#include <sys/socket.h> +#include <net/ethernet.h> +#include <netinet/ip6.h> +#include <netinet/icmp6.h> +#include <netpacket/packet.h> + +#include <linux/rtnetlink.h> +#include <linux/filter.h> +#include "router.h" +#include "ndp.h" + + + +static void handle_solicit(void *addr, void *data, size_t len, + struct interface *iface); +static void handle_rtnetlink(void *addr, void *data, size_t len, + struct interface *iface); +static struct ndp_neighbor* find_neighbor(struct in6_addr *addr, bool strict); +static void modify_neighbor(struct in6_addr *addr, struct interface *iface, + bool add); +static ssize_t ping6(struct in6_addr *addr, + const struct interface *iface); + +static struct list_head neighbors = LIST_HEAD_INIT(neighbors); +static size_t neighbor_count = 0; +static uint32_t rtnl_seqid = 0; + +static int ping_socket = -1; +static struct odhcpd_event ndp_event = {{.fd = -1}, handle_solicit}; +static struct odhcpd_event rtnl_event = {{.fd = -1}, handle_rtnetlink}; + + +// Filter ICMPv6 messages of type neighbor soliciation +static struct sock_filter bpf[] = { + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, offsetof(struct ip6_hdr, ip6_nxt)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3), + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, sizeof(struct ip6_hdr) + + offsetof(struct icmp6_hdr, icmp6_type)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_NEIGHBOR_SOLICIT, 0, 1), + BPF_STMT(BPF_RET | BPF_K, 0xffffffff), + BPF_STMT(BPF_RET | BPF_K, 0), +}; +static const struct sock_fprog bpf_prog = {sizeof(bpf) / sizeof(*bpf), bpf}; + + +// Initialize NDP-proxy +int init_ndp(void) +{ + // Setup netlink socket + if ((rtnl_event.uloop.fd = odhcpd_open_rtnl()) < 0) + return -1; + + // Receive netlink neighbor and ip-address events + uint32_t group = RTNLGRP_IPV6_IFADDR; + setsockopt(rtnl_event.uloop.fd, SOL_NETLINK, + NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)); + group = RTNLGRP_IPV6_ROUTE; + setsockopt(rtnl_event.uloop.fd, SOL_NETLINK, + NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)); + + // Synthesize initial address events + struct { + struct nlmsghdr nh; + struct ifaddrmsg ifa; + } req2 = { + {sizeof(req2), RTM_GETADDR, NLM_F_REQUEST | NLM_F_DUMP, + ++rtnl_seqid, 0}, + {.ifa_family = AF_INET6} + }; + send(rtnl_event.uloop.fd, &req2, sizeof(req2), MSG_DONTWAIT); + odhcpd_register(&rtnl_event); + + + // Create socket for intercepting NDP + int sock = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, + htons(ETH_P_ALL)); // ETH_P_ALL for ingress + egress + if (sock < 0) { + syslog(LOG_ERR, "Unable to open packet socket: %s", + strerror(errno)); + return -1; + } + + if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, + &bpf_prog, sizeof(bpf_prog))) { + syslog(LOG_ERR, "Failed to set BPF: %s", strerror(errno)); + return -1; + } + + ndp_event.uloop.fd = sock; + odhcpd_register(&ndp_event); + + // Open ICMPv6 socket + ping_socket = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMPV6); + + int val = 2; + setsockopt(ping_socket, IPPROTO_RAW, IPV6_CHECKSUM, &val, sizeof(val)); + + // This is required by RFC 4861 + val = 255; + setsockopt(ping_socket, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val)); + setsockopt(ping_socket, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &val, sizeof(val)); + + // Filter all packages, we only want to send + struct icmp6_filter filt; + ICMP6_FILTER_SETBLOCKALL(&filt); + setsockopt(ping_socket, IPPROTO_ICMPV6, ICMP6_FILTER, &filt, sizeof(filt)); + + + // Netlink socket, continued... + group = RTNLGRP_NEIGH; + setsockopt(rtnl_event.uloop.fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)); + + // Synthesize initial neighbor events + struct { + struct nlmsghdr nh; + struct ndmsg ndm; + } req = { + {sizeof(req), RTM_GETNEIGH, NLM_F_REQUEST | NLM_F_DUMP, + ++rtnl_seqid, 0}, + {.ndm_family = AF_INET6} + }; + send(rtnl_event.uloop.fd, &req, sizeof(req), MSG_DONTWAIT); + + return 0; +} + + +int setup_ndp_interface(struct interface *iface, bool enable) +{ + struct packet_mreq mreq = {iface->ifindex, PACKET_MR_ALLMULTI, ETH_ALEN, {0}}; + setsockopt(ndp_event.uloop.fd, SOL_PACKET, PACKET_DROP_MEMBERSHIP, &mreq, sizeof(mreq)); + + struct ndp_neighbor *c, *n; + list_for_each_entry_safe(c, n, &neighbors, head) + if (c->iface == iface && (c->timeout == 0 || iface->ndp != RELAYD_RELAY || !enable)) + modify_neighbor(&c->addr, c->iface, false); + + if (enable && iface->ndp == RELAYD_RELAY) { + setsockopt(ndp_event.uloop.fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)); + + if (iface->static_ndp_len) { + char *entry = alloca(iface->static_ndp_len), *saveptr; + memcpy(entry, iface->static_ndp, iface->static_ndp_len); + + for (entry = strtok_r(entry, " ", &saveptr); entry; entry = strtok_r(NULL, " ", &saveptr)) { + struct ndp_neighbor *n = malloc(sizeof(*n)); + n->iface = iface; + n->timeout = 0; + + char ipbuf[INET6_ADDRSTRLEN]; + if (sscanf(entry, "%45s/%hhu", ipbuf, &n->len) < 2 + || n->len > 128 || inet_pton(AF_INET6, ipbuf, &n->addr) != 1) { + syslog(LOG_ERR, "Invalid static NDP-prefix %s", entry); + return -1; + } + + list_add(&n->head, &neighbors); + } + } + } + + return 0; +} + + +// Send an ICMP-ECHO. This is less for actually pinging but for the +// neighbor cache to be kept up-to-date. +static ssize_t ping6(struct in6_addr *addr, + const struct interface *iface) +{ + struct sockaddr_in6 dest = {AF_INET6, 0, 0, *addr, 0}; + struct icmp6_hdr echo = {.icmp6_type = ICMP6_ECHO_REQUEST}; + struct iovec iov = {&echo, sizeof(echo)}; + + // Linux seems to not honor IPV6_PKTINFO on raw-sockets, so work around + setsockopt(ping_socket, SOL_SOCKET, SO_BINDTODEVICE, + iface->ifname, sizeof(iface->ifname)); + return odhcpd_send(ping_socket, &dest, &iov, 1, iface); +} + + +// Handle solicitations +static void handle_solicit(void *addr, void *data, size_t len, + struct interface *iface) +{ + struct ip6_hdr *ip6 = data; + struct nd_neighbor_solicit *req = (struct nd_neighbor_solicit*)&ip6[1]; + struct sockaddr_ll *ll = addr; + + // Solicitation is for duplicate address detection + bool ns_is_dad = IN6_IS_ADDR_UNSPECIFIED(&ip6->ip6_src); + + // Don't forward any non-DAD solicitation for external ifaces + // TODO: check if we should even forward DADs for them + if (iface->external && !ns_is_dad) + return; + + if (len < sizeof(*ip6) + sizeof(*req)) + return; // Invalid reqicitation + + if (IN6_IS_ADDR_LINKLOCAL(&req->nd_ns_target) || + IN6_IS_ADDR_LOOPBACK(&req->nd_ns_target) || + IN6_IS_ADDR_MULTICAST(&req->nd_ns_target)) + return; // Invalid target + + char ipbuf[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &req->nd_ns_target, ipbuf, sizeof(ipbuf)); + syslog(LOG_NOTICE, "Got a NS for %s", ipbuf); + + uint8_t mac[6]; + odhcpd_get_mac(iface, mac); + if (!memcmp(ll->sll_addr, mac, sizeof(mac)) && + ll->sll_pkttype != PACKET_OUTGOING) + return; // Looped back + + time_t now = time(NULL); + + struct ndp_neighbor *n = find_neighbor(&req->nd_ns_target, false); + if (n && (n->iface || abs(n->timeout - now) < 5)) { + syslog(LOG_NOTICE, "%s is on %s", ipbuf, + (n->iface) ? n->iface->ifname : "<pending>"); + if (!n->iface || n->iface == iface) + return; + + // Found on other interface, answer with advertisement + struct { + struct nd_neighbor_advert body; + struct nd_opt_hdr opt_ll_hdr; + uint8_t mac[6]; + } advert = { + .body = { + .nd_na_hdr = {ND_NEIGHBOR_ADVERT, + 0, 0, {{0}}}, + .nd_na_target = req->nd_ns_target, + }, + .opt_ll_hdr = {ND_OPT_TARGET_LINKADDR, 1}, + }; + + memcpy(advert.mac, mac, sizeof(advert.mac)); + advert.body.nd_na_flags_reserved = ND_NA_FLAG_ROUTER | + ND_NA_FLAG_SOLICITED; + + struct sockaddr_in6 dest = {AF_INET6, 0, 0, ALL_IPV6_NODES, 0}; + if (!ns_is_dad) // If not DAD, then unicast to source + dest.sin6_addr = ip6->ip6_src; + + // Linux seems to not honor IPV6_PKTINFO on raw-sockets, so work around + setsockopt(ping_socket, SOL_SOCKET, SO_BINDTODEVICE, + iface->ifname, sizeof(iface->ifname)); + struct iovec iov = {&advert, sizeof(advert)}; + odhcpd_send(ping_socket, &dest, &iov, 1, iface); + } else { + // Send echo to all other interfaces to see where target is on + // This will trigger neighbor discovery which is what we want. + // We will observe the neighbor cache to see results. + + ssize_t sent = 0; + struct interface *c; + list_for_each_entry(c, &interfaces, head) + if (iface->ndp == RELAYD_RELAY && iface != c && + (!ns_is_dad || !c->external == false)) + sent += ping6(&req->nd_ns_target, c); + + if (sent > 0) // Sent a ping, add pending neighbor entry + modify_neighbor(&req->nd_ns_target, NULL, true); + } +} + + +void odhcpd_setup_route(const struct in6_addr *addr, int prefixlen, + const struct interface *iface, const struct in6_addr *gw, bool add) +{ + struct req { + struct nlmsghdr nh; + struct rtmsg rtm; + struct rtattr rta_dst; + struct in6_addr dst_addr; + struct rtattr rta_oif; + uint32_t ifindex; + struct rtattr rta_table; + uint32_t table; + struct rtattr rta_gw; + struct in6_addr gw; + } req = { + {sizeof(req), 0, NLM_F_REQUEST, ++rtnl_seqid, 0}, + {AF_INET6, prefixlen, 0, 0, 0, 0, 0, 0, 0}, + {sizeof(struct rtattr) + sizeof(struct in6_addr), RTA_DST}, + *addr, + {sizeof(struct rtattr) + sizeof(uint32_t), RTA_OIF}, + iface->ifindex, + {sizeof(struct rtattr) + sizeof(uint32_t), RTA_TABLE}, + RT_TABLE_MAIN, + {sizeof(struct rtattr) + sizeof(struct in6_addr), RTA_GATEWAY}, + IN6ADDR_ANY_INIT, + }; + + if (gw) + req.gw = *gw; + + if (add) { + req.nh.nlmsg_type = RTM_NEWROUTE; + req.nh.nlmsg_flags |= (NLM_F_CREATE | NLM_F_REPLACE); + req.rtm.rtm_protocol = RTPROT_BOOT; + req.rtm.rtm_scope = (gw) ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK; + req.rtm.rtm_type = RTN_UNICAST; + } else { + req.nh.nlmsg_type = RTM_DELROUTE; + req.rtm.rtm_scope = RT_SCOPE_NOWHERE; + } + + size_t reqlen = (gw) ? sizeof(req) : offsetof(struct req, rta_gw); + send(rtnl_event.uloop.fd, &req, reqlen, MSG_DONTWAIT); +} + +// Use rtnetlink to modify kernel routes +static void setup_route(struct in6_addr *addr, struct interface *iface, + bool add) +{ + char namebuf[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, addr, namebuf, sizeof(namebuf)); + syslog(LOG_NOTICE, "%s about %s on %s", (add) ? "Learned" : "Forgot", + namebuf, (iface) ? iface->ifname : "<pending>"); + + if (!iface || !iface->learn_routes) + return; + + odhcpd_setup_route(addr, 128, iface, NULL, add); +} + +static void free_neighbor(struct ndp_neighbor *n) +{ + setup_route(&n->addr, n->iface, false); + list_del(&n->head); + free(n); + --neighbor_count; +} + + +static bool match_neighbor(struct ndp_neighbor *n, struct in6_addr *addr) +{ + if (n->len <= 32) + return ntohl(n->addr.s6_addr32[0]) >> (32 - n->len) == + ntohl(addr->s6_addr32[0]) >> (32 - n->len); + + if (n->addr.s6_addr32[0] != addr->s6_addr32[0]) + return false; + + if (n->len <= 64) + return ntohl(n->addr.s6_addr32[1]) >> (64 - n->len) == + ntohl(addr->s6_addr32[1]) >> (64 - n->len); + + if (n->addr.s6_addr32[1] != addr->s6_addr32[1]) + return false; + + if (n->len <= 96) + return ntohl(n->addr.s6_addr32[2]) >> (96 - n->len) == + ntohl(addr->s6_addr32[2]) >> (96 - n->len); + + if (n->addr.s6_addr32[2] != addr->s6_addr32[2]) + return false; + + return ntohl(n->addr.s6_addr32[3]) >> (128 - n->len) == + ntohl(addr->s6_addr32[3]) >> (128 - n->len); +} + + +static struct ndp_neighbor* find_neighbor(struct in6_addr *addr, bool strict) +{ + time_t now = time(NULL); + struct ndp_neighbor *n, *e; + list_for_each_entry_safe(n, e, &neighbors, head) { + if ((!strict && match_neighbor(n, addr)) || + (n->len == 128 && IN6_ARE_ADDR_EQUAL(&n->addr, addr))) + return n; + + if (!n->iface && abs(n->timeout - now) >= 5) + free_neighbor(n); + } + return NULL; +} + + +// Modified our own neighbor-entries +static void modify_neighbor(struct in6_addr *addr, + struct interface *iface, bool add) +{ + if (!addr || (void*)addr == (void*)iface) + return; + + struct ndp_neighbor *n = find_neighbor(addr, true); + if (!add) { // Delete action + if (n && (!n->iface || n->iface == iface)) + free_neighbor(n); + } else if (!n) { // No entry yet, add one if possible + if (neighbor_count >= NDP_MAX_NEIGHBORS || + !(n = malloc(sizeof(*n)))) + return; + + n->len = 128; + n->addr = *addr; + n->iface = iface; + if (!n->iface) + time(&n->timeout); + list_add(&n->head, &neighbors); + ++neighbor_count; + setup_route(addr, n->iface, add); + } else if (n->iface == iface) { + if (!n->iface) + time(&n->timeout); + } else if (iface && (!n->iface || + (!iface->external && n->iface->external))) { + setup_route(addr, n->iface, false); + n->iface = iface; + setup_route(addr, n->iface, add); + } + // TODO: In case a host switches interfaces we might want + // to set its old neighbor entry to NUD_STALE and ping it + // on the old interface to confirm if the MACs match. +} + + +// Handler for neighbor cache entries from the kernel. This is our source +// to learn and unlearn hosts on interfaces. +static void handle_rtnetlink(_unused void *addr, void *data, size_t len, + _unused struct interface *iface) +{ + for (struct nlmsghdr *nh = data; NLMSG_OK(nh, len); + nh = NLMSG_NEXT(nh, len)) { + struct rtmsg *rtm = NLMSG_DATA(nh); + if ((nh->nlmsg_type == RTM_NEWROUTE || + nh->nlmsg_type == RTM_DELROUTE) && + rtm->rtm_dst_len == 0) + raise(SIGUSR1); // Inform about a change in default route + + struct ndmsg *ndm = NLMSG_DATA(nh); + struct ifaddrmsg *ifa = NLMSG_DATA(nh); + if (nh->nlmsg_type != RTM_NEWNEIGH + && nh->nlmsg_type != RTM_DELNEIGH + && nh->nlmsg_type != RTM_NEWADDR + && nh->nlmsg_type != RTM_DELADDR) + continue; // Unrelated message type + bool is_addr = (nh->nlmsg_type == RTM_NEWADDR + || nh->nlmsg_type == RTM_DELADDR); + + // Family and ifindex are on the same offset for NEIGH and ADDR + if (NLMSG_PAYLOAD(nh, 0) < sizeof(*ndm) + || ndm->ndm_family != AF_INET6) + continue; // + + // Lookup interface + struct interface *iface; + if (!(iface = odhcpd_get_interface_by_index(ndm->ndm_ifindex))) + continue; + + // Data to retrieve + size_t rta_offset = (is_addr) ? sizeof(*ifa) : sizeof(*ndm); + uint16_t atype = (is_addr) ? IFA_ADDRESS : NDA_DST; + ssize_t alen = NLMSG_PAYLOAD(nh, rta_offset); + struct in6_addr *addr = NULL; + + for (struct rtattr *rta = (void*)(((uint8_t*)ndm) + rta_offset); + RTA_OK(rta, alen); rta = RTA_NEXT(rta, alen)) + if (rta->rta_type == atype && + RTA_PAYLOAD(rta) >= sizeof(*addr)) + addr = RTA_DATA(rta); + + // Address not specified or unrelated + if (!addr || IN6_IS_ADDR_LINKLOCAL(addr) || + IN6_IS_ADDR_MULTICAST(addr)) + continue; + + // Check for states + bool add; + if (is_addr) + add = (nh->nlmsg_type == RTM_NEWADDR); + else + add = (nh->nlmsg_type == RTM_NEWNEIGH && (ndm->ndm_state & + (NUD_REACHABLE | NUD_STALE | NUD_DELAY | NUD_PROBE + | NUD_PERMANENT | NUD_NOARP))); + + if (iface->ndp == RELAYD_RELAY) + modify_neighbor(addr, iface, add); + + if (is_addr && iface->ra == RELAYD_SERVER) + raise(SIGUSR1); // Inform about a change in addresses + + if (is_addr && iface->dhcpv6 == RELAYD_SERVER) + iface->ia_reconf = true; + + if (iface->ndp == RELAYD_RELAY && is_addr && iface->master) { + // Replay address changes on all slave interfaces + nh->nlmsg_flags = NLM_F_REQUEST; + + if (nh->nlmsg_type == RTM_NEWADDR) + nh->nlmsg_flags |= NLM_F_CREATE | NLM_F_REPLACE; + + struct interface *c; + list_for_each_entry(c, &interfaces, head) { + if (c->ndp == RELAYD_RELAY && !c->master) { + ifa->ifa_index = c->ifindex; + send(rtnl_event.uloop.fd, nh, nh->nlmsg_len, MSG_DONTWAIT); + } + } + } + + /* TODO: See if this is required for optimal operation + // Keep neighbor entries alive so we don't loose routes + if (add && (ndm->ndm_state & NUD_STALE)) + ping6(addr, iface); + */ + } +} diff --git a/src/ndp.h b/src/ndp.h new file mode 100644 index 0000000..abd5b0a --- /dev/null +++ b/src/ndp.h @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#pragma once +#include "odhcpd.h" +#include <time.h> + +#ifndef SOL_NETLINK +#define SOL_NETLINK 270 +#endif + +#define NDP_MAX_NEIGHBORS 1000 + +struct ndp_neighbor { + struct list_head head; + struct interface *iface; + struct in6_addr addr; + uint8_t len; + time_t timeout; +}; diff --git a/src/odhcpd.c b/src/odhcpd.c new file mode 100644 index 0000000..6070bfb --- /dev/null +++ b/src/odhcpd.c @@ -0,0 +1,423 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <time.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <resolv.h> +#include <getopt.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> +#include <stdbool.h> + +#include <arpa/inet.h> +#include <net/if.h> +#include <netinet/ip6.h> +#include <netpacket/packet.h> +#include <linux/rtnetlink.h> + +#include <sys/socket.h> +#include <sys/ioctl.h> +#include <sys/epoll.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/syscall.h> + +#include <libubox/uloop.h> +#include "odhcpd.h" + + + +static int ioctl_sock; +static int rtnl_socket = -1; +static int rtnl_seq = 0; +static int urandom_fd = -1; + + +int main() +{ + openlog("odhcpd", LOG_PERROR | LOG_PID, LOG_DAEMON); + uloop_init(); + + if (getuid() != 0) { + syslog(LOG_ERR, "Must be run as root!"); + return 2; + } + + ioctl_sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + + if ((rtnl_socket = odhcpd_open_rtnl()) < 0) { + syslog(LOG_ERR, "Unable to open socket: %s", strerror(errno)); + return 2; + } + + if ((urandom_fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC)) < 0) + return 4; + + signal(SIGUSR1, SIG_IGN); + + if (init_router()) + return 4; + + if (init_dhcpv6()) + return 4; + + if (init_ndp()) + return 4; + + if (init_dhcpv4()) + return 4; + + if (init_ubus()) + return 4; + + odhcpd_run(); + return 0; +} + +int odhcpd_open_rtnl(void) +{ + int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + + // Connect to the kernel netlink interface + struct sockaddr_nl nl = {.nl_family = AF_NETLINK}; + if (connect(sock, (struct sockaddr*)&nl, sizeof(nl))) { + syslog(LOG_ERR, "Failed to connect to kernel rtnetlink: %s", + strerror(errno)); + return -1; + } + + return sock; +} + + +// Read IPv6 MTU for interface +int odhcpd_get_interface_mtu(const char *ifname) +{ + char buf[64]; + const char *sysctl_pattern = "/proc/sys/net/ipv6/conf/%s/mtu"; + snprintf(buf, sizeof(buf), sysctl_pattern, ifname); + + int fd = open(buf, O_RDONLY); + ssize_t len = read(fd, buf, sizeof(buf) - 1); + close(fd); + + if (len < 0) + return -1; + + + buf[len] = 0; + return atoi(buf); + +} + + +// Read IPv6 MAC for interface +int odhcpd_get_mac(const struct interface *iface, uint8_t mac[6]) +{ + struct ifreq ifr; + strncpy(ifr.ifr_name, iface->ifname, sizeof(ifr.ifr_name)); + if (ioctl(ioctl_sock, SIOCGIFHWADDR, &ifr) < 0) + return -1; + memcpy(mac, ifr.ifr_hwaddr.sa_data, 6); + return 0; +} + + +// Forwards a packet on a specific interface +ssize_t odhcpd_send(int socket, struct sockaddr_in6 *dest, + struct iovec *iov, size_t iov_len, + const struct interface *iface) +{ + // Construct headers + uint8_t cmsg_buf[CMSG_SPACE(sizeof(struct in6_pktinfo))] = {0}; + struct msghdr msg = {(void*)dest, sizeof(*dest), iov, iov_len, + cmsg_buf, sizeof(cmsg_buf), 0}; + + // Set control data (define destination interface) + struct cmsghdr *chdr = CMSG_FIRSTHDR(&msg); + chdr->cmsg_level = IPPROTO_IPV6; + chdr->cmsg_type = IPV6_PKTINFO; + chdr->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo)); + struct in6_pktinfo *pktinfo = (struct in6_pktinfo*)CMSG_DATA(chdr); + pktinfo->ipi6_ifindex = iface->ifindex; + + // Also set scope ID if link-local + if (IN6_IS_ADDR_LINKLOCAL(&dest->sin6_addr) + || IN6_IS_ADDR_MC_LINKLOCAL(&dest->sin6_addr)) + dest->sin6_scope_id = iface->ifindex; + + // IPV6_PKTINFO doesn't really work for IPv6-raw sockets (bug?) + if (dest->sin6_port == 0) { + msg.msg_control = NULL; + msg.msg_controllen = 0; + } + + char ipbuf[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &dest->sin6_addr, ipbuf, sizeof(ipbuf)); + + ssize_t sent = sendmsg(socket, &msg, MSG_DONTWAIT); + if (sent < 0) + syslog(LOG_WARNING, "Failed to send to %s%%%s (%s)", + ipbuf, iface->ifname, strerror(errno)); + else + syslog(LOG_NOTICE, "Sent %li bytes to %s%%%s", + (long)sent, ipbuf, iface->ifname); + return sent; +} + + +// Detect an IPV6-address currently assigned to the given interface +ssize_t odhcpd_get_interface_addresses(int ifindex, + struct odhcpd_ipaddr *addrs, size_t cnt) +{ + struct { + struct nlmsghdr nhm; + struct ifaddrmsg ifa; + } req = {{sizeof(req), RTM_GETADDR, NLM_F_REQUEST | NLM_F_DUMP, + ++rtnl_seq, 0}, {AF_INET6, 0, 0, 0, ifindex}}; + if (send(rtnl_socket, &req, sizeof(req), 0) < (ssize_t)sizeof(req)) + return 0; + + uint8_t buf[8192]; + ssize_t len = 0, ret = 0; + + for (struct nlmsghdr *nhm = NULL; ; nhm = NLMSG_NEXT(nhm, len)) { + while (len < 0 || !NLMSG_OK(nhm, (size_t)len)) { + len = recv(rtnl_socket, buf, sizeof(buf), 0); + nhm = (struct nlmsghdr*)buf; + if (len < 0 || !NLMSG_OK(nhm, (size_t)len)) { + if (errno == EINTR) + continue; + else + return ret; + } + } + + if (nhm->nlmsg_type != RTM_NEWADDR) + break; + + // Skip address but keep clearing socket buffer + if (ret >= (ssize_t)cnt) + continue; + + struct ifaddrmsg *ifa = NLMSG_DATA(nhm); + if (ifa->ifa_scope != RT_SCOPE_UNIVERSE || + ifa->ifa_index != (unsigned)ifindex) + continue; + + struct rtattr *rta = (struct rtattr*)&ifa[1]; + size_t alen = NLMSG_PAYLOAD(nhm, sizeof(*ifa)); + memset(&addrs[ret], 0, sizeof(addrs[ret])); + addrs[ret].prefix = ifa->ifa_prefixlen; + + while (RTA_OK(rta, alen)) { + if (rta->rta_type == IFA_ADDRESS) { + memcpy(&addrs[ret].addr, RTA_DATA(rta), + sizeof(struct in6_addr)); + } else if (rta->rta_type == IFA_CACHEINFO) { + struct ifa_cacheinfo *ifc = RTA_DATA(rta); + addrs[ret].preferred = ifc->ifa_prefered; + addrs[ret].valid = ifc->ifa_valid; + } + + rta = RTA_NEXT(rta, alen); + } + + if (ifa->ifa_flags & IFA_F_DEPRECATED) + addrs[ret].preferred = 0; + + ++ret; + } + + return ret; +} + + +struct interface* odhcpd_get_interface_by_index(int ifindex) +{ + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) + if (iface->ifindex == ifindex) + return iface; + + return NULL; +} + + +struct interface* odhcpd_get_interface_by_name(const char *name) +{ + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) + if (!strcmp(iface->ifname, name)) + return iface; + + return NULL; +} + + +struct interface* odhcpd_get_master_interface(void) +{ + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) + if (iface->master) + return iface; + + return NULL; +} + + +// Convenience function to receive and do basic validation of packets +static void odhcpd_receive_packets(struct uloop_fd *u, _unused unsigned int events) +{ + struct odhcpd_event *e = container_of(u, struct odhcpd_event, uloop); + + uint8_t data_buf[RELAYD_BUFFER_SIZE], cmsg_buf[128]; + union { + struct sockaddr_in6 in6; + struct sockaddr_in in; + struct sockaddr_ll ll; + struct sockaddr_nl nl; + } addr; + + while (true) { + struct iovec iov = {data_buf, sizeof(data_buf)}; + struct msghdr msg = {&addr, sizeof(addr), &iov, 1, + cmsg_buf, sizeof(cmsg_buf), 0}; + + ssize_t len = recvmsg(u->fd, &msg, MSG_DONTWAIT); + if (len < 0) { + if (errno == EAGAIN) + break; + else + continue; + } + + + // Extract destination interface + int destiface = 0; + int *hlim = NULL; + struct in6_pktinfo *pktinfo; + struct in_pktinfo *pkt4info; + for (struct cmsghdr *ch = CMSG_FIRSTHDR(&msg); ch != NULL && + destiface == 0; ch = CMSG_NXTHDR(&msg, ch)) { + if (ch->cmsg_level == IPPROTO_IPV6 && + ch->cmsg_type == IPV6_PKTINFO) { + pktinfo = (struct in6_pktinfo*)CMSG_DATA(ch); + destiface = pktinfo->ipi6_ifindex; + } else if (ch->cmsg_level == IPPROTO_IP && + ch->cmsg_type == IP_PKTINFO) { + pkt4info = (struct in_pktinfo*)CMSG_DATA(ch); + destiface = pkt4info->ipi_ifindex; + } else if (ch->cmsg_level == IPPROTO_IPV6 && + ch->cmsg_type == IPV6_HOPLIMIT) { + hlim = (int*)CMSG_DATA(ch); + } + } + + // Check hoplimit if received + if (hlim && *hlim != 255) + continue; + + // Detect interface for packet sockets + if (addr.ll.sll_family == AF_PACKET) + destiface = addr.ll.sll_ifindex; + + struct interface *iface = + odhcpd_get_interface_by_index(destiface); + + if (!iface && addr.nl.nl_family != AF_NETLINK) + continue; + + char ipbuf[INET6_ADDRSTRLEN] = "kernel"; + if (addr.ll.sll_family == AF_PACKET && + len >= (ssize_t)sizeof(struct ip6_hdr)) + inet_ntop(AF_INET6, &data_buf[8], ipbuf, sizeof(ipbuf)); + else if (addr.in6.sin6_family == AF_INET6) + inet_ntop(AF_INET6, &addr.in6.sin6_addr, ipbuf, sizeof(ipbuf)); + else if (addr.in.sin_family == AF_INET) + inet_ntop(AF_INET, &addr.in.sin_addr, ipbuf, sizeof(ipbuf)); + + syslog(LOG_NOTICE, "--"); + syslog(LOG_NOTICE, "Received %li Bytes from %s%%%s", (long)len, + ipbuf, (iface) ? iface->ifname : "netlink"); + + e->handle_dgram(&addr, data_buf, len, iface); + } +} + +// Register events for the multiplexer +int odhcpd_register(struct odhcpd_event *event) +{ + event->uloop.cb = odhcpd_receive_packets; + return uloop_fd_add(&event->uloop, ULOOP_READ); +} + +void odhcpd_urandom(void *data, size_t len) +{ + read(urandom_fd, data, len); +} + + +time_t odhcpd_time(void) +{ + struct timespec ts; + syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &ts); + return ts.tv_sec; +} + + +static const char hexdigits[] = "0123456789abcdef"; +static const int8_t hexvals[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -2, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; + +ssize_t odhcpd_unhexlify(uint8_t *dst, size_t len, const char *src) +{ + size_t c; + for (c = 0; c < len && src[0] && src[1]; ++c) { + int8_t x = (int8_t)*src++; + int8_t y = (int8_t)*src++; + if (x < 0 || (x = hexvals[x]) < 0 + || y < 0 || (y = hexvals[y]) < 0) + return -1; + dst[c] = x << 4 | y; + while (((int8_t)*src) < 0 || + (*src && hexvals[(uint8_t)*src] < 0)) + src++; + } + + return c; +} + + +void odhcpd_hexlify(char *dst, const uint8_t *src, size_t len) +{ + for (size_t i = 0; i < len; ++i) { + *dst++ = hexdigits[src[i] >> 4]; + *dst++ = hexdigits[src[i] & 0x0f]; + } + *dst = 0; +} diff --git a/src/odhcpd.h b/src/odhcpd.h new file mode 100644 index 0000000..5608fa9 --- /dev/null +++ b/src/odhcpd.h @@ -0,0 +1,205 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#pragma once +#include <netinet/in.h> +#include <netinet/icmp6.h> +#include <netinet/ether.h> +#include <net/if.h> +#include <stdbool.h> +#include <syslog.h> + +#include "libubox/blobmsg.h" + +#ifndef typeof +#define typeof __typeof +#endif + +#ifndef container_of +#define container_of(ptr, type, member) ( \ + (type *)( (char *)ptr - offsetof(type,member) )) +#endif + +#include "libubox/list.h" +#include "libubox/uloop.h" + +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +// RFC 6106 defines this router advertisement option +#define ND_OPT_ROUTE_INFO 24 +#define ND_OPT_RECURSIVE_DNS 25 +#define ND_OPT_DNS_SEARCH 31 + +#define RELAYD_BUFFER_SIZE 8192 +#define RELAYD_MAX_PREFIXES 8 + +#define _unused __attribute__((unused)) +#define _packed __attribute__((packed)) + + +#define ALL_IPV6_NODES {{{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}}} + +#define ALL_IPV6_ROUTERS {{{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}}} + + +struct interface; +extern struct list_head leases; + +struct odhcpd_event { + struct uloop_fd uloop; + void (*handle_dgram)(void *addr, void *data, size_t len, + struct interface *iface); +}; + + +struct odhcpd_ipaddr { + struct in6_addr addr; + uint8_t prefix; + uint32_t preferred; + uint32_t valid; +}; + +enum odhcpd_mode { + RELAYD_DISABLED, + RELAYD_SERVER, + RELAYD_RELAY, + RELAYD_HYBRID +}; + + +struct config { + bool legacy; + char *dhcp_cb; + char *dhcp_statefile; +} config; + + +struct lease { + struct list_head head; + struct in_addr ipaddr; + uint32_t hostid; + struct ether_addr mac; + uint16_t duid_len; + uint8_t *duid; + char hostname[]; +}; + + +struct interface { + struct list_head head; + + int ifindex; + char ifname[IF_NAMESIZE]; + char name[IF_NAMESIZE]; + bool inuse; + + // Runtime data + struct uloop_timeout timer_rs; + struct list_head ia_assignments; + struct odhcpd_ipaddr ia_addr[8]; + size_t ia_addr_len; + bool ia_reconf; + + // DHCPv4 + struct odhcpd_event dhcpv4_event; + struct list_head dhcpv4_assignments; + + // Services + enum odhcpd_mode ra; + enum odhcpd_mode dhcpv6; + enum odhcpd_mode ndp; + enum odhcpd_mode dhcpv4; + + // Config + bool external; + bool master; + bool ignore; + bool always_rewrite_dns; + bool deprecate_ula_if_public_avail; + bool ra_not_onlink; + bool no_dynamic_dhcp; + + int learn_routes; + int default_router; + int managed; + int route_preference; + + // DHCPv4 + struct in_addr dhcpv4_start; + struct in_addr dhcpv4_end; + struct in_addr *dhcpv4_dns; + size_t dhcpv4_dns_cnt; + uint32_t dhcpv4_leasetime; + + // DNS + struct in6_addr *dns; + size_t dns_cnt; + uint8_t *search; + size_t search_len; + + char* static_ndp; + size_t static_ndp_len; + + char *upstream; + size_t upstream_len; +}; + +extern struct list_head interfaces; + +#define RELAYD_MANAGED_MFLAG 1 +#define RELAYD_MANAGED_NO_AFLAG 2 + + +// Exported main functions +int odhcpd_open_rtnl(void); +int odhcpd_register(struct odhcpd_event *event); + +ssize_t odhcpd_send(int socket, struct sockaddr_in6 *dest, + struct iovec *iov, size_t iov_len, + const struct interface *iface); +ssize_t odhcpd_get_interface_addresses(int ifindex, + struct odhcpd_ipaddr *addrs, size_t cnt); +struct interface* odhcpd_get_interface_by_name(const char *name); +int odhcpd_get_interface_mtu(const char *ifname); +int odhcpd_get_mac(const struct interface *iface, uint8_t mac[6]); +struct interface* odhcpd_get_interface_by_index(int ifindex); +struct interface* odhcpd_get_master_interface(void); +void odhcpd_urandom(void *data, size_t len); +void odhcpd_setup_route(const struct in6_addr *addr, int prefixlen, + const struct interface *iface, const struct in6_addr *gw, bool add); + +void odhcpd_run(void); +time_t odhcpd_time(void); +ssize_t odhcpd_unhexlify(uint8_t *dst, size_t len, const char *src); +void odhcpd_hexlify(char *dst, const uint8_t *src, size_t len); + +int config_parse_interface(struct blob_attr *b, const char *iname); + +const char* ubus_get_ifname(const char *name); +void ubus_apply_network(void); + + +// Exported module initializers +int init_router(void); +int init_dhcpv6(void); +int init_dhcpv4(void); +int init_ndp(void); +int init_ubus(void); + +int setup_router_interface(struct interface *iface, bool enable); +int setup_dhcpv6_interface(struct interface *iface, bool enable); +int setup_ndp_interface(struct interface *iface, bool enable); +int setup_dhcpv4_interface(struct interface *iface, bool enable); diff --git a/src/router.c b/src/router.c new file mode 100644 index 0000000..9258acf --- /dev/null +++ b/src/router.c @@ -0,0 +1,502 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <resolv.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <stdbool.h> +#include <net/route.h> + +#include "router.h" +#include "odhcpd.h" + + +static void forward_router_solicitation(const struct interface *iface); +static void forward_router_advertisement(uint8_t *data, size_t len); + +static void handle_icmpv6(void *addr, void *data, size_t len, + struct interface *iface); +static void send_router_advert(struct uloop_timeout *event); +static void sigusr1_refresh(int signal); + +static struct odhcpd_event router_event = {{.fd = -1}, handle_icmpv6}; + +static FILE *fp_route = NULL; + + +int init_router(void) +{ + // Open ICMPv6 socket + int sock = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMPV6); + if (sock < 0 && errno != EAFNOSUPPORT) { + syslog(LOG_ERR, "Failed to open RAW-socket: %s", strerror(errno)); + return -1; + } + + // Let the kernel compute our checksums + int val = 2; + setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &val, sizeof(val)); + + // This is required by RFC 4861 + val = 255; + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val)); + setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &val, sizeof(val)); + + // We need to know the source interface + val = 1; + setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, sizeof(val)); + setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &val, sizeof(val)); + + // Don't loop back + val = 0; + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof(val)); + + // Filter ICMPv6 package types + struct icmp6_filter filt; + ICMP6_FILTER_SETBLOCKALL(&filt); + ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filt); + ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filt); + setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filt, sizeof(filt)); + + // Register socket + router_event.uloop.fd = sock; + odhcpd_register(&router_event); + + if (!(fp_route = fopen("/proc/net/ipv6_route", "r"))) + syslog(LOG_ERR, "Failed to open routing table: %s", + strerror(errno)); + + signal(SIGUSR1, sigusr1_refresh); + return 0; +} + + +int setup_router_interface(struct interface *iface, bool enable) +{ + struct ipv6_mreq all_nodes = {ALL_IPV6_NODES, iface->ifindex}; + struct ipv6_mreq all_routers = {ALL_IPV6_ROUTERS, iface->ifindex}; + + uloop_timeout_cancel(&iface->timer_rs); + iface->timer_rs.cb = NULL; + + setsockopt(router_event.uloop.fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, + &all_nodes, sizeof(all_nodes)); + setsockopt(router_event.uloop.fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, + &all_routers, sizeof(all_routers)); + + if (!enable) { + if (iface->ra) + send_router_advert(&iface->timer_rs); + } else { + void *mreq = &all_routers; + + if (iface->ra == RELAYD_RELAY && iface->master) { + mreq = &all_nodes; + forward_router_solicitation(iface); + } else if (iface->ra == RELAYD_SERVER && !iface->master) { + iface->timer_rs.cb = send_router_advert; + send_router_advert(&iface->timer_rs); + } + + if (iface->ra == RELAYD_RELAY || (iface->ra == RELAYD_SERVER && !iface->master)) + setsockopt(router_event.uloop.fd, IPPROTO_IPV6, + IPV6_ADD_MEMBERSHIP, mreq, sizeof(all_nodes)); + } + return 0; +} + + +// Signal handler to resend all RDs +static void sigusr1_refresh(_unused int signal) +{ + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) + if (iface->ra == RELAYD_SERVER && !iface->master) + uloop_timeout_set(&iface->timer_rs, 1000); +} + + +// Event handler for incoming ICMPv6 packets +static void handle_icmpv6(_unused void *addr, void *data, size_t len, + struct interface *iface) +{ + struct icmp6_hdr *hdr = data; + if ((iface->ra == RELAYD_SERVER && !iface->master)) { // Server mode + if (hdr->icmp6_type == ND_ROUTER_SOLICIT) + send_router_advert(&iface->timer_rs); + } else if (iface->ra == RELAYD_RELAY) { // Relay mode + if (hdr->icmp6_type == ND_ROUTER_ADVERT && iface->master) + forward_router_advertisement(data, len); + else if (hdr->icmp6_type == ND_ROUTER_SOLICIT && !iface->master) + forward_router_solicitation(odhcpd_get_master_interface()); + } +} + + +static bool match_route(const struct odhcpd_ipaddr *n, const struct in6_addr *addr) +{ + if (n->prefix <= 32) + return ntohl(n->addr.s6_addr32[0]) >> (32 - n->prefix) == + ntohl(addr->s6_addr32[0]) >> (32 - n->prefix); + + if (n->addr.s6_addr32[0] != addr->s6_addr32[0]) + return false; + + return ntohl(n->addr.s6_addr32[1]) >> (64 - n->prefix) == + ntohl(addr->s6_addr32[1]) >> (64 - n->prefix); +} + + +// Detect whether a default route exists, also find the source prefixes +static bool parse_routes(struct odhcpd_ipaddr *n, ssize_t len) +{ + rewind(fp_route); + + char line[512], ifname[16]; + bool found_default = false; + struct odhcpd_ipaddr p = {IN6ADDR_ANY_INIT, 0, 0, 0}; + while (fgets(line, sizeof(line), fp_route)) { + uint32_t rflags; + if (sscanf(line, "00000000000000000000000000000000 00 " + "%*s %*s %*s %*s %*s %*s %*s %15s", ifname) && + strcmp(ifname, "lo")) { + found_default = true; + } else if (sscanf(line, "%8" SCNx32 "%8" SCNx32 "%*8" SCNx32 "%*8" SCNx32 " %hhx %*s " + "%*s 00000000000000000000000000000000 %*s %*s %*s %" SCNx32 " lo", + &p.addr.s6_addr32[0], &p.addr.s6_addr32[1], &p.prefix, &rflags) && + p.prefix > 0 && (rflags & RTF_NONEXTHOP) && (rflags & RTF_REJECT)) { + // Find source prefixes by scanning through unreachable-routes + p.addr.s6_addr32[0] = htonl(p.addr.s6_addr32[0]); + p.addr.s6_addr32[1] = htonl(p.addr.s6_addr32[1]); + + for (ssize_t i = 0; i < len; ++i) { + if (n[i].prefix <= 64 && n[i].prefix >= p.prefix && + match_route(&p, &n[i].addr)) { + n[i].prefix = p.prefix; + break; + } + } + + } + } + + return found_default; +} + + +// Router Advert server mode +static void send_router_advert(struct uloop_timeout *event) +{ + struct interface *iface = + container_of(event, struct interface, timer_rs); + + int mtu = odhcpd_get_interface_mtu(iface->ifname); + if (mtu < 0) + mtu = 1500; + + struct { + struct nd_router_advert h; + struct icmpv6_opt lladdr; + struct nd_opt_mtu mtu; + struct nd_opt_prefix_info prefix[RELAYD_MAX_PREFIXES]; + } adv = { + .h = {{.icmp6_type = ND_ROUTER_ADVERT, .icmp6_code = 0}, 0, 0}, + .lladdr = {ND_OPT_SOURCE_LINKADDR, 1, {0}}, + .mtu = {ND_OPT_MTU, 1, 0, htonl(mtu)}, + }; + adv.h.nd_ra_flags_reserved = ND_RA_FLAG_OTHER; + if (iface->managed >= RELAYD_MANAGED_MFLAG) + adv.h.nd_ra_flags_reserved |= ND_RA_FLAG_MANAGED; + + if (iface->route_preference < 0) + adv.h.nd_ra_flags_reserved |= ND_RA_PREF_LOW; + else if (iface->route_preference > 0) + adv.h.nd_ra_flags_reserved |= ND_RA_PREF_HIGH; + odhcpd_get_mac(iface, adv.lladdr.data); + + // If not currently shutting down + struct odhcpd_ipaddr addrs[RELAYD_MAX_PREFIXES]; + ssize_t ipcnt = 0; + + // If not shutdown + if (event->cb) { + ipcnt = odhcpd_get_interface_addresses(iface->ifindex, + addrs, ARRAY_SIZE(addrs)); + + // Check default route + if (parse_routes(addrs, ipcnt) || iface->default_router > 1) + adv.h.nd_ra_router_lifetime = + htons(3 * MaxRtrAdvInterval); + } + + // Construct Prefix Information options + bool have_public = false; + size_t cnt = 0; + + struct in6_addr *dns_addr = NULL; + uint32_t dns_time = 0; + size_t dns_cnt = 1; + + for (ssize_t i = 0; i < ipcnt; ++i) { + struct odhcpd_ipaddr *addr = &addrs[i]; + if (addr->prefix > 64) + continue; // Address not suitable + + if (addr->preferred > MaxPreferredTime) + addr->preferred = MaxPreferredTime; + + if (addr->valid > MaxValidTime) + addr->valid = MaxValidTime; + + struct nd_opt_prefix_info *p = NULL; + for (size_t i = 0; i < cnt; ++i) { + if (!memcmp(&adv.prefix[i].nd_opt_pi_prefix, + &addr->addr, 8)) + p = &adv.prefix[i]; + } + + if (!p) { + if (cnt >= ARRAY_SIZE(adv.prefix)) + break; + + p = &adv.prefix[cnt++]; + } + + if ((addr->addr.s6_addr[0] & 0xfe) != 0xfc && addr->preferred > 0) + have_public = true; + + memcpy(&p->nd_opt_pi_prefix, &addr->addr, 8); + p->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION; + p->nd_opt_pi_len = 4; + p->nd_opt_pi_prefix_len = 64; + p->nd_opt_pi_flags_reserved = 0; + if (!iface->ra_not_onlink) + p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_ONLINK; + if (iface->managed < RELAYD_MANAGED_NO_AFLAG) + p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO; + p->nd_opt_pi_valid_time = htonl(addr->valid); + p->nd_opt_pi_preferred_time = htonl(addr->preferred); + + if (addr->preferred > dns_time) { + dns_time = addr->preferred; + dns_addr = &addr->addr; + } + } + + if (!have_public && !iface->default_router && adv.h.nd_ra_router_lifetime) { + syslog(LOG_WARNING, "A default route is present but there is no public prefix " + "on %s thus we don't announce a default route!", iface->ifname); + adv.h.nd_ra_router_lifetime = 0; + } + + if (have_public && iface->deprecate_ula_if_public_avail) + for (size_t i = 0; i < cnt; ++i) + if ((adv.prefix[i].nd_opt_pi_prefix.s6_addr[0] & 0xfe) == 0xfc) + adv.prefix[i].nd_opt_pi_preferred_time = 0; + + // DNS Recursive DNS + if (iface->dns_cnt > 0) { + dns_addr = iface->dns; + dns_cnt = iface->dns_cnt; + dns_time = 2 * MaxRtrAdvInterval; + } + + if (!dns_addr) + dns_cnt = 0; + + struct { + uint8_t type; + uint8_t len; + uint8_t pad; + uint8_t pad2; + uint32_t lifetime; + } dns = {ND_OPT_RECURSIVE_DNS, (1 + (2 * dns_cnt)), 0, 0, htonl(dns_time)}; + + + + // DNS Search options + uint8_t search_buf[256], *search_domain = iface->search; + size_t search_len = iface->search_len, search_padded = 0; + + if (!search_domain && !res_init() && _res.dnsrch[0] && _res.dnsrch[0][0]) { + int len = dn_comp(_res.dnsrch[0], search_buf, + sizeof(search_buf), NULL, NULL); + if (len > 0) { + search_domain = search_buf; + search_len = len; + } + } + + if (search_len > 0) + search_padded = ((search_len + 7) & (~7)) + 8; + + struct { + uint8_t type; + uint8_t len; + uint8_t pad; + uint8_t pad2; + uint32_t lifetime; + uint8_t name[]; + } *search = alloca(sizeof(*search) + search_padded); + search->type = ND_OPT_DNS_SEARCH; + search->len = search_len ? ((sizeof(*search) + search_padded) / 8) : 0; + search->pad = 0; + search->pad2 = 0; + search->lifetime = htonl(2 * MaxRtrAdvInterval);; + memcpy(search->name, search_domain, search_len); + memset(&search->name[search_len], 0, search_padded - search_len); + + + size_t routes_cnt = 0; + struct { + uint8_t type; + uint8_t len; + uint8_t prefix; + uint8_t flags; + uint32_t lifetime; + uint32_t addr[4]; + } routes[RELAYD_MAX_PREFIXES]; + + for (ssize_t i = 0; i < ipcnt; ++i) { + struct odhcpd_ipaddr *addr = &addrs[i]; + if (addr->prefix > 64 || addr->prefix == 0) { + continue; // Address not suitable + } else if (addr->prefix > 32) { + addr->addr.s6_addr32[1] &= htonl(~((1U << (64 - addr->prefix)) - 1)); + } else if (addr->prefix <= 32) { + addr->addr.s6_addr32[0] &= htonl(~((1U << (32 - addr->prefix)) - 1)); + addr->addr.s6_addr32[1] = 0; + } + + routes[routes_cnt].type = ND_OPT_ROUTE_INFO; + routes[routes_cnt].len = sizeof(*routes) / 8; + routes[routes_cnt].prefix = addr->prefix; + routes[routes_cnt].flags = 0; + if (iface->route_preference < 0) + routes[routes_cnt].flags |= ND_RA_PREF_LOW; + else if (iface->route_preference > 0) + routes[routes_cnt].flags |= ND_RA_PREF_HIGH; + routes[routes_cnt].lifetime = htonl(addr->valid); + routes[routes_cnt].addr[0] = addr->addr.s6_addr32[0]; + routes[routes_cnt].addr[1] = addr->addr.s6_addr32[1]; + routes[routes_cnt].addr[2] = addr->addr.s6_addr32[2]; + routes[routes_cnt].addr[3] = addr->addr.s6_addr32[3]; + + ++routes_cnt; + } + + + struct iovec iov[] = {{&adv, (uint8_t*)&adv.prefix[cnt] - (uint8_t*)&adv}, + {&routes, routes_cnt * sizeof(*routes)}, + {&dns, (dns_cnt) ? sizeof(dns) : 0}, + {dns_addr, dns_cnt * sizeof(*dns_addr)}, + {search, search->len * 8}}; + struct sockaddr_in6 all_nodes = {AF_INET6, 0, 0, ALL_IPV6_NODES, 0}; + odhcpd_send(router_event.uloop.fd, + &all_nodes, iov, ARRAY_SIZE(iov), iface); + + // Rearm timer + int msecs; + odhcpd_urandom(&msecs, sizeof(msecs)); + msecs = (labs(msecs) % (1000 * (MaxRtrAdvInterval + - MinRtrAdvInterval))) + (MinRtrAdvInterval * 1000); + uloop_timeout_set(&iface->timer_rs, msecs); +} + + +// Forward router solicitation +static void forward_router_solicitation(const struct interface *iface) +{ + if (!iface) + return; + + struct icmp6_hdr rs = {ND_ROUTER_SOLICIT, 0, 0, {{0}}}; + struct iovec iov = {&rs, sizeof(rs)}; + struct sockaddr_in6 all_routers = + {AF_INET6, 0, 0, ALL_IPV6_ROUTERS, iface->ifindex}; + + syslog(LOG_NOTICE, "Sending RS to %s", iface->ifname); + odhcpd_send(router_event.uloop.fd, &all_routers, &iov, 1, iface); +} + + +// Handler for incoming router solicitations on slave interfaces +static void forward_router_advertisement(uint8_t *data, size_t len) +{ + struct nd_router_advert *adv = (struct nd_router_advert *)data; + + // Rewrite options + uint8_t *end = data + len; + uint8_t *mac_ptr = NULL; + struct in6_addr *dns_ptr = NULL; + size_t dns_count = 0; + + struct icmpv6_opt *opt; + icmpv6_for_each_option(opt, &adv[1], end) { + if (opt->type == ND_OPT_SOURCE_LINKADDR) { + // Store address of source MAC-address + mac_ptr = opt->data; + } else if (opt->type == ND_OPT_RECURSIVE_DNS && opt->len > 1) { + // Check if we have to rewrite DNS + dns_ptr = (struct in6_addr*)&opt->data[6]; + dns_count = (opt->len - 1) / 2; + } + } + + syslog(LOG_NOTICE, "Got a RA"); + + // Indicate a proxy, however we don't follow the rest of RFC 4389 yet + adv->nd_ra_flags_reserved |= ND_RA_FLAG_PROXY; + + // Forward advertisement to all slave interfaces + struct sockaddr_in6 all_nodes = {AF_INET6, 0, 0, ALL_IPV6_NODES, 0}; + struct iovec iov = {data, len}; + + struct odhcpd_ipaddr addr; + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) { + if (iface->ra != RELAYD_RELAY || iface->master) + continue; + + // Fixup source hardware address option + if (mac_ptr) + odhcpd_get_mac(iface, mac_ptr); + + // If we have to rewrite DNS entries + if (iface->always_rewrite_dns && dns_ptr && dns_count > 0) { + const struct in6_addr *rewrite = iface->dns; + size_t rewrite_cnt = iface->dns_cnt; + + if (rewrite_cnt == 0) { + if (odhcpd_get_interface_addresses(iface->ifindex, &addr, 1) < 1) + continue; // Unable to comply + + rewrite = &addr.addr; + rewrite_cnt = 1; + } + + // Copy over any other addresses + for (size_t i = 0; i < dns_count; ++i) { + size_t j = (i < rewrite_cnt) ? i : rewrite_cnt - 1; + dns_ptr[i] = rewrite[j]; + } + } + + odhcpd_send(router_event.uloop.fd, &all_nodes, &iov, 1, iface); + } +} diff --git a/src/router.h b/src/router.h new file mode 100644 index 0000000..1e8649c --- /dev/null +++ b/src/router.h @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License v2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#pragma once +#include <stdint.h> +#include <netinet/in.h> +#include <netinet/icmp6.h> + +struct icmpv6_opt { + uint8_t type; + uint8_t len; + uint8_t data[6]; +}; + + +#define icmpv6_for_each_option(opt, start, end)\ + for (opt = (struct icmpv6_opt*)(start);\ + (void*)(opt + 1) <= (void*)(end) && opt->len > 0 &&\ + (void*)(opt + opt->len) <= (void*)(end); opt += opt->len) + + +#define MaxRtrAdvInterval 600 +#define MinRtrAdvInterval (MaxRtrAdvInterval / 3) +#define MaxValidTime 7200 +#define MaxPreferredTime (3 * MaxRtrAdvInterval) + +#define ND_RA_FLAG_PROXY 0x4 +#define ND_RA_PREF_HIGH (1 << 3) +#define ND_RA_PREF_LOW (3 << 3) diff --git a/src/ubus.c b/src/ubus.c new file mode 100644 index 0000000..0b7ec41 --- /dev/null +++ b/src/ubus.c @@ -0,0 +1,314 @@ +#include <syslog.h> +#include <libubus.h> +#include <libubox/uloop.h> +#include <arpa/inet.h> + +#include "odhcpd.h" +#include "dhcpv6.h" +#include "dhcpv4.h" + + +static struct ubus_context *ubus = NULL; +static struct ubus_subscriber netifd; +static struct blob_buf b; +static struct blob_attr *dump = NULL; +static uint32_t objid = 0; + + +static int handle_dhcpv4_leases(struct ubus_context *ctx, _unused struct ubus_object *obj, + struct ubus_request_data *req, _unused const char *method, + _unused struct blob_attr *msg) +{ + blob_buf_init(&b, 0); + void *a = blobmsg_open_table(&b, "device"); + time_t now = odhcpd_time(); + + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) { + if (iface->dhcpv4 != RELAYD_SERVER) + continue; + + void *i = blobmsg_open_table(&b, iface->ifname); + void *j = blobmsg_open_array(&b, "leases"); + + struct dhcpv4_assignment *lease; + list_for_each_entry(lease, &iface->dhcpv4_assignments, head) { + if (lease->valid_until < now) + continue; + + void *l = blobmsg_open_table(&b, NULL); + + char *buf = blobmsg_alloc_string_buffer(&b, "mac", 13); + odhcpd_hexlify(buf, lease->hwaddr, sizeof(lease->hwaddr)); + blobmsg_add_string_buffer(&b); + + blobmsg_add_string(&b, "hostname", lease->hostname); + + buf = blobmsg_alloc_string_buffer(&b, "ip", INET_ADDRSTRLEN); + struct in_addr addr = {htonl(lease->addr)}; + inet_ntop(AF_INET, &addr, buf, INET_ADDRSTRLEN); + blobmsg_add_string_buffer(&b); + + blobmsg_add_u32(&b, "valid", now - lease->valid_until); + + blobmsg_close_table(&b, l); + } + + blobmsg_close_array(&b, j); + blobmsg_close_table(&b, i); + } + + blobmsg_close_table(&b, a); + ubus_send_reply(ctx, req, b.head); + return 0; +} + + +static int handle_dhcpv6_leases(_unused struct ubus_context *ctx, _unused struct ubus_object *obj, + _unused struct ubus_request_data *req, _unused const char *method, + _unused struct blob_attr *msg) +{ + blob_buf_init(&b, 0); + void *a = blobmsg_open_table(&b, "device"); + time_t now = odhcpd_time(); + + struct interface *iface; + list_for_each_entry(iface, &interfaces, head) { + if (iface->dhcpv6 != RELAYD_SERVER) + continue; + + void *i = blobmsg_open_table(&b, iface->ifname); + void *j = blobmsg_open_array(&b, "leases"); + + struct dhcpv6_assignment *lease; + list_for_each_entry(lease, &iface->ia_assignments, head) { + if (lease->valid_until < now) + continue; + + void *l = blobmsg_open_table(&b, NULL); + + char *buf = blobmsg_alloc_string_buffer(&b, "duid", 264); + odhcpd_hexlify(buf, lease->clid_data, lease->clid_len); + blobmsg_add_string_buffer(&b); + + blobmsg_add_u32(&b, "iaid", ntohl(lease->iaid)); + blobmsg_add_string(&b, "hostname", (lease->hostname) ? lease->hostname : ""); + blobmsg_add_u32(&b, "assigned", lease->assigned); + blobmsg_add_u32(&b, "length", lease->length); + + void *m = blobmsg_open_array(&b, "ipv6"); + struct in6_addr addr; + for (size_t i = 0; i < iface->ia_addr_len; ++i) { + if (iface->ia_addr[i].prefix > 64) + continue; + + addr = iface->ia_addr[i].addr; + if (lease->length == 128) + addr.s6_addr32[3] = htonl(lease->assigned); + else + addr.s6_addr32[1] |= htonl(lease->assigned); + + char *c = blobmsg_alloc_string_buffer(&b, NULL, INET6_ADDRSTRLEN); + inet_ntop(AF_INET6, &addr, c, INET6_ADDRSTRLEN); + blobmsg_add_string_buffer(&b); + } + blobmsg_close_table(&b, m); + + blobmsg_add_u32(&b, "valid", now - lease->valid_until); + + blobmsg_close_table(&b, l); + } + + blobmsg_close_array(&b, j); + blobmsg_close_table(&b, i); + } + + blobmsg_close_table(&b, a); + ubus_send_reply(ctx, req, b.head); + return 0; +} + + +static struct ubus_method main_object_methods[] = { + {.name = "ipv4leases", .handler = handle_dhcpv4_leases}, + {.name = "ipv6leases", .handler = handle_dhcpv6_leases}, +}; + +static struct ubus_object_type main_object_type = + UBUS_OBJECT_TYPE("dhcp", main_object_methods); + +static struct ubus_object main_object = { + .name = "dhcp", + .type = &main_object_type, + .methods = main_object_methods, + .n_methods = ARRAY_SIZE(main_object_methods), +}; + + +enum { + DUMP_ATTR_INTERFACE, + DUMP_ATTR_MAX +}; + +static const struct blobmsg_policy dump_attrs[DUMP_ATTR_MAX] = { + [DUMP_ATTR_INTERFACE] = { .name = "interface", .type = BLOBMSG_TYPE_ARRAY }, +}; + + +enum { + IFACE_ATTR_INTERFACE, + IFACE_ATTR_IFNAME, + IFACE_ATTR_UP, + IFACE_ATTR_DATA, + IFACE_ATTR_MAX, +}; + +static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = { + [IFACE_ATTR_INTERFACE] = { .name = "interface", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_IFNAME] = { .name = "ifname", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_UP] = { .name = "up", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_DATA] = { .name = "data", .type = BLOBMSG_TYPE_TABLE }, +}; + +static void handle_dump(_unused struct ubus_request *req, _unused int type, struct blob_attr *msg) +{ + struct blob_attr *tb[DUMP_ATTR_INTERFACE]; + blobmsg_parse(dump_attrs, DUMP_ATTR_MAX, tb, blob_data(msg), blob_len(msg)); + + if (!tb[DUMP_ATTR_INTERFACE]) + return; + + free(dump); + dump = blob_memdup(tb[DUMP_ATTR_INTERFACE]); + raise(SIGHUP); +} + + +static struct interface* find_interface(struct blob_attr *msg) +{ + struct blob_attr *tb[IFACE_ATTR_MAX]; + blobmsg_parse(iface_attrs, IFACE_ATTR_MAX, tb, blob_data(msg), blob_len(msg)); + + const char *interface = (tb[IFACE_ATTR_INTERFACE]) ? + blobmsg_get_string(tb[IFACE_ATTR_INTERFACE]) : ""; + const char *ifname = (tb[IFACE_ATTR_IFNAME]) ? + blobmsg_get_string(tb[IFACE_ATTR_IFNAME]) : ""; + + struct interface *c; + list_for_each_entry(c, &interfaces, head) + if (!strcmp(interface, c->name) || !strcmp(ifname, c->ifname)) + return c; + + return NULL; +} + + +static int handle_update(_unused struct ubus_context *ctx, _unused struct ubus_object *obj, + _unused struct ubus_request_data *req, _unused const char *method, + struct blob_attr *msg) +{ + struct interface *iface = find_interface(msg); + if (iface && iface->ignore) + return 0; + + ubus_invoke(ubus, objid, "dump", NULL, handle_dump, NULL, 0); + return 0; +} + + +static void subscribe_netifd(void) +{ + netifd.cb = handle_update; + ubus_subscribe(ubus, &netifd, objid); + ubus_invoke(ubus, objid, "dump", NULL, handle_dump, NULL, 0); +} + + +void ubus_apply_network(void) +{ + struct blob_attr *c; + int rem; + + if (!dump) + return; + + blobmsg_for_each_attr(c, dump, rem) { + struct interface *iface = find_interface(c); + if (!iface || !iface->ignore) + config_parse_interface(c, NULL); + } +} + + +enum { + OBJ_ATTR_ID, + OBJ_ATTR_PATH, + OBJ_ATTR_MAX +}; + +static const struct blobmsg_policy obj_attrs[OBJ_ATTR_MAX] = { + [OBJ_ATTR_ID] = { .name = "id", .type = BLOBMSG_TYPE_INT32 }, + [OBJ_ATTR_PATH] = { .name = "path", .type = BLOBMSG_TYPE_STRING }, +}; + + +static void handle_event(_unused struct ubus_context *ctx, _unused struct ubus_event_handler *ev, + _unused const char *type, struct blob_attr *msg) +{ + struct blob_attr *tb[OBJ_ATTR_MAX]; + blobmsg_parse(obj_attrs, OBJ_ATTR_MAX, tb, blob_data(msg), blob_len(msg)); + objid = 0; + + if (!tb[OBJ_ATTR_ID] || !tb[OBJ_ATTR_PATH]) + return; + + if (strcmp(blobmsg_get_string(tb[OBJ_ATTR_PATH]), "network.interface")) + return; + + objid = blobmsg_get_u32(tb[OBJ_ATTR_ID]); + subscribe_netifd(); +} + +static struct ubus_event_handler event_handler = { .cb = handle_event }; + + +const char* ubus_get_ifname(const char *name) +{ + struct blob_attr *c; + int rem; + + if (!dump) + return NULL; + + blobmsg_for_each_attr(c, dump, rem) { + struct blob_attr *tb[IFACE_ATTR_MAX]; + blobmsg_parse(iface_attrs, IFACE_ATTR_MAX, tb, blob_data(c), blob_len(c)); + + if (!tb[IFACE_ATTR_INTERFACE] || strcmp(name, + blobmsg_get_string(tb[IFACE_ATTR_INTERFACE]))) + continue; + + if (tb[IFACE_ATTR_IFNAME]) + return blobmsg_get_string(tb[IFACE_ATTR_IFNAME]); + } + + return NULL; +} + + +int init_ubus(void) +{ + if (!(ubus = ubus_connect(NULL))) { + syslog(LOG_ERR, "Unable to connect to ubus: %s", strerror(errno)); + return -1; + } + + ubus_add_uloop(ubus); + ubus_add_object(ubus, &main_object); + ubus_register_event_handler(ubus, &event_handler, "ubus.object.add"); + if (!ubus_lookup_id(ubus, "network.interface", &objid)) + subscribe_netifd(); + + return 0; +} + |