udhcpsnoop: add new version of the dhcp snooping daemon
authorJohn Crispin <[email protected]>
Mon, 26 May 2025 12:38:47 +0000 (14:38 +0200)
committerFelix Fietkau <[email protected]>
Fri, 18 Jul 2025 11:59:31 +0000 (13:59 +0200)
The daemon sniffs DHCP packets even on bridged interfaces and provides
fingerprints of requests via ubus.

Signed-off-by: John Crispin <[email protected]>
12 files changed:
net/udhcpsnoop/Makefile [new file with mode: 0644]
net/udhcpsnoop/files/dhcpsnoop.conf [new file with mode: 0644]
net/udhcpsnoop/files/dhcpsnoop.hotplug [new file with mode: 0644]
net/udhcpsnoop/files/dhcpsnoop.init [new file with mode: 0644]
net/udhcpsnoop/src/CMakeLists.txt [new file with mode: 0644]
net/udhcpsnoop/src/cache.c [new file with mode: 0644]
net/udhcpsnoop/src/dev.c [new file with mode: 0644]
net/udhcpsnoop/src/dhcp.c [new file with mode: 0644]
net/udhcpsnoop/src/dhcpsnoop.h [new file with mode: 0644]
net/udhcpsnoop/src/main.c [new file with mode: 0644]
net/udhcpsnoop/src/msg.h [new file with mode: 0644]
net/udhcpsnoop/src/ubus.c [new file with mode: 0644]

diff --git a/net/udhcpsnoop/Makefile b/net/udhcpsnoop/Makefile
new file mode 100644 (file)
index 0000000..e5f1105
--- /dev/null
@@ -0,0 +1,32 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=udhcpsnoop
+PKG_RELEASE:=1
+
+PKG_LICENSE:=GPL-2.0
+PKG_MAINTAINER:=John Crispin <[email protected]>
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+define Package/udhcpsnoop
+  SECTION:=net
+  CATEGORY:=Network
+  TITLE:=DHCP Snooping Daemon
+  DEPENDS:=+libubox +libubus +kmod-ifb +tc
+endef
+
+define Package/udhcpsnoop/install
+       $(INSTALL_DIR) \
+               $(1)/usr/sbin \
+               $(1)/etc/init.d \
+               $(1)/etc/config \
+               $(1)/etc/hotplug.d/net
+       $(INSTALL_DIR) $(1)/usr/sbin
+       $(INSTALL_BIN) $(PKG_BUILD_DIR)/udhcpsnoop $(1)/usr/sbin/
+       $(INSTALL_BIN) ./files/dhcpsnoop.init $(1)/etc/init.d/dhcpsnoop
+       $(INSTALL_DATA) ./files/dhcpsnoop.conf $(1)/etc/config/dhcpsnoop
+       $(INSTALL_DATA) ./files/dhcpsnoop.hotplug $(1)/etc/hotplug.d/net/10-dhcpsnoop
+endef
+
+$(eval $(call BuildPackage,udhcpsnoop))
diff --git a/net/udhcpsnoop/files/dhcpsnoop.conf b/net/udhcpsnoop/files/dhcpsnoop.conf
new file mode 100644 (file)
index 0000000..0b480ed
--- /dev/null
@@ -0,0 +1,6 @@
+#config device
+#      option disabled 1
+#      option name eth0
+#      option ingress 1
+#      option egress 1
+
diff --git a/net/udhcpsnoop/files/dhcpsnoop.hotplug b/net/udhcpsnoop/files/dhcpsnoop.hotplug
new file mode 100644 (file)
index 0000000..f45e0d1
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+ubus call dhcpsnoop check_devices
diff --git a/net/udhcpsnoop/files/dhcpsnoop.init b/net/udhcpsnoop/files/dhcpsnoop.init
new file mode 100644 (file)
index 0000000..27ec852
--- /dev/null
@@ -0,0 +1,60 @@
+#!/bin/sh /etc/rc.common
+# Copyright (c) 2021 OpenWrt.org
+
+START=40
+
+USE_PROCD=1
+PROG=/usr/sbin/udhcpsnoop
+
+add_option() {
+       local type="$1"
+       local name="$2"
+       local default="$3"
+
+       config_get val "$cfg" "$name"
+
+       [ -n "$val" ] && json_add_$type "$name" "${val:-$default}"
+}
+
+add_device() {
+       local cfg="$1"
+
+       config_get_bool disabled "$cfg" disabled 0
+       [ "$disabled" -gt 0 ] && return
+
+       config_get name "$cfg" name
+       json_add_object "$name"
+
+       add_option boolean ingress 1
+       add_option boolean egress 1
+
+       json_close_object
+}
+
+reload_service() {
+       json_init
+
+       config_load dhcpsnoop
+
+       json_add_object devices
+       config_foreach add_device device 
+       json_close_object
+
+       ubus call dhcpsnoop config "$(json_dump)"
+}
+
+service_triggers() {
+       procd_add_reload_trigger dhcpsnoop
+}
+
+start_service() {
+       procd_open_instance
+       procd_set_param command "$PROG"
+       procd_set_param respawn
+       procd_close_instance
+}
+
+service_started() {
+       ubus -t 10 wait_for dhcpsnoop
+       [ $? = 0 ] && reload_service
+}
diff --git a/net/udhcpsnoop/src/CMakeLists.txt b/net/udhcpsnoop/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..1b677dd
--- /dev/null
@@ -0,0 +1,16 @@
+cmake_minimum_required(VERSION 3.10)
+
+PROJECT(udhcpsnoop C)
+INCLUDE(GNUInstallDirs)
+ADD_DEFINITIONS(-Os -ggdb -Wall -Werror --std=gnu99 -Wmissing-declarations)
+
+SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "")
+
+SET(SOURCES main.c ubus.c dev.c dhcp.c cache.c)
+SET(LIBS ubox ubus)
+
+ADD_EXECUTABLE(udhcpsnoop ${SOURCES})
+TARGET_LINK_LIBRARIES(udhcpsnoop ${LIBS})
+INSTALL(TARGETS udhcpsnoop
+       RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}
+)
diff --git a/net/udhcpsnoop/src/cache.c b/net/udhcpsnoop/src/cache.c
new file mode 100644 (file)
index 0000000..df606a1
--- /dev/null
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Felix Fietkau <[email protected]>
+ */
+
+#include <libubox/avl.h>
+
+#include "dhcpsnoop.h"
+#include "msg.h"
+
+#define MAC_FMT "%02x:%02x:%02x:%02x:%02x:%02x"
+#define MAC_VAR(x) x[0], x[1], x[2], x[3], x[4], x[5]
+
+#define IP_FMT  "%d.%d.%d.%d"
+#define IP_VAR(x) x[0], x[1], x[2], x[3]
+
+struct mac {
+        struct avl_node avl;
+       uint8_t mac[6];
+       uint8_t ip[4];
+       struct uloop_timeout rebind;
+};
+
+static int
+avl_mac_cmp(const void *k1, const void *k2, void *ptr)
+{
+       return memcmp(k1, k2, 6);
+}
+
+static struct avl_tree mac_tree = AVL_TREE_INIT(mac_tree, avl_mac_cmp, false, NULL);
+
+static void
+cache_expire(struct uloop_timeout *t)
+{
+       struct mac *mac = container_of(t, struct mac, rebind);
+
+       avl_delete(&mac_tree, &mac->avl);
+       free(mac);
+}
+
+void
+cache_entry(void *_msg, uint32_t rebind)
+{
+       struct dhcpv4_message *msg = (struct dhcpv4_message *) _msg;
+       struct mac *mac;
+
+       mac = avl_find_element(&mac_tree, msg->chaddr, mac, avl);
+
+       if (!mac) {
+               mac = malloc(sizeof(*mac));
+               if (!mac)
+                       return;
+               memset(mac, 0, sizeof(*mac));
+               memcpy(mac->mac, msg->chaddr, 6);
+               mac->avl.key = mac->mac;
+               mac->rebind.cb = cache_expire;
+               avl_insert(&mac_tree, &mac->avl);
+       }
+       memcpy(mac->ip, &msg->yiaddr.s_addr, 4);
+       uloop_timeout_set(&mac->rebind, rebind * 1000);
+}
+
+void
+cache_dump(struct blob_buf *b)
+{
+       struct mac *mac;
+
+       avl_for_each_element(&mac_tree, mac, avl) {
+               char addr[18];
+               char ip[16];
+
+               snprintf(addr, sizeof(addr), MAC_FMT, MAC_VAR(mac->mac));
+               snprintf(ip, sizeof(ip), IP_FMT, IP_VAR(mac->ip));
+
+               blobmsg_add_string(b, addr, ip);
+       }
+}
diff --git a/net/udhcpsnoop/src/dev.c b/net/udhcpsnoop/src/dev.c
new file mode 100644 (file)
index 0000000..8adcfde
--- /dev/null
@@ -0,0 +1,496 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Felix Fietkau <[email protected]>
+ */
+#include <netinet/if_ether.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/udp.h>
+#include <netpacket/packet.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <libubox/vlist.h>
+#include <libubox/avl-cmp.h>
+
+#include "dhcpsnoop.h"
+
+#define APPEND(_buf, _ofs, _format, ...) _ofs += snprintf(_buf + _ofs, sizeof(_buf) - _ofs, _format, ##__VA_ARGS__)
+
+struct vlan_hdr {
+       uint16_t tci;
+       uint16_t proto;
+};
+
+struct gre_hdr {
+       uint16_t flags;
+       uint16_t proto;
+};
+
+struct packet {
+       void *buffer;
+       unsigned int len;
+};
+
+
+struct device {
+       struct vlist_node node;
+       char ifname[IFNAMSIZ + 1];
+
+       int ifindex;
+       bool ingress;
+       bool egress;
+
+       bool changed;
+       bool active;
+};
+
+static void dev_update_cb(struct vlist_tree *tree, struct vlist_node *node_new,
+                         struct vlist_node *node_old);
+
+static struct uloop_fd ufd;
+static VLIST_TREE(devices, avl_strcmp, dev_update_cb, true, false);
+
+static void *pkt_peek(struct packet *pkt, unsigned int len)
+{
+       if (len > pkt->len)
+               return NULL;
+
+       return pkt->buffer;
+}
+
+
+static void *pkt_pull(struct packet *pkt, unsigned int len)
+{
+       void *ret = pkt_peek(pkt, len);
+
+       if (!ret)
+               return NULL;
+
+       pkt->buffer += len;
+       pkt->len -= len;
+
+       return ret;
+}
+
+static bool
+proto_is_vlan(uint16_t proto)
+{
+       return proto == ETH_P_8021Q || proto == ETH_P_8021AD;
+}
+
+static void
+dhcpsnoop_packet_cb(struct packet *pkt)
+{
+       struct ethhdr *eth;
+       struct ip6_hdr *ip6;
+       struct ip *ip;
+       struct udphdr *udp;
+       uint16_t proto, port;
+       const char *type;
+       bool ipv6 = false;
+       uint32_t rebind = 0;
+
+inside_tunnel:
+       eth = pkt_pull(pkt, sizeof(*eth));
+       if (!eth)
+               return;
+
+       proto = be16_to_cpu(eth->h_proto);
+       if (proto_is_vlan(proto)) {
+               struct vlan_hdr *vlan;
+
+               vlan = pkt_pull(pkt, sizeof(*vlan));
+               if (!vlan)
+                       return;
+
+               proto = be16_to_cpu(vlan->proto);
+       }
+
+       switch (proto) {
+       case ETH_P_IP:
+               ip = pkt_peek(pkt, sizeof(struct ip));
+               if (!ip)
+                       return;
+
+               if (!pkt_pull(pkt, ip->ip_hl * 4))
+                       return;
+
+               proto = ip->ip_p;
+               break;
+       case ETH_P_IPV6:
+               ip6 = pkt_pull(pkt, sizeof(*ip6));
+               if (!ip6)
+                       return;
+
+               proto = ip6->ip6_nxt;
+               ipv6 = true;
+               break;
+       default:
+               return;
+       }
+
+       if (proto == IPPROTO_GRE) {
+               struct gre_hdr *gre;
+               gre = pkt_pull(pkt, sizeof(*gre));
+               if (!gre) return;
+               proto = be16_to_cpu(gre->proto);
+               if (proto != 0x6558) return;
+               goto inside_tunnel;
+       }
+
+       if (proto != IPPROTO_UDP)
+               return;
+
+       udp = pkt_pull(pkt, sizeof(struct udphdr));
+       if (!udp)
+               return;
+
+       port = ntohs(udp->uh_sport);
+
+       if (!ipv6)
+               type = dhcpsnoop_parse_ipv4(pkt->buffer, pkt->len, port, &rebind);
+       else
+               type = dhcpsnoop_parse_ipv6(pkt->buffer, pkt->len, port);
+
+       if (!type)
+               return;
+
+       dhcpsnoop_ubus_notify(type, pkt->buffer, pkt->len);
+       if (!ipv6 && !strcmp(type, "ack") && rebind)
+               cache_entry(pkt->buffer, rebind);
+}
+
+static void
+dhcpsnoop_socket_cb(struct uloop_fd *fd, unsigned int events)
+{
+       static uint8_t buf[8192];
+       struct packet pkt = {
+               .buffer = buf,
+       };
+       int len;
+
+retry:
+       len = recvfrom(fd->fd, buf, sizeof(buf), MSG_DONTWAIT, NULL, NULL);
+       if (len < 0) {
+               if (errno == EINTR)
+                       goto retry;
+               return;
+       }
+
+       if (!len)
+               return;
+
+       pkt.len = len;
+       dhcpsnoop_packet_cb(&pkt);
+}
+
+static int
+dhcpsnoop_open_socket(void)
+{
+       struct sockaddr_ll sll = {
+               .sll_family = AF_PACKET,
+               .sll_protocol = htons(ETH_P_ALL),
+       };
+       int sock;
+
+       sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
+       if (sock == -1) {
+               ULOG_ERR("failed to create raw socket: %s\n", strerror(errno));
+               return -1;
+       }
+
+       sll.sll_ifindex = if_nametoindex(DHCPSNOOP_IFB_NAME);
+       if (bind(sock, (struct sockaddr *)&sll, sizeof(sll))) {
+               ULOG_ERR("failed to bind socket to "DHCPSNOOP_IFB_NAME": %s\n",
+                        strerror(errno));
+               goto error;
+       }
+
+       fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
+
+       ufd.fd = sock;
+       ufd.cb = dhcpsnoop_socket_cb;
+       uloop_fd_add(&ufd, ULOOP_READ);
+
+       return 0;
+
+error:
+       close(sock);
+       return -1;
+}
+
+static int
+prepare_filter_cmd(char *buf, int len, const char *dev, int prio, bool add, bool egress)
+{
+       return snprintf(buf, len, "tc filter %s dev '%s' %sgress prio %d",
+                       add ? "add" : "del", dev, egress ? "e" : "in", prio);
+}
+
+#define MATCH_GRE_ETH_IP_UDP_DHCP_67 \
+       " match u16 0x6558 0xffff at 22 " \
+       " match u16 0x0800 0xffff at 36 " \
+       " match u8 17 0xff at 47 " \
+       " match u16 67 0xffff at 58 "
+
+#define MATCH_GRE_ETH_VLAN_IP_UDP_DHCP_67 \
+       " match u16 0x6558 0xffff at 22 " \
+       " match u16 0x8100 0xffff at 36 " \
+       " match u16 0x0800 0xffff at 40 " \
+       " match u8 17 0xff at 51 " \
+       " match u16 67 0xffff at 62 "
+
+#define MATCH_GRE_ETH_IP_UDP_DHCP_68 \
+       " match u16 0x6558 0xffff at 22 " \
+       " match u16 0x0800 0xffff at 36 " \
+       " match u8 17 0xff at 47 " \
+       " match u16 68 0xffff at 58 "
+
+#define MATCH_GRE_ETH_VLAN_IP_UDP_DHCP_68 \
+       " match u16 0x6558 0xffff at 22 " \
+       " match u16 0x8100 0xffff at 36 " \
+       " match u16 0x0800 0xffff at 40 " \
+       " match u8 17 0xff at 51 " \
+       " match u16 68 0xffff at 62 "
+
+static void
+dhcpsnoop_dev_attach_filters(struct device *dev, bool egress)
+{
+       int prio = DHCPSNOOP_PRIO_BASE;
+       char buf[350];
+       int ofs;
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol ip u32 match ip sport 67 0xffff"
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol 802.1Q u32 offset plus 4 match ip sport 67 0xffff"
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol ip u32 match ip sport 68 0xffff"
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol 802.1Q u32 offset plus 4 match ip sport 68 0xffff"
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       /* GRE */
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol ip u32 match ip protocol 47 0xff"
+                        MATCH_GRE_ETH_IP_UDP_DHCP_67
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol ip u32 match ip protocol 47 0xff"
+                        MATCH_GRE_ETH_IP_UDP_DHCP_68
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol ip u32 match ip protocol 47 0xff "
+                        MATCH_GRE_ETH_VLAN_IP_UDP_DHCP_67
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol ip u32 match ip protocol 47 0xff"
+                        MATCH_GRE_ETH_VLAN_IP_UDP_DHCP_68
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       /* IPv6 */
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol ipv6 u32 match ip6 sport 546 0xfffe"
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+
+       ofs = prepare_filter_cmd(buf, sizeof(buf), dev->ifname, prio++, true, egress);
+       APPEND(buf, ofs, " protocol 802.1Q u32 offset plus 4 match ip6 sport 546 0xfffe"
+                        " flowid 1:1 action mirred ingress mirror dev " DHCPSNOOP_IFB_NAME " continue");
+       dhcpsnoop_run_cmd(buf, false);
+}
+
+static void
+dhcpsnoop_dev_cleanup_filters(struct device *dev, bool egress)
+{
+       char buf[128];
+       int i;
+
+       for (i = DHCPSNOOP_PRIO_BASE; i < DHCPSNOOP_PRIO_BASE + 10; i++) {
+               prepare_filter_cmd(buf, sizeof(buf), dev->ifname, i, false, egress);
+               dhcpsnoop_run_cmd(buf, true);
+       }
+}
+
+static void
+dhcpsnoop_dev_attach(struct device *dev)
+{
+       char buf[64];
+
+       dev->active = true;
+       snprintf(buf, sizeof(buf), "tc qdisc add dev '%s' clsact", dev->ifname);
+       dhcpsnoop_run_cmd(buf, true);
+
+       if (dev->ingress)
+               dhcpsnoop_dev_attach_filters(dev, false);
+       if (dev->egress)
+               dhcpsnoop_dev_attach_filters(dev, true);
+}
+
+static void
+dhcpsnoop_dev_cleanup(struct device *dev)
+{
+       dev->active = false;
+       dhcpsnoop_dev_cleanup_filters(dev, true);
+       dhcpsnoop_dev_cleanup_filters(dev, false);
+}
+
+static void
+__dhcpsnoop_dev_check(struct device *dev)
+{
+       int ifindex;
+
+       ifindex = if_nametoindex(dev->ifname);
+       if (ifindex != dev->ifindex) {
+               dev->ifindex = ifindex;
+               dev->changed = true;
+       }
+
+       if (!dev->changed)
+               return;
+
+       dev->changed = false;
+       dhcpsnoop_dev_cleanup(dev);
+       if (ifindex)
+               dhcpsnoop_dev_attach(dev);
+}
+
+static void dev_update_cb(struct vlist_tree *tree, struct vlist_node *node_new,
+                         struct vlist_node *node_old)
+{
+       struct device *dev = NULL, *dev_free = NULL;
+
+       if (node_old && node_new) {
+               dev = container_of(node_old, struct device, node);
+               dev_free = container_of(node_new, struct device, node);
+
+               if (dev->ingress != dev_free->ingress ||
+                       dev->egress != dev_free->egress)
+                       dev->changed = true;
+
+               dev->ingress = dev_free->ingress;
+               dev->egress = dev_free->egress;
+       } else if (node_old) {
+               dev_free = container_of(node_old, struct device, node);
+               if (dev_free->active)
+                       dhcpsnoop_dev_cleanup(dev_free);
+       } else if (node_new) {
+               dev = container_of(node_new, struct device, node);
+       }
+
+       if (dev)
+               __dhcpsnoop_dev_check(dev);
+       if (dev_free)
+               free(dev_free);
+}
+
+static void
+dhcpsnoop_dev_config_add(struct blob_attr *data)
+{
+       enum {
+               DEV_ATTR_INGRESS,
+               DEV_ATTR_EGRESS,
+               __DEV_ATTR_MAX
+       };
+       static const struct blobmsg_policy policy[__DEV_ATTR_MAX] = {
+               [DEV_ATTR_INGRESS] = { "ingress", BLOBMSG_TYPE_BOOL },
+               [DEV_ATTR_EGRESS] = { "egress", BLOBMSG_TYPE_BOOL },
+       };
+       struct blob_attr *tb[__DEV_ATTR_MAX];
+       struct blob_attr *cur;
+       struct device *dev;
+       int len;
+
+       if (blobmsg_type(data) != BLOBMSG_TYPE_TABLE)
+               return;
+
+       dev = calloc(1, sizeof(*dev));
+       len = snprintf(dev->ifname, sizeof(dev->ifname), "%s", blobmsg_name(data));
+       if (!len || len > IFNAMSIZ)
+               goto free;
+
+       blobmsg_parse(policy, ARRAY_SIZE(tb), tb, blobmsg_data(data), blobmsg_len(data));
+
+       if ((cur = tb[DEV_ATTR_INGRESS]) != NULL)
+               dev->ingress = blobmsg_get_bool(cur);
+       if ((cur = tb[DEV_ATTR_EGRESS]) != NULL)
+               dev->egress = blobmsg_get_bool(cur);
+
+       if (!dev->ingress && !dev->egress)
+               goto free;
+
+       vlist_add(&devices, &dev->node, dev->ifname);
+       return;
+
+free:
+       free(dev);
+       return;
+}
+
+void dhcpsnoop_dev_config_update(struct blob_attr *data, bool add_only)
+{
+       struct blob_attr *cur;
+       int rem;
+
+       if (!add_only)
+               vlist_update(&devices);
+
+       blobmsg_for_each_attr(cur, data, rem)
+               dhcpsnoop_dev_config_add(cur);
+
+       if (!add_only)
+               vlist_flush(&devices);
+}
+
+void dhcpsnoop_dev_check(void)
+{
+       struct device *dev;
+
+       vlist_for_each_element(&devices, dev, node)
+               __dhcpsnoop_dev_check(dev);
+}
+
+int dhcpsnoop_dev_init(void)
+{
+       dhcpsnoop_dev_done();
+
+       if (dhcpsnoop_run_cmd("ip link add "DHCPSNOOP_IFB_NAME" type ifb", false) ||
+           dhcpsnoop_run_cmd("ip link set dev "DHCPSNOOP_IFB_NAME" up", false) ||
+           dhcpsnoop_open_socket())
+               return -1;
+
+       return 0;
+}
+
+void dhcpsnoop_dev_done(void)
+{
+       if (ufd.registered) {
+               uloop_fd_delete(&ufd);
+               close(ufd.fd);
+       }
+
+       dhcpsnoop_run_cmd("ip link del "DHCPSNOOP_IFB_NAME, true);
+       vlist_flush_all(&devices);
+}
diff --git a/net/udhcpsnoop/src/dhcp.c b/net/udhcpsnoop/src/dhcp.c
new file mode 100644 (file)
index 0000000..3d27927
--- /dev/null
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Felix Fietkau <[email protected]>
+ */
+
+#include "dhcpsnoop.h"
+#include "msg.h"
+
+const char *dhcpsnoop_parse_ipv4(const void *buf, size_t len, uint16_t port, uint32_t *expire)
+{
+       const struct dhcpv4_message *msg = buf;
+       const uint8_t *pos, *end;
+       uint32_t leasetime = 0, rebind = 0, renew = 0;
+       char type = 0;
+
+       if (port != 67 && port != 68)
+               return NULL;
+
+       if (len < sizeof(*msg))
+               return NULL;
+
+       if (ntohl(msg->magic) != DHCPV4_MAGIC)
+               return NULL;
+
+       pos = msg->options;
+       end = buf + len;
+
+       while (pos < end) {
+               const uint8_t *opt = pos++;
+
+               if (*opt == DHCPV4_OPT_END)
+                       break;
+
+               if (*opt == DHCPV4_OPT_PAD)
+                       continue;
+
+               if (pos >= end || 1 + *pos > end - pos)
+                       break;
+
+               pos += *pos + 1;
+               if (pos >= end)
+                       break;
+
+               switch (*opt) {
+               case DHCPV4_OPT_MSG_TYPE:
+                       if (!opt[1])
+                               continue;
+                       type = opt[2];
+                       break;
+               case DHCPV4_OPT_LEASETIME:
+                       if (opt[1] != 4)
+                               continue;
+                       leasetime = *((uint32_t *) &opt[2]);
+                       break;
+               case DHCPV4_OPT_REBIND:
+                       if (opt[1] != 4)
+                               continue;
+                       rebind = *((uint32_t *) &opt[2]);
+                       break;
+               case DHCPV4_OPT_RENEW:
+                       if (opt[1] != 4)
+                               continue;
+                       renew = *((uint32_t *) &opt[2]);
+                       break;
+               }
+       }
+
+       if (renew)
+               *expire = renew;
+       else if (rebind)
+               *expire = rebind;
+       else if (leasetime)
+               *expire = leasetime;
+       else
+               *expire = 24 * 60 * 60;
+       *expire = ntohl(*expire);
+
+       switch(type) {
+       case DHCPV4_MSG_ACK:
+               return "ack";
+       case DHCPV4_MSG_DISCOVER:
+               return "discover";
+       case DHCPV4_MSG_OFFER:
+               return "offer";
+       case DHCPV4_MSG_REQUEST:
+               return "request";
+       }
+
+       return NULL;
+}
+
+const char *dhcpsnoop_parse_ipv6(const void *buf, size_t len, uint16_t port)
+{
+       const struct dhcpv6_message *msg = buf;
+
+       if (port != 546 && port != 547)
+               return NULL;
+
+       switch(msg->msg_type) {
+       case DHCPV6_MSG_SOLICIT:
+               return "solicit";
+       case DHCPV6_MSG_REPLY:
+               return "reply";
+       case DHCPV6_MSG_RENEW:
+               return "renew";
+       default:
+               return NULL;
+       }
+}
+
+
diff --git a/net/udhcpsnoop/src/dhcpsnoop.h b/net/udhcpsnoop/src/dhcpsnoop.h
new file mode 100644 (file)
index 0000000..cc46ceb
--- /dev/null
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Felix Fietkau <[email protected]>
+ */
+#ifndef __DHCPSNOOP_H
+#define __DHCPSNOOP_H
+
+#include <libubox/blobmsg.h>
+#include <libubox/ulog.h>
+#include <libubox/uloop.h>
+
+#define DHCPSNOOP_IFB_NAME "ifb-dhcp"
+#define DHCPSNOOP_PRIO_BASE    0x100
+
+int dhcpsnoop_run_cmd(char *cmd, bool ignore_error);
+
+int dhcpsnoop_dev_init(void);
+void dhcpsnoop_dev_done(void);
+void dhcpsnoop_dev_config_update(struct blob_attr *data, bool add_only);
+void dhcpsnoop_dev_check(void);
+
+void dhcpsnoop_ubus_init(void);
+void dhcpsnoop_ubus_done(void);
+void dhcpsnoop_ubus_notify(const char *type, const uint8_t *msg, size_t len);
+
+const char *dhcpsnoop_parse_ipv4(const void *buf, size_t len, uint16_t port, uint32_t *rebind);
+const char *dhcpsnoop_parse_ipv6(const void *buf, size_t len, uint16_t port);
+
+void cache_entry(void *msg, uint32_t rebind);
+void cache_dump(struct blob_buf *b);
+
+#endif
diff --git a/net/udhcpsnoop/src/main.c b/net/udhcpsnoop/src/main.c
new file mode 100644 (file)
index 0000000..a4fe70c
--- /dev/null
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Felix Fietkau <[email protected]>
+ */
+#include <sys/wait.h>
+#include <unistd.h>
+#include <stdio.h>
+#include "dhcpsnoop.h"
+
+int dhcpsnoop_run_cmd(char *cmd, bool ignore_error)
+{
+       char *argv[] = { "sh", "-c", cmd, NULL };
+       bool first = true;
+       int status = -1;
+       char buf[512];
+       int fds[2];
+       FILE *f;
+       int pid;
+
+       if (pipe(fds))
+               return -1;
+
+       pid = fork();
+       if (!pid) {
+               close(fds[0]);
+               if (fds[1] != STDOUT_FILENO)
+                       dup2(fds[1], STDOUT_FILENO);
+               if (fds[1] != STDERR_FILENO)
+                       dup2(fds[1], STDERR_FILENO);
+               if (fds[1] > STDERR_FILENO)
+                       close(fds[1]);
+               execv("/bin/sh", argv);
+               exit(1);
+       }
+
+       if (pid < 0)
+               return -1;
+
+       close(fds[1]);
+       f = fdopen(fds[0], "r");
+       if (!f) {
+               close(fds[0]);
+               goto out;
+       }
+
+       while (fgets(buf, sizeof(buf), f) != NULL) {
+               if (!strlen(buf))
+                       break;
+               if (ignore_error)
+                       continue;
+               if (first) {
+                       ULOG_WARN("Command: %s\n", cmd);
+                       first = false;
+               }
+               ULOG_WARN("%s%s", buf, strchr(buf, '\n') ? "" : "\n");
+       }
+
+       fclose(f);
+
+out:
+       while (waitpid(pid, &status, 0) < 0)
+               if (errno != EINTR)
+                       break;
+
+       return status;
+}
+
+int main(int argc, char **argv)
+{
+       ulog_open(ULOG_STDIO | ULOG_SYSLOG, LOG_DAEMON, "udhcpsnoop");
+       uloop_init();
+       dhcpsnoop_ubus_init();
+       dhcpsnoop_dev_init();
+
+       ulog_threshold(LOG_INFO);
+       uloop_run();
+
+       dhcpsnoop_ubus_done();
+       dhcpsnoop_dev_done();
+       uloop_done();
+
+       return 0;
+}
+
diff --git a/net/udhcpsnoop/src/msg.h b/net/udhcpsnoop/src/msg.h
new file mode 100644 (file)
index 0000000..7fc3483
--- /dev/null
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: BSD-3-Clause */
+#ifndef __DHCPSNOOP_MSG_H
+#define __DHCPSNOOP_MSG_H
+
+#include <netinet/in.h>
+#include <stdint.h>
+
+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,
+       DHCPV4_MSG_FORCERENEW = 9,
+};
+
+enum dhcpv4_opt {
+       DHCPV4_OPT_PAD = 0,
+       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_REQOPTS = 55,
+       DHCPV4_OPT_RENEW = 58,
+       DHCPV4_OPT_REBIND = 59,
+       DHCPV4_OPT_IPADDRESS = 50,
+       DHCPV4_OPT_MSG_TYPE = 53,
+       DHCPV4_OPT_HOSTNAME = 12,
+       DHCPV4_OPT_REQUEST = 17,
+       DHCPV4_OPT_USER_CLASS = 77,
+       DHCPV4_OPT_AUTHENTICATION = 90,
+       DHCPV4_OPT_SEARCH_DOMAIN = 119,
+       DHCPV4_OPT_FORCERENEW_NONCE_CAPABLE = 145,
+       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];
+       uint32_t magic;
+       uint8_t options[];
+} __attribute__((packed));
+
+#define DHCPV4_MAGIC 0x63825363
+
+enum dhcpv6_opt {
+       DHCPV6_MSG_SOLICIT = 1,
+       DHCPV6_MSG_ADVERTISE = 2,
+       DHCPV6_MSG_REQUEST = 3,
+       DHCPV6_MSG_CONFIRM = 4,
+       DHCPV6_MSG_RENEW = 5,
+       DHCPV6_MSG_REBIND = 6,
+       DHCPV6_MSG_REPLY = 7,
+       DHCPV6_MSG_RELEASE = 8,
+       DHCPV6_MSG_DECLINE = 9,
+       DHCPV6_MSG_RECONFIGURE = 10,
+       DHCPV6_MSG_INFORMATION_REQUEST = 11,
+       DHCPV6_MSG_RELAY_FORW = 12,
+       DHCPV6_MSG_RELAY_REPL = 13,
+};
+struct dhcpv6_message {
+       uint8_t msg_type;
+       uint8_t transaction_id[3];
+       uint8_t options[];
+} __attribute__((packed));
+
+#endif
diff --git a/net/udhcpsnoop/src/ubus.c b/net/udhcpsnoop/src/ubus.c
new file mode 100644 (file)
index 0000000..4567e1c
--- /dev/null
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Felix Fietkau <[email protected]>
+ */
+#include <libubus.h>
+
+#include "dhcpsnoop.h"
+
+enum {
+       DS_CONFIG_DEVICES,
+       __DS_CONFIG_MAX
+};
+
+static const struct blobmsg_policy dhcpsnoop_config_policy[__DS_CONFIG_MAX] = {
+       [DS_CONFIG_DEVICES] = { "devices", BLOBMSG_TYPE_TABLE },
+};
+
+static struct blob_buf b;
+
+static int
+dhcpsnoop_ubus_config(struct ubus_context *ctx, struct ubus_object *obj,
+                  struct ubus_request_data *req, const char *method,
+                  struct blob_attr *msg)
+{
+       struct blob_attr *tb[__DS_CONFIG_MAX];
+
+       blobmsg_parse(dhcpsnoop_config_policy, __DS_CONFIG_MAX, tb,
+                     blobmsg_data(msg), blobmsg_len(msg));
+
+       dhcpsnoop_dev_config_update(tb[DS_CONFIG_DEVICES], false);
+
+       dhcpsnoop_dev_check();
+
+       return 0;
+}
+
+
+static int
+dhcpsnoop_ubus_add_devices(struct ubus_context *ctx, struct ubus_object *obj,
+                          struct ubus_request_data *req, const char *method,
+                          struct blob_attr *msg)
+{
+       struct blob_attr *tb[__DS_CONFIG_MAX];
+
+       blobmsg_parse(dhcpsnoop_config_policy, __DS_CONFIG_MAX, tb,
+                     blobmsg_data(msg), blobmsg_len(msg));
+
+       dhcpsnoop_dev_config_update(tb[DS_CONFIG_DEVICES], true);
+
+       dhcpsnoop_dev_check();
+
+       return 0;
+}
+
+static int
+dhcpsnoop_ubus_check_devices(struct ubus_context *ctx, struct ubus_object *obj,
+                         struct ubus_request_data *req, const char *method,
+                         struct blob_attr *msg)
+{
+       dhcpsnoop_dev_check();
+
+       return 0;
+}
+
+static int
+dhcpsnoop_ubus_dump(struct ubus_context *ctx, struct ubus_object *obj,
+                   struct ubus_request_data *req, const char *method,
+                   struct blob_attr *msg)
+{
+       blob_buf_init(&b, 0);
+
+       cache_dump(&b);
+
+       ubus_send_reply(ctx, req, b.head);
+
+       return 0;
+}
+
+static const struct ubus_method dhcpsnoop_methods[] = {
+       UBUS_METHOD("config", dhcpsnoop_ubus_config, dhcpsnoop_config_policy),
+       UBUS_METHOD("add_devices", dhcpsnoop_ubus_add_devices, dhcpsnoop_config_policy),
+       UBUS_METHOD_NOARG("check_devices", dhcpsnoop_ubus_check_devices),
+       UBUS_METHOD_NOARG("dump", dhcpsnoop_ubus_dump),
+};
+
+static struct ubus_object_type dhcpsnoop_object_type =
+       UBUS_OBJECT_TYPE("dhcpsnoop", dhcpsnoop_methods);
+
+static struct ubus_object dhcpsnoop_object = {
+       .name = "dhcpsnoop",
+       .type = &dhcpsnoop_object_type,
+       .methods = dhcpsnoop_methods,
+       .n_methods = ARRAY_SIZE(dhcpsnoop_methods),
+};
+
+static void
+ubus_connect_handler(struct ubus_context *ctx)
+{
+       ubus_add_object(ctx, &dhcpsnoop_object);
+}
+
+static struct ubus_auto_conn conn;
+
+void dhcpsnoop_ubus_init(void)
+{
+       conn.cb = ubus_connect_handler;
+       ubus_auto_connect(&conn);
+}
+
+void dhcpsnoop_ubus_done(void)
+{
+       ubus_auto_shutdown(&conn);
+       blob_buf_free(&b);
+}
+
+void dhcpsnoop_ubus_notify(const char *type, const uint8_t *msg, size_t len)
+{
+       char *buf;
+
+       fprintf(stderr, "dhcp message type=%s\n", type);
+
+       if (!dhcpsnoop_object.has_subscribers)
+               return;
+
+       blob_buf_init(&b, 0);
+       buf = blobmsg_alloc_string_buffer(&b, "packet", 2 * len + 1);
+       while (len > 0) {
+               buf += sprintf(buf, "%02x", *msg);
+               msg++;
+               len--;
+       }
+       blobmsg_add_string_buffer(&b);
+
+       ubus_notify(&conn.ctx, &dhcpsnoop_object, type, b.head, -1);
+}