diff -urpN iproute2-7.0.0/include/libnetlink.h iproute2-7.0.0-iparp/include/libnetlink.h --- iproute2-7.0.0/include/libnetlink.h 2026-04-13 22:42:58.000000000 +0300 +++ iproute2-7.0.0-iparp/include/libnetlink.h 2026-05-20 20:12:14.882268792 +0300 @@ -74,6 +74,8 @@ int rtnl_neighdump_req(struct rtnl_handl __attribute__((warn_unused_result)); int rtnl_neightbldump_req(struct rtnl_handle *rth, int family) __attribute__((warn_unused_result)); +int rtnl_arpdump_req(struct rtnl_handle *rth, int family) + __attribute__((warn_unused_result)); int rtnl_mdbdump_req(struct rtnl_handle *rth, int family) __attribute__((warn_unused_result)); int rtnl_brvlandump_req(struct rtnl_handle *rth, int family, __u32 dump_flags) diff -urpN iproute2-7.0.0/include/uapi/linux/rtnetlink.h iproute2-7.0.0-iparp/include/uapi/linux/rtnetlink.h --- iproute2-7.0.0/include/uapi/linux/rtnetlink.h 2026-04-13 22:42:58.000000000 +0300 +++ iproute2-7.0.0-iparp/include/uapi/linux/rtnetlink.h 2026-05-20 20:12:14.882645500 +0300 @@ -206,6 +206,11 @@ enum { #define RTM_MAX (((__RTM_MAX + 3) & ~3) - 1) }; +#define RTM_NEWARPRULE iparp_RTM_NEWARPRULE +#define RTM_DELARPRULE iparp_RTM_DELARPRULE +#define RTM_GETARPRULE iparp_RTM_GETARPRULE +extern __u16 iparp_RTM_NEWARPRULE, iparp_RTM_DELARPRULE, iparp_RTM_GETARPRULE; + #define RTM_NR_MSGTYPES (RTM_MAX + 1 - RTM_BASE) #define RTM_NR_FAMILIES (RTM_NR_MSGTYPES >> 2) #define RTM_FAM(cmd) (((cmd) - RTM_BASE) >> 2) @@ -792,6 +797,9 @@ enum rtnetlink_groups { }; #define RTNLGRP_MAX (__RTNLGRP_MAX - 1) +#define RTNLGRP_ARP iparp_RTNLGRP_ARP +extern enum rtnetlink_groups iparp_RTNLGRP_ARP; + /* TC action piece */ struct tcamsg { unsigned char tca_family; @@ -841,6 +849,54 @@ enum { /* End of information exported to user level */ +/****************************************************************************** + * Definitions used in ARP tables administration + ****/ + +#define ARPA_TABLE_INPUT 0 +#define ARPA_TABLE_OUTPUT 1 +#define ARPA_TABLE_FORWARD 2 +#define ARPA_TABLE_ALL -1 + +#define ARPM_F_PREFSRC 0x0001 +#define ARPM_F_WILDIIF 0x0002 +#define ARPM_F_WILDOIF 0x0004 +#define ARPM_F_BROADCAST 0x0008 +#define ARPM_F_UNICAST 0x0010 + +struct arpmsg +{ + unsigned char arpm_family; + unsigned char arpm_table; + unsigned char arpm_action; + unsigned char arpm_from_len; + unsigned char arpm_to_len; + unsigned char arpm__pad1; + unsigned short arpm__pad2; + unsigned arpm_pref; + unsigned arpm_flags; +}; + +enum +{ + ARPA_UNSPEC, + ARPA_FROM, /* FROM IP prefix */ + ARPA_TO, /* TO IP prefix */ + ARPA_LLFROM, /* FROM LL prefix */ + ARPA_LLTO, /* TO LL prefix */ + ARPA_LLSRC, /* New SRC lladdr */ + ARPA_LLDST, /* New DST lladdr */ + ARPA_IIF, /* In interface prefix */ + ARPA_OIF, /* Out interface prefix */ + ARPA_SRC, /* New IP SRC */ + ARPA_DST, /* New IP DST, not used */ + ARPA_PACKETS, /* Packets */ +}; + +#define ARPA_MAX ARPA_PACKETS + +#define ARPA_RTA(r) ((struct rtattr*)(((char*)(r)) + NLMSG_ALIGN(sizeof(struct arpmsg)))) +#define ARPA_PAYLOAD(n) NLMSG_PAYLOAD(n,sizeof(struct arpmsg)) #endif /* __LINUX_RTNETLINK_H */ diff -urpN iproute2-7.0.0/ip/Makefile iproute2-7.0.0-iparp/ip/Makefile --- iproute2-7.0.0/ip/Makefile 2026-04-13 22:42:58.000000000 +0300 +++ iproute2-7.0.0-iparp/ip/Makefile 2026-05-20 20:12:14.882949208 +0300 @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 -IPOBJ=ip.o ipaddress.o ipaddrlabel.o iproute.o iprule.o ipnetns.o \ +IPOBJ=ip.o ipaddress.o ipaddrlabel.o iproute.o iprule.o iparp.o ipnetns.o \ rtm_map.o iptunnel.o ip6tunnel.o tunnel.o ipneigh.o ipntable.o iplink.o \ ipmaddr.o ipmonitor.o ipmroute.o ipprefix.o iptuntap.o iptoken.o \ ipxfrm.o xfrm_state.o xfrm_policy.o xfrm_monitor.o iplink_dummy.o \ diff -urpN iproute2-7.0.0/ip/ip.c iproute2-7.0.0-iparp/ip/ip.c --- iproute2-7.0.0/ip/ip.c 2026-04-13 22:42:58.000000000 +0300 +++ iproute2-7.0.0-iparp/ip/ip.c 2026-05-20 20:12:14.883130998 +0300 @@ -59,8 +59,8 @@ static void usage(void) fprintf(stderr, "Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }\n" " ip [ -force ] -batch filename\n" - "where OBJECT := { address | addrlabel | fou | help | ila | ioam | l2tp | link |\n" - " macsec | maddress | monitor | mptcp | mroute | mrule |\n" + "where OBJECT := { address | addrlabel | arp | fou | help | ila | ioam | l2tp |\n" + " link | macsec | maddress | monitor | mptcp | mroute | mrule |\n" " neighbor | neighbour | netconf | netns | nexthop | ntable |\n" " ntbl | route | rule | sr | stats | tap | tcpmetrics |\n" " token | tunnel | tuntap | vrf | xfrm }\n" @@ -87,6 +87,7 @@ static const struct cmd { } cmds[] = { { "address", do_ipaddr }, { "addrlabel", do_ipaddrlabel }, + { "arp", do_iparprule }, { "maddress", do_multiaddr }, { "route", do_iproute }, { "rule", do_iprule }, diff -urpN iproute2-7.0.0/ip/ip_common.h iproute2-7.0.0-iparp/ip/ip_common.h --- iproute2-7.0.0/ip/ip_common.h 2026-04-13 22:42:58.000000000 +0300 +++ iproute2-7.0.0-iparp/ip/ip_common.h 2026-05-20 20:12:14.883349514 +0300 @@ -72,6 +72,9 @@ int do_iptunnel(int argc, char **argv); int do_ip6tunnel(int argc, char **argv); int do_iptuntap(int argc, char **argv); int do_iplink(int argc, char **argv); +int do_iparprule(int argc, char **argv); +int print_arprule(struct nlmsghdr *n, void *arg); +int iparp_detect_version(int needed); int do_ipmacsec(int argc, char **argv); int do_ipmonitor(int argc, char **argv); int do_multiaddr(int argc, char **argv); diff -urpN iproute2-7.0.0/ip/iparp.c iproute2-7.0.0-iparp/ip/iparp.c --- iproute2-7.0.0/ip/iparp.c 1970-01-01 02:00:00.000000000 +0200 +++ iproute2-7.0.0-iparp/ip/iparp.c 2026-05-20 20:12:40.115085105 +0300 @@ -0,0 +1,1033 @@ +/* + * iparp.c "ip arp". + * + * Authors: Julian Anastasov , March 2002-2023 + * + * 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; + * + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rt_names.h" +#include "utils.h" +#include "ip_common.h" +#include "json_print.h" + +static void usage(void) __attribute__((noreturn)); + +#define CMD_LIST 0x0001 /* list, lst, show */ +#define CMD_APPEND 0x0002 /* append */ +#define CMD_PREPEND 0x0004 /* prepend, insert */ +#define CMD_ADD 0x0008 /* add, create */ +#define CMD_DEL 0x0010 /* delete, remove */ +#define CMD_CHANGE 0x0020 /* change, chg, update */ +#define CMD_REPLACE 0x0040 /* replace, set */ +#define CMD_FLUSH 0x0080 /* flush */ +#define CMD_TEST 0x0100 /* test */ + +typedef struct { + char *name; + int code; +} strarr_t; + +static strarr_t cmds[] = { + { "list", CMD_LIST }, + { "lst", CMD_LIST }, + { "show", CMD_LIST }, + { "append", CMD_APPEND }, + { "prepend", CMD_PREPEND }, + { "insert", CMD_PREPEND }, + { "add", CMD_ADD }, + { "create", CMD_ADD }, + { "delete", CMD_DEL }, + { "remove", CMD_DEL }, + { "change", CMD_CHANGE }, + { "chg", CMD_CHANGE }, + { "update", CMD_CHANGE }, + { "replace", CMD_REPLACE }, + { "set", CMD_REPLACE }, + { "flush", CMD_FLUSH }, + { "test", CMD_TEST }, + + { NULL, 0 } +}; + +static strarr_t arp_tables[] = { + { "input", ARPA_TABLE_INPUT }, + { "output", ARPA_TABLE_OUTPUT }, + { "forward", ARPA_TABLE_FORWARD }, + + { NULL, 0 } +}; + +static strarr_t arp_actions[] = { + { "deny", 0 }, + { "allow", 1 }, + + { NULL, 0 } +}; + +static int cmd; + +struct compat_iparp_codes { + __u32 max_version; + __u16 rule_base; + enum rtnetlink_groups group; +}; + +/* The kernel patch is not official and always appends new constants, + * make sure we cover many kernels with same binary. + */ +static struct compat_iparp_codes code_table[] = { + /* We are not sure for the codes in future kernels */ +#define MAX_IPARP_VERSION KERNEL_VERSION(7, 0, 99) + + /* A kernel can add rules and/or groups, here is the list + * of such kernels, from latest to oldest supported kernel version + */ + { KERNEL_VERSION(6, 14, 0), 124, 40 }, + { KERNEL_VERSION(5, 18, 0), 124, 37 }, + { KERNEL_VERSION(5, 17, 0), 120, 35 }, + { KERNEL_VERSION(5, 13, 0), 120, 34 }, + { KERNEL_VERSION(5, 6, 0), 116, 34 }, + { KERNEL_VERSION(5, 5, 0), 112, 33 }, + { KERNEL_VERSION(5, 3, 0), 108, 33 }, + { KERNEL_VERSION(4, 19, 0), 104, 32 }, + { KERNEL_VERSION(4, 13, 0), 100, 32 }, + { KERNEL_VERSION(4, 11, 0), 96, 30 }, + { KERNEL_VERSION(4, 7, 0), 96, 29 }, + { KERNEL_VERSION(4, 1, 0), 92, 29 }, + { KERNEL_VERSION(4, 0, 0), 92, 27 }, + { KERNEL_VERSION(3, 8, 0), 88, 27 }, + { KERNEL_VERSION(3, 7, 0), 80, 24 }, + { KERNEL_VERSION(3, 1, 0), 80, 23 }, + { KERNEL_VERSION(2, 6, 29), 80, 23 }, + { KERNEL_VERSION(2, 6, 28), 76, 23 }, + { KERNEL_VERSION(2, 6, 25), 76, 21 }, + { KERNEL_VERSION(2, 6, 24), 72, 21 }, + { KERNEL_VERSION(2, 6, 19), 68, 20 }, + { KERNEL_VERSION(2, 6, 15), 68, 19 }, + { KERNEL_VERSION(2, 6, 13), 68, 15 }, +}; + +int iparp_detect_version(int needed) +{ + struct utsname un; + int m, p, s, v, i; + + if (iparp_RTM_NEWARPRULE) + return 1; + if (uname(&un) < 0) + goto err; + sscanf(un.release, "%d.%d.%d", &m, &p, &s); + v = KERNEL_VERSION(m, p, s); + if (v >= MAX_IPARP_VERSION) + goto err; + for (i = 0; i < ARRAY_SIZE(code_table); i++) { + if (v >= code_table[i].max_version) { + iparp_RTM_NEWARPRULE = code_table[i].rule_base + 0; + iparp_RTM_DELARPRULE = code_table[i].rule_base + 1; + iparp_RTM_GETARPRULE = code_table[i].rule_base + 2; + iparp_RTNLGRP_ARP = code_table[i].group; + return 1; + } + } + +err: + if (needed) { + fprintf(stderr, "ip arp: unsupported kernel version\n"); + exit(1); + } + return 0; +} + +static struct +{ + int tb; + int pref; + int action; + int flushed; + char *flushb; + int flushp; + int flushe; + inet_prefix rto; + inet_prefix mto; + inet_prefix rfrom; + inet_prefix mfrom; + inet_prefix rsrc; + __u8 llfrom[MAX_ADDR_LEN]; + __u8 llto[MAX_ADDR_LEN]; + __u8 llsrc[MAX_ADDR_LEN]; + __u8 lldst[MAX_ADDR_LEN]; + int llfrom_len; + int llto_len; + int llsrc_len; + int lldst_len; + int broadcasts; + int unicasts; + char *iif; + char *oif; + __u32 packets; +} f = { + tb: ARPA_TABLE_ALL, + action: -1, +}; + +static strarr_t *lookup_strarr(strarr_t *arr, char *name) +{ + strarr_t *p; + + for (p = arr; p->name; p++) { + if (matches(name, p->name) == 0) + return p; + } + return NULL; +} + +static strarr_t *lookup_strarr_code(strarr_t *arr, int code) +{ + strarr_t *p; + + for (p = arr; p->name; p++) { + if (p->code == code) + return p; + } + return NULL; +} + +static void usage(void) +{ + fprintf(stderr, "Usage: ip arp [ list | flush ] [ RULE ]\n"); + fprintf(stderr, " ip arp [ append | prepend | add | del | change | replace | test ] RULE\n"); + fprintf(stderr, "RULE := [ table TABLE_NAME ] [ pref NUMBER ] [ from PREFIX ] [ to PREFIX ]\n"); + fprintf(stderr, " [ iif STRING ] [ oif STRING ] [ llfrom PREFIX ] [ llto PREFIX ]\n"); + fprintf(stderr, " [ broadcasts ] [ unicasts ] [ ACTION ] [ ALTER ]\n"); + fprintf(stderr, "TABLE_NAME := [ input | forward | output ]\n"); + fprintf(stderr, "ACTION := [ deny | allow ]\n"); + fprintf(stderr, "ALTER := [ src IP ] [ llsrc LLADDR ] [ lldst LLADDR ]\n"); + exit(-1); +} + +static int flush_update(void) +{ + if (rtnl_send_check(&rth, f.flushb, f.flushp) < 0) { + perror("Failed to send flush request\n"); + return -1; + } + f.flushp = 0; + return 0; +} + +int print_arprule(struct nlmsghdr *n, void *arg) +{ + FILE *fp = (FILE*)arg; + struct arpmsg *r = NLMSG_DATA(n); + int len = n->nlmsg_len; + int host_len = 32, family = AF_INET; + struct rtattr * tb[RTA_MAX+1]; + inet_prefix vto, vfrom, vsrc; + __u32 packets = 0; + SPRINT_BUF(b1); + + if (n->nlmsg_type != RTM_NEWARPRULE && n->nlmsg_type != RTM_DELARPRULE) + return 0; + + if (f.flushb && n->nlmsg_type != RTM_NEWARPRULE) + return 0; + + len -= NLMSG_LENGTH(sizeof(*r)); + if (len < 0) + return -1; + + if (f.tb >= 0 && f.tb != r->arpm_table) + return 0; + + if (f.pref > 0 && f.pref != r->arpm_pref) + return 0; + + if (f.action >= 0 && f.action != r->arpm_action) + return 0; + + if (f.rto.family && f.rto.bitlen > r->arpm_to_len) + return 0; + if (f.mto.family && f.mto.bitlen >= 0 && + f.mto.bitlen < r->arpm_to_len) + return 0; + + if (f.rfrom.family && f.rfrom.bitlen > r->arpm_from_len) + return 0; + if (f.mfrom.family && f.mfrom.bitlen >= 0 && + f.mfrom.bitlen < r->arpm_from_len) + return 0; + + if (f.broadcasts && !(r->arpm_flags & ARPM_F_BROADCAST)) + return 0; + + if (f.unicasts && !(r->arpm_flags & ARPM_F_UNICAST)) + return 0; + + memset(tb, 0, sizeof(tb)); + parse_rtattr(tb, ARPA_MAX, ARPA_RTA(r), len); + + memset(&vto, 0, sizeof(vto)); + if (tb[ARPA_TO]) + memcpy(&vto.data, RTA_DATA(tb[ARPA_TO]), (r->arpm_to_len+7)/8); + + memset(&vfrom, 0, sizeof(vfrom)); + if (tb[ARPA_FROM]) + memcpy(&vfrom.data, RTA_DATA(tb[ARPA_FROM]), + (r->arpm_from_len+7)/8); + + memset(&vsrc, 0, sizeof(vsrc)); + if (tb[ARPA_SRC]) + memcpy(&vsrc.data, RTA_DATA(tb[ARPA_SRC]), 4); + + if (f.rto.family && inet_addr_match(&vto, &f.rto, f.rto.bitlen)) + return 0; + if (f.mto.family && f.mto.bitlen >= 0 && + inet_addr_match(&vto, &f.mto, r->arpm_to_len)) + return 0; + + if (f.rfrom.family && inet_addr_match(&vfrom, &f.rfrom, f.rfrom.bitlen)) + return 0; + if (f.mfrom.family && f.mfrom.bitlen >= 0 && + inet_addr_match(&vfrom, &f.mfrom, r->arpm_from_len)) + return 0; + + if (f.rsrc.family && inet_addr_match(&vsrc, &f.rsrc, f.rsrc.bitlen)) + return 0; + + if (f.iif) { + char *dev, *cp; + int size; + + if (!tb[ARPA_IIF]) + return 0; + dev = (char*)RTA_DATA(tb[ARPA_IIF]); + size = strlen(dev); + if ((cp = strchr(f.iif, '+')) && size > cp - f.iif) + size = cp - f.iif; + if (strncmp(dev, f.iif, size) || + (0 != f.iif[size] && '+' != f.iif[size])) + return 0; + } + + if (f.oif) { + char *dev, *cp; + int size; + + if (!tb[ARPA_OIF]) + return 0; + dev = (char*)RTA_DATA(tb[ARPA_OIF]); + size = strlen(dev); + if ((cp = strchr(f.oif, '+')) && size > cp - f.oif) + size = cp - f.oif; + if (strncmp(dev, f.oif, size) || + (0 != f.oif[size] && '+' != f.oif[size])) + return 0; + } + + if (f.packets) { + if (tb[ARPA_PACKETS]) + memcpy(&packets, RTA_DATA(tb[ARPA_PACKETS]), 4); + if (packets < f.packets) + return 0; + } + + if (f.flushb) { + struct nlmsghdr *fn; + if (NLMSG_ALIGN(f.flushp) + n->nlmsg_len > f.flushe) { + if (flush_update()) + return -1; + } + fn = (struct nlmsghdr*)(f.flushb + NLMSG_ALIGN(f.flushp)); + memcpy(fn, n, n->nlmsg_len); + fn->nlmsg_type = RTM_DELARPRULE; + fn->nlmsg_flags = NLM_F_REQUEST; + fn->nlmsg_seq = ++rth.seq; + f.flushp = (((char*)fn) + n->nlmsg_len) - f.flushb; + f.flushed++; + if (show_stats < 2) + return 0; + } + + print_headers(fp, "[ARPRULE]"); + + open_json_object(NULL); + + if (n->nlmsg_type == RTM_DELARPRULE) + print_bool(PRINT_ANY, "deleted", "Deleted ", true); + + { + strarr_t *p = lookup_strarr_code(arp_tables, r->arpm_table); + char *text = p? p->name:"unknown"; + char buf1[64]; + + snprintf(buf1, sizeof(buf1), "%s ", text); + print_null(PRINT_ANY, text, buf1, NULL); + } + + print_uint(PRINT_ANY, "rule", "rule %u ", r->arpm_pref); + + { + strarr_t *p = lookup_strarr_code(arp_actions, r->arpm_action); + char *text = p? p->name:"unk"; + char buf1[64]; + + snprintf(buf1, sizeof(buf1), "%s ", text); + print_null(PRINT_ANY, text, buf1, NULL); + } + + if (tb[ARPA_FROM]) { + const char *a = rt_addr_n2a_rta(family, tb[ARPA_FROM]); + + print_string(PRINT_FP, NULL, "from ", NULL); + print_color_string(PRINT_ANY, ifa_family_color(family), + "from", "%s", a); + if (r->arpm_from_len != host_len) + print_uint(PRINT_ANY, "fromlen", "/%u", + r->arpm_from_len); + } else if (r->arpm_from_len) { + print_string(PRINT_ANY, "from", "from %s", "0"); + print_uint(PRINT_ANY, "fromlen", "/%u", r->arpm_from_len); + } else { + print_string(PRINT_ANY, "from", "from %s", "all"); + } + + if (tb[ARPA_TO]) { + const char *a = rt_addr_n2a_rta(family, tb[ARPA_TO]); + + print_string(PRINT_FP, NULL, " to ", NULL); + print_color_string(PRINT_ANY, ifa_family_color(family), + "to", "%s", a); + if (r->arpm_to_len != host_len) + print_uint(PRINT_ANY, "tolen", "/%u", + r->arpm_to_len); + } else if (r->arpm_to_len) { + print_string(PRINT_ANY, "to", " to %s", "0"); + print_uint(PRINT_ANY, "tolen", "/%u", r->arpm_to_len); + } else { + print_string(PRINT_ANY, "to", " to %s", "all"); + } + + if (tb[ARPA_LLFROM]) { + const char *a = ll_addr_n2a(RTA_DATA(tb[ARPA_LLFROM]), + RTA_PAYLOAD(tb[ARPA_LLFROM]), + ARPHRD_VOID, + b1, sizeof(b1)); + print_string(PRINT_FP, NULL, " llfrom ", NULL); + print_color_string(PRINT_ANY, COLOR_MAC, + "llfrom", "%s", a); + } + + if (tb[ARPA_LLTO]) { + const char *a = ll_addr_n2a(RTA_DATA(tb[ARPA_LLTO]), + RTA_PAYLOAD(tb[ARPA_LLTO]), + ARPHRD_VOID, + b1, sizeof(b1)); + print_string(PRINT_FP, NULL, " llto ", NULL); + print_color_string(PRINT_ANY, COLOR_MAC, + "llto", "%s", a); + } + + if (tb[ARPA_IIF]) { + snprintf(b1, sizeof(b1), "%s%s", + (char*)RTA_DATA(tb[ARPA_IIF]), + (r->arpm_flags & ARPM_F_WILDIIF) ? "+" : ""); + if (!is_json_context()) + fprintf(fp, " iif "); + print_color_string(PRINT_ANY, COLOR_IFNAME, + "iif", "%s", b1); + } + + if (tb[ARPA_OIF]) { + snprintf(b1, sizeof(b1), "%s%s", + (char*)RTA_DATA(tb[ARPA_OIF]), + (r->arpm_flags & ARPM_F_WILDOIF) ? "+" : ""); + if (!is_json_context()) + fprintf(fp, " oif "); + print_color_string(PRINT_ANY, COLOR_IFNAME, + "oif", "%s", b1); + } + + if (r->arpm_flags & ARPM_F_BROADCAST) { + print_bool(PRINT_ANY, "broadcasts", " broadcasts", true); + } + + if (r->arpm_flags & ARPM_F_UNICAST) { + print_bool(PRINT_ANY, "unicasts", " unicasts", true); + } + + if (tb[ARPA_LLSRC]) { + const char *a = ll_addr_n2a(RTA_DATA(tb[ARPA_LLSRC]), + RTA_PAYLOAD(tb[ARPA_LLSRC]), + ARPHRD_VOID, + b1, sizeof(b1)); + print_string(PRINT_FP, NULL, " llsrc ", NULL); + print_color_string(PRINT_ANY, COLOR_MAC, + "llsrc", "%s", a); + } + + if (tb[ARPA_LLDST]) { + const char *a = ll_addr_n2a(RTA_DATA(tb[ARPA_LLDST]), + RTA_PAYLOAD(tb[ARPA_LLDST]), + ARPHRD_VOID, + b1, sizeof(b1)); + print_string(PRINT_FP, NULL, " lldst ", NULL); + print_color_string(PRINT_ANY, COLOR_MAC, + "lldst", "%s", a); + } + + if (tb[ARPA_SRC]) { + print_string(PRINT_FP, NULL, " src ", NULL); + print_color_string(PRINT_ANY, ifa_family_color(family), + "src", "%s", + format_host_rta(family, tb[ARPA_SRC])); + } + + if (show_stats) { + print_string(PRINT_FP, NULL, "%s", _SL_); + + if (tb[ARPA_PACKETS]) + print_uint(PRINT_ANY, "packets", " packets %u", + *(unsigned*) RTA_DATA(tb[ARPA_PACKETS])); + } + + print_string(PRINT_FP, NULL, "\n", ""); + close_json_object(); + fflush(fp); + + return 0; +} + +static int iparprule_do_cmd(int c, int argc, char **argv) +{ + struct { + struct nlmsghdr n; + struct arpmsg r; + char buf[1024]; + } req; + int af = AF_INET; + int wild = c & (CMD_LIST | CMD_FLUSH); + char *dev = 0; + + f.tb = ARPA_TABLE_ALL; + f.action = -1; + + iparp_detect_version(1); + + if (!wild) { + memset(&req, 0, sizeof(req)); + + if (c & CMD_DEL) + req.n.nlmsg_type = RTM_DELARPRULE; + else + req.n.nlmsg_type = RTM_NEWARPRULE; + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct arpmsg)); + req.n.nlmsg_flags = NLM_F_REQUEST; + req.r.arpm_table = ARPA_TABLE_ALL; + + switch (c) { + case CMD_APPEND: + req.n.nlmsg_flags |= NLM_F_CREATE|NLM_F_APPEND; + break; + case CMD_PREPEND: + req.n.nlmsg_flags |= NLM_F_CREATE; + break; + case CMD_ADD: + req.n.nlmsg_flags |= NLM_F_CREATE|NLM_F_EXCL; + break; + case CMD_DEL: + req.n.nlmsg_flags |= 0; + break; + case CMD_CHANGE: + req.n.nlmsg_flags |= NLM_F_REPLACE; + break; + case CMD_REPLACE: + req.n.nlmsg_flags |= NLM_F_CREATE|NLM_F_REPLACE; + break; + case CMD_TEST: + req.n.nlmsg_flags |= NLM_F_EXCL; + break; + } + } + + for (; argc > 0; argc--, argv++) { + if (matches(*argv, "table") == 0 || + matches(*argv, "lookup") == 0) { + strarr_t *c; + + NEXT_ARG(); + if (f.tb != ARPA_TABLE_ALL) duparg("table", *argv); + c = lookup_strarr(arp_tables, *argv); + if (!c && strcmp(*argv, "all") != 0) + invarg("table value is invalid", *argv); + if (c) + f.tb = c->code; + continue; + } + + if (matches(*argv, "rule") == 0 || + matches(*argv, "preference") == 0 || + matches(*argv, "order") == 0 || + matches(*argv, "priority") == 0) { + NEXT_ARG(); + goto get_pref_; + } + + if (matches(*argv, "input") == 0) { + if (f.tb != ARPA_TABLE_ALL) duparg("input", *argv); + f.tb = ARPA_TABLE_INPUT; + continue; + } + + if (matches(*argv, "output") == 0) { + if (f.tb != ARPA_TABLE_ALL) duparg("output", *argv); + f.tb = ARPA_TABLE_OUTPUT; + continue; + } + + if (matches(*argv, "forward") == 0) { + if (f.tb != ARPA_TABLE_ALL) duparg("forward", *argv); + f.tb = ARPA_TABLE_FORWARD; + continue; + } + + if (matches(*argv, "allow") == 0) { + f.action = 1; + continue; + } + + if (matches(*argv, "deny") == 0 || + matches(*argv, "drop") == 0) { + f.action = 0; + continue; + } + + if (matches(*argv, "from") == 0) { + /* For any command */ + NEXT_ARG(); + if (wild && matches(*argv, "root") == 0) { + NEXT_ARG(); + get_prefix(&f.rfrom, *argv, af); + continue; + } + if (wild && matches(*argv, "match") == 0) { + NEXT_ARG(); + get_prefix(&f.mfrom, *argv, af); + continue; + } + if (matches(*argv, "exact") == 0) { + NEXT_ARG(); + } + if (f.mfrom.family) duparg("from", *argv); + get_prefix(&f.mfrom, *argv, af); + f.rfrom = f.mfrom; + if (!wild) { + req.r.arpm_from_len = f.mfrom.bitlen; + addattr_l(&req.n, sizeof(req), ARPA_FROM, + &f.mfrom.data, 4); + } + continue; + } + + if (matches(*argv, "to") == 0) { + /* For any command */ + NEXT_ARG(); + if (wild && matches(*argv, "root") == 0) { + NEXT_ARG(); + get_prefix(&f.rto, *argv, af); + continue; + } + if (wild && matches(*argv, "match") == 0) { + NEXT_ARG(); + get_prefix(&f.mto, *argv, af); + continue; + } + if (matches(*argv, "exact") == 0) { + NEXT_ARG(); + } + if (f.mto.family) duparg("to", *argv); + get_prefix(&f.mto, *argv, af); + f.rto = f.mto; + if (!wild) { + req.r.arpm_to_len = f.mto.bitlen; + addattr_l(&req.n, sizeof(req), ARPA_TO, + &f.mto.data, 4); + } + continue; + } + + if (matches(*argv, "src") == 0) { + NEXT_ARG(); + if (wild && matches(*argv, "root") == 0) { + NEXT_ARG(); + get_prefix(&f.rsrc, *argv, af); + continue; + } + if (matches(*argv, "exact") == 0) { + NEXT_ARG(); + } + if (f.rsrc.family) duparg("src", *argv); + get_prefix(&f.rsrc, *argv, af); + if (!f.rsrc.bitlen) { + f.rsrc.bitlen = 32; + f.rsrc.data[0] = 0; + } + if (f.rsrc.bitlen != 32) + invarg("src value is invalid", *argv); + if (!wild) + addattr_l(&req.n, sizeof(req), ARPA_SRC, + &f.rsrc.data, 4); + continue; + } + + if (matches(*argv, "llfrom") == 0) { + /* For any command */ + char llabuf[MAX_ADDR_LEN]; + int l; + + NEXT_ARG(); + + if (f.llfrom_len) duparg("llfrom", *argv); + + l = ll_addr_a2n(llabuf, sizeof(llabuf), *argv); + if (l < 0) return 1; + if (l > sizeof(llabuf)) l = sizeof(llabuf); + f.llfrom_len = l; + memcpy(f.llfrom, llabuf, l); + if (!wild) + addattr_l(&req.n, sizeof(req), ARPA_LLFROM, + llabuf, l); + continue; + } + + if (matches(*argv, "llto") == 0) { + /* For any command */ + char llabuf[MAX_ADDR_LEN]; + int l; + + NEXT_ARG(); + + if (f.llto_len) duparg("llto", *argv); + + l = ll_addr_a2n(llabuf, sizeof(llabuf), *argv); + if (l < 0) return 1; + if (l > sizeof(llabuf)) l = sizeof(llabuf); + f.llto_len = l; + memcpy(f.llto, llabuf, l); + if (!wild) + addattr_l(&req.n, sizeof(req), ARPA_LLTO, + llabuf, l); + continue; + } + + if (matches(*argv, "llsrc") == 0) { + /* For any command */ + char llabuf[MAX_ADDR_LEN]; + int l; + + NEXT_ARG(); + + if (f.llsrc_len) duparg("llsrc", *argv); + + l = ll_addr_a2n(llabuf, sizeof(llabuf), *argv); + if (l < 0) return 1; + if (l > sizeof(llabuf)) l = sizeof(llabuf); + f.llsrc_len = l; + memcpy(f.llsrc, llabuf, l); + if (!wild) + addattr_l(&req.n, sizeof(req), ARPA_LLSRC, + llabuf, l); + continue; + } + + if (matches(*argv, "lldst") == 0) { + /* For any command */ + char llabuf[MAX_ADDR_LEN]; + int l; + + NEXT_ARG(); + + if (f.lldst_len) duparg("lldst", *argv); + + l = ll_addr_a2n(llabuf, sizeof(llabuf), *argv); + if (l < 0) return 1; + if (l > sizeof(llabuf)) l = sizeof(llabuf); + f.lldst_len = l; + memcpy(f.lldst, llabuf, l); + if (!wild) + addattr_l(&req.n, sizeof(req), ARPA_LLDST, + llabuf, l); + continue; + } + + if (strcmp(*argv, "dev") == 0) { + NEXT_ARG(); + if (dev) duparg("dev", *argv); + dev = *argv; + continue; + } + + if (strcmp(*argv, "iif") == 0) { + NEXT_ARG(); + if (f.iif) duparg("iif", *argv); + f.iif = *argv; + continue; + } + + if (strcmp(*argv, "oif") == 0) { + NEXT_ARG(); + if (f.oif) duparg("oif", *argv); + f.oif = *argv; + continue; + } + + if (matches(*argv, "packets") == 0) { + NEXT_ARG(); + if (f.packets) duparg("packets", *argv); + if (get_u32(&f.packets, *argv, 0)) + invarg("packets value is invalid", *argv); + if (!wild) + addattr32(&req.n, sizeof(req), ARPA_PACKETS, + f.packets); + continue; + } + + if (matches(*argv, "broadcasts") == 0 || + strcmp(*argv, "brd") == 0) { + f.broadcasts = 1; + continue; + } + + if (matches(*argv, "unicasts") == 0) { + f.unicasts = 1; + continue; + } + + if (matches(*argv, "help") == 0) + usage(); + + get_pref_: + + { + __u32 pref; + + if (f.pref) duparg("preference", *argv); + + if (get_u32(&pref, *argv, 0) || !pref) + invarg("preference value is invalid", *argv); + f.pref = pref; + } + } + + if (f.tb == ARPA_TABLE_ALL && !wild) { + f.tb = ARPA_TABLE_INPUT; + } + + if (dev) { + if (f.tb == ARPA_TABLE_INPUT && !f.iif) + f.iif = dev; + else + if (f.tb == ARPA_TABLE_OUTPUT && !f.oif) + f.oif = dev; + else + invarg("unexpected or duplicate value for dev", dev); + } + + if (!wild) { + if (f.action < 0) { + /* Default action is allow */ + f.action = 1; + } + } + + /* Unexpected parameters for table input */ + if (!wild && f.tb == ARPA_TABLE_INPUT) { + if (f.rsrc.family) { + fprintf(stderr, "Unexpected argument for table input: src\n"); + return 1; + } + if (f.oif) { + fprintf(stderr, "Unexpected argument for table input: oif\n"); + return 1; + } + } + + /* Unexpected parameters for table output */ + if (!wild && f.tb == ARPA_TABLE_OUTPUT) { + if (f.iif) { + fprintf(stderr, "Unexpected argument for table output: iif\n"); + return 1; + } + if (f.broadcasts) { + fprintf(stderr, "Unexpected argument for table output: broadcasts\n"); + return 1; + } + if (f.unicasts) { + fprintf(stderr, "Unexpected argument for table output: unicasts\n"); + return 1; + } + } + + /* Unexpected parameters for table forward */ + if (!wild && f.tb == ARPA_TABLE_FORWARD) { + if (f.rsrc.family) { + fprintf(stderr, "Unexpected argument for table forward: src\n"); + return 1; + } + } + + if (!wild && f.broadcasts && f.unicasts) { + fprintf(stderr, "Incompatible arguments: broadcasts and unicasts\n"); + return 1; + } + + if (!wild) { + if (!(f.pref || f.mfrom.family || f.mto.family || + f.llfrom_len || f.llto_len || f.iif || f.oif || + f.broadcasts || f.unicasts)) { + fprintf(stderr, "No arguments for rule selection.\n"); + return 1; + } + } + + if (!wild) { + req.r.arpm_family = AF_INET; + req.r.arpm_table = f.tb; + req.r.arpm_pref = f.pref; + req.r.arpm_action = f.action; + + if (f.broadcasts) + req.r.arpm_flags |= ARPM_F_BROADCAST; + if (f.unicasts) + req.r.arpm_flags |= ARPM_F_UNICAST; + + if (f.iif) { + char *cp = strchr(f.iif, '+'); + + if (cp) { + *cp = 0; + req.r.arpm_flags |= ARPM_F_WILDIIF; + } + if (strlen(f.iif) > IFNAMSIZ-1) { + fprintf(stderr, "Value for iif is too long\n"); + return 1; + } + addattr_l(&req.n, sizeof(req), ARPA_IIF, + f.iif, strlen(f.iif)+1); + } + if (f.oif) { + char *cp = strchr(f.oif, '+'); + + if (cp) { + *cp = 0; + req.r.arpm_flags |= ARPM_F_WILDOIF; + } + if (strlen(f.oif) > IFNAMSIZ-1) { + fprintf(stderr, "Value for oif is too long\n"); + return 1; + } + addattr_l(&req.n, sizeof(req), ARPA_OIF, + f.oif, strlen(f.oif)+1); + } + } + + ll_init_map(&rth); + + if (rtnl_open(&rth, 0) < 0) + return 1; + + if (c & CMD_FLUSH) { + int round = 0; + char flushb[4096-512]; + + f.flushb = flushb; + f.flushp = 0; + f.flushe = sizeof(flushb); + + for (;;) { + if (rtnl_arpdump_req(&rth, af) < 0) { + perror("Cannot send dump request"); + exit(1); + } + f.flushed = 0; + if (rtnl_dump_filter(&rth, print_arprule, stdout) < 0) { + fprintf(stderr, "Flush terminated\n"); + exit(1); + } + if (f.flushed == 0) { + if (show_stats) { + if (round == 0) + printf("Nothing to flush.\n"); + else + printf("*** Flush is complete after %d round%s ***\n", round, round > 1?"s":""); + } + fflush(stdout); + return 0; + } + round++; + if (flush_update() < 0) + exit(1); + if (show_stats) { + printf("\n*** Round %d, deleting %d entries ***\n", round, f.flushed); + fflush(stdout); + } + } + } + else + if (wild) { + if (rtnl_arpdump_req(&rth, af) < 0) { + perror("Cannot send dump request"); + exit(1); + } + + new_json_obj(json); + if (rtnl_dump_filter(&rth, print_arprule, stdout) < 0) { + fprintf(stderr, "Dump terminated\n"); + exit(1); + } + delete_json_obj(); + } else { + + if (rtnl_talk(&rth, &req.n, NULL) < 0) + return 2; + } + + return 0; +} + +int do_iparprule(int argc, char **argv) +{ +strarr_t *c; + + if (argc < 1) + return iparprule_do_cmd(CMD_LIST, 0, NULL); + c = lookup_strarr(cmds, argv[0]); + if (c) { + cmd = c->code; + return iparprule_do_cmd(cmd, argc-1, argv+1); + } + if (matches(argv[0], "help") == 0) + usage(); + + fprintf(stderr, "Command \"%s\" is unknown, try \"ip arp help\".\n", *argv); + exit(-1); +} + diff -urpN iproute2-7.0.0/ip/ipmonitor.c iproute2-7.0.0-iparp/ip/ipmonitor.c --- iproute2-7.0.0/ip/ipmonitor.c 2026-04-13 22:42:58.000000000 +0300 +++ iproute2-7.0.0-iparp/ip/ipmonitor.c 2026-05-20 20:12:14.884111013 +0300 @@ -31,7 +31,7 @@ static void usage(void) fprintf(stderr, "Usage: ip monitor [ all | OBJECTS ] [ FILE ] [ label ] [ all-nsid ]\n" " [ dev DEVICE ]\n" - "OBJECTS := address | link | mroute | maddress | acaddress | neigh |\n" + "OBJECTS := address | arp | link | mroute | maddress | acaddress | neigh |\n" " netconf | nexthop | nsid | prefix | route | rule | stats\n" "FILE := file FILENAME\n"); exit(-1); @@ -63,6 +63,12 @@ static int accept_msg(struct rtnl_ctrl_d ctrl_data = ctrl; + if (n->nlmsg_type == RTM_NEWARPRULE || + n->nlmsg_type == RTM_DELARPRULE) { + print_arprule(n, arg); + return 0; + } + switch (n->nlmsg_type) { case RTM_NEWROUTE: case RTM_DELROUTE: { @@ -188,6 +194,7 @@ static int accept_msg(struct rtnl_ctrl_d #define IPMON_LNEXTHOP BIT(10) #define IPMON_LMADDR BIT(11) #define IPMON_LACADDR BIT(12) +#define IPMON_ARP BIT(13) #define IPMON_L_ALL (~0) @@ -216,6 +223,8 @@ int do_ipmonitor(int argc, char **argv) lmask |= IPMON_LMADDR; } else if (strcmp(*argv, "acaddress") == 0) { lmask |= IPMON_LACADDR; + } else if (matches(*argv, "arp") == 0) { + lmask |= IPMON_ARP; } else if (matches(*argv, "route") == 0) { lmask |= IPMON_LROUTE; } else if (matches(*argv, "mroute") == 0) { @@ -372,6 +381,14 @@ int do_ipmonitor(int argc, char **argv) } } + if (lmask & IPMON_ARP && + iparp_detect_version(nmask & IPMON_ARP) && + rtnl_add_nl_group(&rth, RTNLGRP_ARP) < 0 && + nmask & IPMON_ARP) { + fprintf(stderr, "Failed to add arp group to list\n"); + exit(1); + } + if (listen_all_nsid && rtnl_listen_all_nsid(&rth) < 0) exit(1); diff -urpN iproute2-7.0.0/lib/libnetlink.c iproute2-7.0.0-iparp/lib/libnetlink.c --- iproute2-7.0.0/lib/libnetlink.c 2026-04-13 22:42:58.000000000 +0300 +++ iproute2-7.0.0-iparp/lib/libnetlink.c 2026-05-20 20:12:14.884522140 +0300 @@ -436,6 +436,26 @@ int rtnl_neightbldump_req(struct rtnl_ha return send(rth->fd, &req, sizeof(req), 0); } +__u16 iparp_RTM_NEWARPRULE, iparp_RTM_DELARPRULE, iparp_RTM_GETARPRULE; +enum rtnetlink_groups iparp_RTNLGRP_ARP; + +int rtnl_arpdump_req(struct rtnl_handle *rth, int family) +{ + struct { + struct nlmsghdr nlh; + struct arpmsg arpm; + char buf[256]; + } req = { + .nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct arpmsg)), + .nlh.nlmsg_type = RTM_GETARPRULE, + .nlh.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST, + .nlh.nlmsg_seq = rth->dump = ++rth->seq, + .arpm.arpm_family = family, + }; + + return send(rth->fd, &req, sizeof(req), 0); +} + int rtnl_mdbdump_req(struct rtnl_handle *rth, int family) { struct {