embeddable zeroconf/ip code
David Brownell <[email protected]> Mon, 18 Oct 2004 12:16:14 -0700
| Newsgroups | gmane.network.zeroconf.workers |
|---|---|
| Message-ID | <[email protected]> |
--Boundary-00=_+ZBdBnMUOjzvtaY Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Content-Disposition: inline Hi, I recently found myself hunting for a simpler version of what zcip-0.4 does, and I found one that just uses a packet socket -- no library frameworks necessary, just simple libc code! (From the zeroconf.org site.) So I whacked at it a bit, and the results are attached. It should be a pretty close match to draft-ietf-zeroconf-ipv4-linklocal-17.txt and when linked to BusyBox on ARM it takes less than 3KBytes (smaller than "ping"). I tested it a bit on x86 Linux, it seems to behave in my non-zeroconf network. Under light load and all that. I'm posting this here, since I wasn't the only one who wanted lighter weight code for this! I'd appreciate feedback best in the form of patches, though of course bug reports are worth something too ... we do want to abolish protocol bugs as quickly as possible. Shell commands of interest: cc -DDEBUG -Wall -Os zcip.c -o zcip zcip -qvf -r 169.254.1.101 eth0 zcip.script zcip eth0 zcip.script Have fun! - Dave p.s. I'm no longer on either of the ZCIP related lists I've sent this to, please keep that in mind when following up! --Boundary-00=_+ZBdBnMUOjzvtaY Content-Type: text/x-csrc; charset="us-ascii"; name="zcip.c" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="zcip.c" /* * ZeroConf IPv4 Link-Local addressing (see <http://www.zeroconf.org/>) * * Copyright (C) 2003 by Arthur van Hoff ([email protected]) * Copyright (C) 2004 by David Brownell * * 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. * * 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. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA */ /* * This is a user-mode implementation that's a pretty close match to * draft-ietf-zeroconf-ipv4-linklocal-17.txt (the July 2004 version). * It can work by itself or (adding -DIN_BUSYBOX) from BusyBox 1.0. * * ZCIP just manages the 169.254.*.* addresses. That network is not * routed at the IP level, though various proxies or bridges can * certainly be used. Its naming is built over multicast DNS. */ // #define DEBUG // TODO: // - daemon needs more testing, robustness under load // - manageability: calling scripts can easily fail // - implement some syslog option '-s' // - link status monitoring #include <errno.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <time.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/poll.h> #include <sys/wait.h> #include <sys/time.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/ether.h> #include <net/ethernet.h> #include <net/if.h> #include <net/if_arp.h> #include <linux/if_packet.h> #include <linux/sockios.h> struct arp_packet { struct ether_header hdr; struct arphdr arp; struct ether_addr source_addr; struct in_addr source_ip; struct ether_addr target_addr; struct in_addr target_ip; unsigned char pad[18]; } __attribute__ ((__packed__)); /* 169.254.0.0 */ static uint32_t LINKLOCAL_ADDR = 0xa9fe0000; static uint32_t LINKLOCAL_MASK = 0xFFFF0000; /* protocol timeout parameters, specified in seconds */ static const unsigned PROBE_WAIT = 1; static const unsigned PROBE_MIN = 1; static const unsigned PROBE_MAX = 2; static const unsigned PROBE_NUM = 3; static const unsigned MAX_COLLISIONS = 10; static const unsigned RATE_LIMIT_INTERVAL = 60; static const unsigned ANNOUNCE_WAIT = 2; static const unsigned ANNOUNCE_NUM = 2; static const unsigned ANNOUNCE_INTERVAL = 2; static const unsigned DEFEND_INTERVAL = 10; static const unsigned char ZCIP_VERSION[] = "18 October 2004"; static char *prog; static struct in_addr null_ip = { 0 }; static struct ether_addr null_addr = { {0, 0, 0, 0, 0, 0} }; static int verbose = 0; #ifdef DEBUG #define DBG(fmt,args...) \ fprintf(stderr, "%s: " fmt , prog , ## args) #define VDBG(fmt,args...) do { \ if (verbose) fprintf(stderr, "%s: " fmt , prog ,## args); \ } while (0) /** * Convert an ethernet address to a printable string. */ static char * ether2str(const struct ether_addr *addr) { static char str[32]; snprintf(str, sizeof (str), "%02x:%02x:%02x:%02x:%02x:%02x", addr->ether_addr_octet[0], addr->ether_addr_octet[1], addr->ether_addr_octet[2], addr->ether_addr_octet[3], addr->ether_addr_octet[4], addr->ether_addr_octet[5]); return str; } #else #define DBG(fmt,args...) \ do { } while (0) #define VDBG DBG #endif /* DEBUG */ /** * Pick a random link local IP address on 169.254/16, except that * the first and last 256 addresses are reserved. */ static void pick(struct in_addr *ip) { ip->s_addr = htonl(LINKLOCAL_ADDR | ((abs(random()) % 0xFD00) + 0x0100)); } /** * Broadcast an ARP packet. */ static int arp(int fd, struct sockaddr *saddr, int op, struct ether_addr *source_addr, struct in_addr source_ip, struct ether_addr *target_addr, struct in_addr target_ip) { struct arp_packet p; memset(&p, 0, sizeof (p)); // ether header p.hdr.ether_type = htons(ETHERTYPE_ARP); memcpy(p.hdr.ether_shost, source_addr, ETH_ALEN); memset(p.hdr.ether_dhost, 0xff, ETH_ALEN); // arp request p.arp.ar_hrd = htons(ARPHRD_ETHER); p.arp.ar_pro = htons(ETHERTYPE_IP); p.arp.ar_hln = ETH_ALEN; p.arp.ar_pln = 4; p.arp.ar_op = htons(op); memcpy(&p.source_addr, source_addr, ETH_ALEN); memcpy(&p.source_ip, &source_ip, sizeof (p.source_ip)); memcpy(&p.target_addr, target_addr, ETH_ALEN); memcpy(&p.target_ip, &target_ip, sizeof (p.target_ip)); // send it if (sendto(fd, &p, sizeof (p), 0, saddr, sizeof (*saddr)) < 0) { perror("sendto"); return -errno; } return 0; } /** * Run a script. */ int run(char *script, char *arg, char *intf, struct in_addr *ip) { int pid, status; char *why; if (script != NULL) { VDBG("%s run %s %s\n", intf, script, arg); #ifdef __uClinux__ #error need to use vfork/execle #else pid = fork(); if (pid < 0) { why = "fork"; goto bad; } if (pid == 0) { // child process setenv("interface", intf, 1); if (ip != NULL) { setenv("ip", inet_ntoa(*ip), 1); // FIXME syslog these calls } execl(script, script, arg, NULL); why = "execl"; goto bad; } #endif if (waitpid(pid, &status, 0) <= 0) { why = "waitpid"; goto bad; } if (WEXITSTATUS(status) != 0) { fprintf(stderr, "%s: script %s failed, exit=%d\n", prog, script, WEXITSTATUS(status)); return -errno; } } return 0; bad: // FIXME syslog errors here perror(why); return -errno; } #ifdef IN_BUSYBOX #include "busybox.h" #define main zcip_main #endif /** * Print usage information. */ static void __attribute__ ((noreturn)) usage(const char *msg) { fprintf(stderr, "%s: %s\n", prog, msg); #ifndef IN_BUSYBOX fprintf(stderr, "Usage: %s [OPTIONS] ifname script\n" "\t-f foreground mode\n" "\t-q quit after address (no daemon)\n" "\t-r 169.254.x.x request this address first\n" "\t-v verbose; show version\n", prog); exit(0); #else bb_show_usage(); #endif } /** * Return milliseconds of random delay, up to "secs" seconds. */ static inline unsigned ms_rdelay(unsigned secs) { return random() % (secs * 1000); } /** * main program */ int main(int argc, char *argv[]) { char *intf = NULL; char *script = NULL; char *why; struct sockaddr saddr; struct arp_packet p; struct ifreq ifr; struct ether_addr addr; struct in_addr ip = { 0 }; int fd; int quit = 0; int ready = 0; int foreground = 0; suseconds_t timeout = 0; // milliseconds time_t defend = 0; int collisions = 0; int nprobes = 0; int nclaims = 0; int t; // parse commandline: prog [options] ifname script prog = argv[0]; while ((t = getopt(argc, argv, "fqr:v")) != EOF) { switch (t) { case 'f': foreground = 1; continue; case 'q': quit = 1; continue; case 'r': if (inet_aton(optarg, &ip) == 0 || (ntohl(ip.s_addr) & LINKLOCAL_MASK) != LINKLOCAL_ADDR) { usage("invalid link address"); } continue; case 'v': if (!verbose) printf("%s: version %s\n", prog, ZCIP_VERSION); verbose++; continue; default: usage("bad option"); } } if (optind < argc - 1) { intf = argv[optind++]; script = argv[optind++]; } if (optind != argc || !intf) usage("wrong number of arguments"); // initialize the interface (modprobe, ifup, etc) if (run(script, "init", intf, NULL) < 0) return EXIT_FAILURE; // initialize saddr memset(&saddr, 0, sizeof (saddr)); strncpy(saddr.sa_data, intf, sizeof (saddr.sa_data)); // open an ARP socket if ((fd = socket(PF_PACKET, SOCK_PACKET, htons(ETH_P_ARP))) < 0) { why = "open"; goto bad; } // bind to the interface's ARP socket if (bind(fd, &saddr, sizeof (saddr)) < 0) { why = "bind"; goto bad; } // get the interface's ethernet address memset(&ifr, 0, sizeof (ifr)); strncpy(ifr.ifr_name, intf, sizeof (ifr.ifr_name)); if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) { why = "get ethernet address"; goto bad; } memcpy(&addr, &ifr.ifr_hwaddr.sa_data, ETHER_ADDR_LEN); // start with some stable ip address, either a function of the // hardware address or the last address we used. retry intervals // and new addresses should be less predictable. t = (addr.ether_addr_octet[ETHER_ADDR_LEN - 4] << 24) | (addr.ether_addr_octet[ETHER_ADDR_LEN - 3] << 16) | (addr.ether_addr_octet[ETHER_ADDR_LEN - 2] << 8) | (addr.ether_addr_octet[ETHER_ADDR_LEN - 1] << 0); if (ip.s_addr == 0) { srandom(t); pick(&ip); } srandom(t + time(0)); // daemonize now; don't delay system startup if (!foreground) { if (daemon(0, verbose) < 0) { why = "daemon"; goto bad; } } // run the dynamic address negotiation protocol, // restarting after address collisions: // - start with some address we want to try // - short random delay // - arp probes to see if another host else uses it // - arp announcements that we're claiming it // - use it // - defend it, within limits while (1) { struct pollfd fds[1]; struct timeval tv1; fds[0].fd = fd; fds[0].events = POLLIN | POLLERR; fds[0].revents = 0; // poll, being ready to adjust current timeout if (timeout > 0) { gettimeofday(&tv1, NULL); tv1.tv_usec += (timeout % 1000) * 1000; if (tv1.tv_usec > 1000000) { tv1.tv_usec -= 1000000; tv1.tv_sec++; } tv1.tv_sec += timeout / 1000; } else if (timeout == 0) { timeout = ms_rdelay(PROBE_WAIT); } VDBG("...wait %ld %s nprobes=%d, nclaims=%d\n", timeout, intf, nprobes, nclaims); switch (poll(fds, 1, timeout)) { // timeouts trigger protocol transitions case 0: // probes if (nprobes < PROBE_NUM) { nprobes++; VDBG("probe/%d %s@%s\n", nprobes, intf, inet_ntoa(ip)); (void)arp(fd, &saddr, ARPOP_REQUEST, &addr, null_ip, &null_addr, ip); if (nprobes < PROBE_NUM) { timeout = PROBE_MIN * 1000; timeout += ms_rdelay(PROBE_MAX - PROBE_MIN); } else timeout = ANNOUNCE_WAIT * 1000; } // then announcements else if (nclaims < ANNOUNCE_NUM) { nclaims++; VDBG("announce/%d %s@%s\n", nclaims, intf, inet_ntoa(ip)); (void)arp(fd, &saddr, ARPOP_REQUEST, &addr, ip, &addr, ip); if (nclaims < ANNOUNCE_NUM) { timeout = ANNOUNCE_INTERVAL * 1000; } else { // link can be used a bit earlier run(script, "config", intf, &ip); ready = 1; collisions = 0; timeout = -1; // NOTE: all other exit paths // should deconfig ... if (quit) return EXIT_SUCCESS; } } break; // packets arriving case 1: // maybe adjust timeout if (timeout > 0) { struct timeval tv2; gettimeofday(&tv2, NULL); if (timercmp(&tv1, &tv2, <)) { timeout = -1; } else { timersub(&tv1, &tv2, &tv1); timeout = 1000 * tv1.tv_sec + tv1.tv_usec / 1000; } } if ((fds[0].revents & POLLIN) == 0) { if (fds[0].revents & POLLERR) { // FIXME: links routinely go down; // this shouldn't necessarily exit. fprintf(stderr, "%s %s: poll error\n", prog, intf); if (ready) { run(script, "deconfig", intf, &ip); } return EXIT_FAILURE; } continue; } // read ARP packet if (recv(fd, &p, sizeof (p), 0) < 0) { // FIXME stderr may be bad here (syslog) why = "recv"; goto bad; } if ((ntohs(p.hdr.ether_type) != ETHERTYPE_ARP)) continue; VDBG("%s recv arp type=%d, op=%d,\n", intf, ntohs(p.hdr.ether_type), ntohs(p.arp.ar_op)); VDBG("\tsource=%s %s\n", ether2str(&p.source_addr), inet_ntoa(p.source_ip)); VDBG("\ttarget=%s %s\n", ether2str(&p.target_addr), inet_ntoa(p.target_ip)); if (p.arp.ar_op != htons(ARPOP_REQUEST) && p.arp.ar_op != htons(ARPOP_REPLY)) continue; // some cases are always collisions if ((p.source_ip.s_addr == ip.s_addr) && (memcmp(&addr, &p.source_addr, ETH_ALEN) != 0)) { collision: VDBG("%s ARP conflict from %s\n", intf, ether2str(&p.source_addr)); if (ready) { time_t now = time(0); if ((defend + DEFEND_INTERVAL) < now) { defend = now; (void)arp(fd, &saddr, ARPOP_REQUEST, &addr, ip, &addr, ip); VDBG("%s defend\n", intf); timeout = -1; continue; } defend = now; ready = 0; run(script, "deconfig", intf, &ip); } collisions++; if (collisions >= MAX_COLLISIONS) { VDBG("%s ratelimit\n", intf); sleep(RATE_LIMIT_INTERVAL); } // restart the whole protocol pick(&ip); timeout = 0; nprobes = 0; nclaims = 0; } // two hosts probing one address is a collision too else if (p.target_ip.s_addr == ip.s_addr && nclaims == 0 && ntohs(p.arp.ar_op) == ARPOP_REQUEST && memcmp(&addr, &p.target_addr, ETH_ALEN) != 0) { goto collision; } break; default: // FIXME stderr may be bad here (syslog) why = "poll"; goto bad; } } bad: perror(why); return EXIT_FAILURE; } --Boundary-00=_+ZBdBnMUOjzvtaY Content-Type: application/x-shellscript; name="zcip.script" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="zcip.script" #!/bin/sh # only for use as a "zcip" callback script if [ "x$interface" = x ] then exit 1 fi # zcip should start on boot/resume and various media changes case "$1" in init) # for now, zcip requires the link to be already up, # and it drops links when they go down. that may # not be the most robust model. exit 0 ;; config) if [ x$ip = x ] then exit 1 fi # FIXME remember $ip for $interface, to use on restart exec ip address add dev $interface \ scope link local $ip/16 broadcast + ;; deconfig) if [ x$ip = x ] then exit 1 fi exec ip address del dev $interface local $ip ;; esac exit 1 --Boundary-00=_+ZBdBnMUOjzvtaY Content-Type: application/x-shellscript; name="zcip.sh" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="zcip.sh" #!/bin/sh PATH=$PATH ZCIP=/sbin/zcip IFNAME=eth0 SCRIPT=/etc/zcip.script # SHOULD use "-r 169.254.x.x" to reclaim the last value # it should have been saved away somewhere by $SCRIPT FLAGS="" case "$1" in start|restart) ip link set $IFNAME up exec $ZCIP $FLAGS $IFNAME $SCRIPT ;; stop) # FIXME should probably save and use daemon's PID ;; status) ;; *) echo "Usage: $0 {start|stop|status|restart}" exit 1 ;; esac --Boundary-00=_+ZBdBnMUOjzvtaY-- ------------------------------------------------------- This SF.net email is sponsored by: IT Product Guide on ITManagersJournal Use IT products in your business? Tell us what you think of them. Give us Your Opinions, Get Free ThinkGeek Gift Certificates! Click to find out more http://productguide.itmanagersjournal.com/guidepromo.tmpl