/* * arpd.c ARP helper daemon. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. * * Authors: Alexey Kuznetsov, */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libnetlink.h" #include "utils.h" #include "rt_names.h" int resolve_hosts; DB *dbase; char *dbname = "/var/lib/arpd/arpd.db"; int ifnum; int *ifvec; char **ifnames; struct dbkey { __u32 iface; __u32 addr; }; #define IS_NEG(x) (((__u8 *)(x))[0] == 0xFF) #define NEG_TIME(x) (((x)[2]<<24)|((x)[3]<<16)|((x)[4]<<8)|(x)[5]) #define NEG_AGE(x) ((__u32)time(NULL) - NEG_TIME((__u8 *)x)) #define NEG_VALID(x) (NEG_AGE(x) < negative_timeout) #define NEG_CNT(x) (((__u8 *)(x))[1]) struct rtnl_handle rth; struct pollfd pset[2]; int udp_sock = -1; volatile int do_exit; volatile int do_sync; volatile int do_stats; struct { unsigned long arp_new; unsigned long arp_change; unsigned long app_recv; unsigned long app_success; unsigned long app_bad; unsigned long app_neg; unsigned long app_suppressed; unsigned long kern_neg; unsigned long kern_new; unsigned long kern_change; unsigned long probes_sent; unsigned long probes_suppressed; } stats; int active_probing; int negative_timeout = 60; int no_kernel_broadcasts; int broadcast_rate = 1000; int broadcast_burst = 3000; int poll_timeout = 30000; static void usage(void) { fprintf(stderr, "Usage: arpd [ -lkh? ] [ -a N ] [ -b dbase ] [ -B number ] [ -f file ] [ -n time ] [-p interval ] [ -R rate ] [ interfaces ]\n"); exit(1); } static int handle_if(int ifindex) { int i; if (ifnum == 0) return 1; for (i = 0; i < ifnum; i++) if (ifvec[i] == ifindex) return 1; return 0; } int sysctl_adjusted; static void do_sysctl_adjustments(void) { int i; if (!ifnum) return; for (i = 0; i < ifnum; i++) { char buf[128]; FILE *fp; if (active_probing) { sprintf(buf, "/proc/sys/net/ipv4/neigh/%s/mcast_solicit", ifnames[i]); if ((fp = fopen(buf, "w")) != NULL) { if (no_kernel_broadcasts) strcpy(buf, "0\n"); else sprintf(buf, "%d\n", active_probing >= 2 ? 1 : 3-active_probing); fputs(buf, fp); fclose(fp); } } sprintf(buf, "/proc/sys/net/ipv4/neigh/%s/app_solicit", ifnames[i]); if ((fp = fopen(buf, "w")) != NULL) { sprintf(buf, "%d\n", active_probing <= 1 ? 1 : active_probing); fputs(buf, fp); fclose(fp); } } sysctl_adjusted = 1; } static void undo_sysctl_adjustments(void) { int i; if (!sysctl_adjusted) return; for (i = 0; i < ifnum; i++) { char buf[128]; FILE *fp; if (active_probing) { sprintf(buf, "/proc/sys/net/ipv4/neigh/%s/mcast_solicit", ifnames[i]); if ((fp = fopen(buf, "w")) != NULL) { strcpy(buf, "3\n"); fputs(buf, fp); fclose(fp); } } sprintf(buf, "/proc/sys/net/ipv4/neigh/%s/app_solicit", ifnames[i]); if ((fp = fopen(buf, "w")) != NULL) { strcpy(buf, "0\n"); fputs(buf, fp); fclose(fp); } } sysctl_adjusted = 0; } static int send_probe(int ifindex, __u32 addr) { struct ifreq ifr = { .ifr_ifindex = ifindex }; struct sockaddr_in dst = { .sin_family = AF_INET, .sin_port = htons(1025), .sin_addr.s_addr = addr, }; socklen_t len; unsigned char buf[256]; struct arphdr *ah = (struct arphdr *)buf; unsigned char *p = (unsigned char *)(ah+1); struct sockaddr_ll sll = { .sll_family = AF_PACKET, .sll_ifindex = ifindex, .sll_protocol = htons(ETH_P_ARP), }; if (ioctl(udp_sock, SIOCGIFNAME, &ifr)) return -1; if (ioctl(udp_sock, SIOCGIFHWADDR, &ifr)) return -1; if (ifr.ifr_hwaddr.sa_family != ARPHRD_ETHER) return -1; if (setsockopt(udp_sock, SOL_SOCKET, SO_BINDTODEVICE, ifr.ifr_name, strlen(ifr.ifr_name)+1) < 0) return -1; if (connect(udp_sock, (struct sockaddr *)&dst, sizeof(dst)) < 0) return -1; len = sizeof(dst); if (getsockname(udp_sock, (struct sockaddr *)&dst, &len) < 0) return -1; ah->ar_hrd = htons(ifr.ifr_hwaddr.sa_family); ah->ar_pro = htons(ETH_P_IP); ah->ar_hln = 6; ah->ar_pln = 4; ah->ar_op = htons(ARPOP_REQUEST); memcpy(p, ifr.ifr_hwaddr.sa_data, ah->ar_hln); p += ah->ar_hln; memcpy(p, &dst.sin_addr, 4); p += 4; memset(sll.sll_addr, 0xFF, sizeof(sll.sll_addr)); memcpy(p, &sll.sll_addr, ah->ar_hln); p += ah->ar_hln; memcpy(p, &addr, 4); p += 4; if (sendto(pset[0].fd, buf, p-buf, 0, (struct sockaddr *)&sll, sizeof(sll)) < 0) return -1; stats.probes_sent++; return 0; } /* Be very tough on sending probes: 1 per second with burst of 3. */ static int queue_active_probe(int ifindex, __u32 addr) { static struct timeval prev; static int buckets; struct timeval now; gettimeofday(&now, NULL); if (prev.tv_sec) { int diff = (now.tv_sec-prev.tv_sec)*1000+(now.tv_usec-prev.tv_usec)/1000; buckets += diff; } else { buckets = broadcast_burst; } if (buckets > broadcast_burst) buckets = broadcast_burst; if (buckets >= broadcast_rate && !send_probe(ifindex, addr)) { buckets -= broadcast_rate; prev = now; return 0; } stats.probes_suppressed++; return -1; } static int respond_to_kernel(int ifindex, __u32 addr, char *lla, int llalen) { struct { struct nlmsghdr n; struct ndmsg ndm; char buf[256]; } req = { .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ndmsg)), .n.nlmsg_flags = NLM_F_REQUEST, .n.nlmsg_type = RTM_NEWNEIGH, .ndm.ndm_family = AF_INET, .ndm.ndm_state = NUD_STALE, .ndm.ndm_ifindex = ifindex, .ndm.ndm_type = RTN_UNICAST, }; addattr_l(&req.n, sizeof(req), NDA_DST, &addr, 4); addattr_l(&req.n, sizeof(req), NDA_LLADDR, lla, llalen); return rtnl_send(&rth, &req, req.n.nlmsg_len) <= 0; } static void prepare_neg_entry(__u8 *ndata, __u32 stamp) { ndata[0] = 0xFF; ndata[1] = 0; ndata[2] = stamp>>24; ndata[3] = stamp>>16; ndata[4] = stamp>>8; ndata[5] = stamp; } static int do_one_request(struct nlmsghdr *n) { struct ndmsg *ndm = NLMSG_DATA(n); int len = n->nlmsg_len; struct rtattr *tb[NDA_MAX+1]; struct dbkey key; DBT dbkey, dbdat; int do_acct = 0; if (n->nlmsg_type == NLMSG_DONE) { dbase->sync(dbase, 0); /* Now we have at least mirror of kernel db, so that * may start real resolution. */ do_sysctl_adjustments(); return 0; } if (n->nlmsg_type != RTM_GETNEIGH && n->nlmsg_type != RTM_NEWNEIGH) return 0; len -= NLMSG_LENGTH(sizeof(*ndm)); if (len < 0) return -1; if (ndm->ndm_family != AF_INET || (ifnum && !handle_if(ndm->ndm_ifindex)) || ndm->ndm_flags || ndm->ndm_type != RTN_UNICAST || !(ndm->ndm_state&~NUD_NOARP)) return 0; parse_rtattr(tb, NDA_MAX, NDA_RTA(ndm), len); if (!tb[NDA_DST]) return 0; key.iface = ndm->ndm_ifindex; memcpy(&key.addr, RTA_DATA(tb[NDA_DST]), 4); dbkey.data = &key; dbkey.size = sizeof(key); if (dbase->get(dbase, &dbkey, &dbdat, 0) != 0) { dbdat.data = 0; dbdat.size = 0; } if (n->nlmsg_type == RTM_GETNEIGH) { if (!(n->nlmsg_flags&NLM_F_REQUEST)) return 0; if (!(ndm->ndm_state&(NUD_PROBE|NUD_INCOMPLETE))) { stats.app_bad++; return 0; } if (ndm->ndm_state&NUD_PROBE) { /* If we get this, kernel still has some valid * address, but unicast probing failed and host * is either dead or changed its mac address. * Kernel is going to initiate broadcast resolution. * OK, we invalidate our information as well. */ if (dbdat.data && !IS_NEG(dbdat.data)) stats.app_neg++; dbase->del(dbase, &dbkey, 0); } else { /* If we get this kernel does not have any information. * If we have something tell this to kernel. */ stats.app_recv++; if (dbdat.data && !IS_NEG(dbdat.data)) { stats.app_success++; respond_to_kernel(key.iface, key.addr, dbdat.data, dbdat.size); return 0; } /* Sheeit! We have nothing to tell. */ /* If we have recent negative entry, be silent. */ if (dbdat.data && NEG_VALID(dbdat.data)) { if (NEG_CNT(dbdat.data) >= active_probing) { stats.app_suppressed++; return 0; } do_acct = 1; } } if (active_probing && queue_active_probe(ndm->ndm_ifindex, key.addr) == 0 && do_acct) { NEG_CNT(dbdat.data)++; dbase->put(dbase, &dbkey, &dbdat, 0); } } else if (n->nlmsg_type == RTM_NEWNEIGH) { if (n->nlmsg_flags&NLM_F_REQUEST) return 0; if (ndm->ndm_state&NUD_FAILED) { /* Kernel was not able to resolve. Host is dead. * Create negative entry if it is not present * or renew it if it is too old. */ if (!dbdat.data || !IS_NEG(dbdat.data) || !NEG_VALID(dbdat.data)) { __u8 ndata[6]; stats.kern_neg++; prepare_neg_entry(ndata, time(NULL)); dbdat.data = ndata; dbdat.size = sizeof(ndata); dbase->put(dbase, &dbkey, &dbdat, 0); } } else if (tb[NDA_LLADDR]) { if (dbdat.data && !IS_NEG(dbdat.data)) { if (memcmp(RTA_DATA(tb[NDA_LLADDR]), dbdat.data, dbdat.size) == 0) return 0; stats.kern_change++; } else { stats.kern_new++; } dbdat.data = RTA_DATA(tb[NDA_LLADDR]); dbdat.size = RTA_PAYLOAD(tb[NDA_LLADDR]); dbase->put(dbase, &dbkey, &dbdat, 0); } } return 0; } static void load_initial_table(void) { if (rtnl_wilddump_request(&rth, AF_INET, RTM_GETNEIGH) < 0) { perror("dump request failed"); exit(1); } } static void get_kern_msg(void) { int status; struct nlmsghdr *h; struct sockaddr_nl nladdr = {}; struct iovec iov; char buf[8192]; struct msghdr msg = { (void *)&nladdr, sizeof(nladdr), &iov, 1, NULL, 0, 0 }; iov.iov_base = buf; iov.iov_len = sizeof(buf); status = recvmsg(rth.fd, &msg, MSG_DONTWAIT); if (status <= 0) return; if (msg.msg_namelen != sizeof(nladdr)) return; if (nladdr.nl_pid) return; for (h = (struct nlmsghdr *)buf; status >= sizeof(*h); ) { int len = h->nlmsg_len; int l = len - sizeof(*h); if (l < 0 || len > status) return; if (do_one_request(h) < 0) return; status -= NLMSG_ALIGN(len); h = (struct nlmsghdr *)((char *)h + NLMSG_ALIGN(len)); } } /* Receive gratuitous ARP messages and store them, that's all. */ static void get_arp_pkt(void) { unsigned char buf[1024]; struct sockaddr_ll sll; socklen_t sll_len = sizeof(sll); struct arphdr *a = (struct arphdr *)buf; struct dbkey key; DBT dbkey, dbdat; int n; n = recvfrom(pset[0].fd, buf, sizeof(buf), MSG_DONTWAIT, (struct sockaddr *)&sll, &sll_len); if (n < 0) { if (errno != EINTR && errno != EAGAIN) syslog(LOG_ERR, "recvfrom: %m"); return; } if (ifnum && !handle_if(sll.sll_ifindex)) return; /* Sanity checks */ if (n < sizeof(*a) || (a->ar_op != htons(ARPOP_REQUEST) && a->ar_op != htons(ARPOP_REPLY)) || a->ar_pln != 4 || a->ar_pro != htons(ETH_P_IP) || a->ar_hln != sll.sll_halen || sizeof(*a) + 2*4 + 2*a->ar_hln > n) return; key.iface = sll.sll_ifindex; memcpy(&key.addr, (char *)(a+1) + a->ar_hln, 4); /* DAD message, ignore. */ if (key.addr == 0) return; dbkey.data = &key; dbkey.size = sizeof(key); if (dbase->get(dbase, &dbkey, &dbdat, 0) == 0 && !IS_NEG(dbdat.data)) { if (memcmp(dbdat.data, a+1, dbdat.size) == 0) return; stats.arp_change++; } else { stats.arp_new++; } dbdat.data = a+1; dbdat.size = a->ar_hln; dbase->put(dbase, &dbkey, &dbdat, 0); } static void catch_signal(int sig, void (*handler)(int)) { struct sigaction sa = { .sa_handler = handler }; #ifdef SA_INTERRUPT sa.sa_flags = SA_INTERRUPT; #endif sigaction(sig, &sa, NULL); } #include sigjmp_buf env; volatile int in_poll; static void sig_exit(int signo) { do_exit = 1; if (in_poll) siglongjmp(env, 1); } static void sig_sync(int signo) { do_sync = 1; if (in_poll) siglongjmp(env, 1); } static void sig_stats(int signo) { do_sync = 1; do_stats = 1; if (in_poll) siglongjmp(env, 1); } static void send_stats(void) { syslog(LOG_INFO, "arp_rcv: n%lu c%lu app_rcv: tot %lu hits %lu bad %lu neg %lu sup %lu", stats.arp_new, stats.arp_change, stats.app_recv, stats.app_success, stats.app_bad, stats.app_neg, stats.app_suppressed ); syslog(LOG_INFO, "kern: n%lu c%lu neg %lu arp_send: %lu rlim %lu", stats.kern_new, stats.kern_change, stats.kern_neg, stats.probes_sent, stats.probes_suppressed ); do_stats = 0; } int main(int argc, char **argv) { int opt; int do_list = 0; char *do_load = NULL; while ((opt = getopt(argc, argv, "h?b:lf:a:n:p:kR:B:")) != EOF) { switch (opt) { case 'b': dbname = optarg; break; case 'f': if (do_load) { fprintf(stderr, "Duplicate option -f\n"); usage(); } do_load = optarg; break; case 'l': do_list = 1; break; case 'a': active_probing = atoi(optarg); break; case 'n': negative_timeout = atoi(optarg); break; case 'k': no_kernel_broadcasts = 1; break; case 'p': if ((poll_timeout = 1000 * strtod(optarg, NULL)) < 100) { fprintf(stderr, "Invalid poll timeout\n"); exit(-1); } break; case 'R': if ((broadcast_rate = atoi(optarg)) <= 0 || (broadcast_rate = 1000/broadcast_rate) <= 0) { fprintf(stderr, "Invalid ARP rate\n"); exit(-1); } break; case 'B': if ((broadcast_burst = atoi(optarg)) <= 0 || (broadcast_burst = 1000*broadcast_burst) <= 0) { fprintf(stderr, "Invalid ARP burst\n"); exit(-1); } break; case 'h': case '?': default: usage(); } } argc -= optind; argv += optind; if (argc > 0) { ifnum = argc; ifnames = argv; ifvec = malloc(argc*sizeof(int)); if (!ifvec) { perror("malloc"); exit(-1); } } if ((udp_sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket"); exit(-1); } if (ifnum) { int i; struct ifreq ifr = {}; for (i = 0; i < ifnum; i++) { strncpy(ifr.ifr_name, ifnames[i], IFNAMSIZ); if (ioctl(udp_sock, SIOCGIFINDEX, &ifr)) { perror("ioctl(SIOCGIFINDEX)"); exit(-1); } ifvec[i] = ifr.ifr_ifindex; } } dbase = dbopen(dbname, O_CREAT|O_RDWR, 0644, DB_HASH, NULL); if (dbase == NULL) { perror("db_open"); exit(-1); } if (do_load) { char buf[128]; FILE *fp; struct dbkey k; DBT dbkey, dbdat; dbkey.data = &k; dbkey.size = sizeof(k); if (strcmp(do_load, "-") == 0 || strcmp(do_load, "--") == 0) { fp = stdin; } else if ((fp = fopen(do_load, "r")) == NULL) { perror("fopen"); goto do_abort; } buf[sizeof(buf)-1] = 0; while (fgets(buf, sizeof(buf), fp)) { __u8 b1[6]; char ipbuf[128]; char macbuf[128]; if (buf[0] == '#') continue; if (sscanf(buf, "%u%s%s", &k.iface, ipbuf, macbuf) != 3) { fprintf(stderr, "Wrong format of input file \"%s\"\n", do_load); goto do_abort; } if (strncmp(macbuf, "FAILED:", 7) == 0) continue; if (!inet_aton(ipbuf, (struct in_addr *)&k.addr)) { fprintf(stderr, "Invalid IP address: \"%s\"\n", ipbuf); goto do_abort; } if (ll_addr_a2n((char *) b1, 6, macbuf) != 6) goto do_abort; dbdat.size = 6; if (dbase->put(dbase, &dbkey, &dbdat, 0)) { perror("hash->put"); goto do_abort; } } dbase->sync(dbase, 0); if (fp != stdin) fclose(fp); } if (do_list) { DBT dbkey, dbdat; printf("%-8s %-15s %s\n", "#Ifindex", "IP", "MAC"); while (dbase->seq(dbase, &dbkey, &dbdat, R_NEXT) == 0) { struct dbkey *key = dbkey.data; if (handle_if(key->iface)) { if (!IS_NEG(dbdat.data)) { char b1[18]; printf("%-8d %-15s %s\n", key->iface, inet_ntoa(*(struct in_addr *)&key->addr), ll_addr_n2a(dbdat.data, 6, ARPHRD_ETHER, b1, 18)); } else { printf("%-8d %-15s FAILED: %dsec ago\n", key->iface, inet_ntoa(*(struct in_addr *)&key->addr), NEG_AGE(dbdat.data)); } } } } if (do_load || do_list) goto out; pset[0].fd = socket(PF_PACKET, SOCK_DGRAM, 0); if (pset[0].fd < 0) { perror("socket"); exit(-1); } if (1) { struct sockaddr_ll sll = { .sll_family = AF_PACKET, .sll_protocol = htons(ETH_P_ARP), .sll_ifindex = (ifnum == 1 ? ifvec[0] : 0), }; if (bind(pset[0].fd, (struct sockaddr *)&sll, sizeof(sll)) < 0) { perror("bind"); goto do_abort; } } if (rtnl_open(&rth, RTMGRP_NEIGH) < 0) { perror("rtnl_open"); goto do_abort; } pset[1].fd = rth.fd; load_initial_table(); if (daemon(0, 0)) { perror("arpd: daemon"); goto do_abort; } openlog("arpd", LOG_PID | LOG_CONS, LOG_DAEMON); catch_signal(SIGINT, sig_exit); catch_signal(SIGTERM, sig_exit); catch_signal(SIGHUP, sig_sync); catch_signal(SIGUSR1, sig_stats); #define EVENTS (POLLIN|POLLPRI|POLLERR|POLLHUP) pset[0].events = EVENTS; pset[0].revents = 0; pset[1].events = EVENTS; pset[1].revents = 0; sigsetjmp(env, 1); for (;;) { in_poll = 1; if (do_exit) break; if (do_sync) { in_poll = 0; dbase->sync(dbase, 0); do_sync = 0; in_poll = 1; } if (do_stats) send_stats(); if (poll(pset, 2, poll_timeout) > 0) { in_poll = 0; if (pset[0].revents&EVENTS) get_arp_pkt(); if (pset[1].revents&EVENTS) get_kern_msg(); } else { do_sync = 1; } } undo_sysctl_adjustments(); out: dbase->close(dbase); exit(0); do_abort: dbase->close(dbase); exit(-1); }