Date: Mon, 24 Aug 2009 09:25:13 +0300 From: Ilkka Virta Subject: Some dhcp_probe changes/enhancements (mostly for Linux) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="4Ckj6UjgE2iN1+kY" Content-Disposition: inline --4Ckj6UjgE2iN1+kY Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Hello chaps, I was looking for a program to find DHCP servers on a network, and found your dhcp_probe, which seems to do what is required. And basically it worked on Linux too, except for some issues, which I tried to fix (see below). I only had Linux machines at hand so I couldn't check if any changes work on other platforms as well. First, on default settings, pcap_dispatch sleeps indefinitely while waiting for a packet (as is described in the source code comments). Using -T makes the timeout work, but the call still can't be interrupted with SIGINT or SIGTERM. Internally, it seems that pcap_dispatch wraps the recvfrom() call in a loop while waiting for something, and one should call pcap_breakloop() on the pcap handle to get out of it. (I looked at libpcap-0.9.8) Also, pcap_dispatch doesn't necessarily wait for the whole time, since on Linux the recvfrom() call returns after one packet (according to comments in the pcap sources, it never returns more than one packet on each call). To fix these, I wrapped the call to pcap_dispatch inside a loop, and used alarm() to break out of it. The signal is set without SA_RESTART so the system call should get interrupted as needed. If that happens, this should (as far as I can guess) work on the other Unixes too. Second, while I was at it, I added an option to make the program change its UID after creating the necessary structures, so as not to run as root while reading data from the network. As a prelude to this, I added an option to keep the pcap handle open all the time, since opening it requires superuser powers. I get a slightly itchy feeling about processes running (unnecessarily) as root, so this makes me feel better. I think the downside of keeping the packet capture open all the time should be minor enough at least in non-promiscuous mode. Patches attached: dhcp-probe-01-pcap-loop.patch - wrap pcap_dispatch in a loop dhcp-probe-02-keep-pcap.patch - add option to keep pcap open all the time dhcp-probe-03-drop-privs.patch - add option to change uid after setup Feel free to use/commit/distribute/anything any or all of these. -- Ilkka Virta --4Ckj6UjgE2iN1+kY Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="dhcp-probe-01-pcap-loop.patch" --- dhcp-probe/src/dhcp_probe.c.orig 2009-08-16 12:24:10.000000000 +0300 +++ dhcp-probe/src/dhcp_probe.c 2009-08-16 11:52:26.000000000 +0300 @@ -59,6 +59,7 @@ volatile sig_atomic_t reopen_log_file; /* for signal handler */ volatile sig_atomic_t reopen_capture_file; /* for signal handler */ volatile sig_atomic_t quit_requested; /* for signal requested */ +volatile sig_atomic_t alarm_fired; /* for signal requested */ pcap_t *pd = NULL; /* libpcap - packet capture descriptor used for actual packet capture */ pcap_t *pd_template = NULL; /* libpcap - packet capture descriptor just used as template */ @@ -74,6 +75,27 @@ int use_8021q = 0; int vlan_id = 0; +/* capture packets from pcap for timeout seconds */ +int +loop_for_packets(int timeout) +{ + int packets_recv = 0; + + alarm_fired = 0; + alarm(timeout); + + do { + int pcap_rc = pcap_dispatch(pd, -1, process_response, NULL); + if (pcap_rc == -1) + report(LOG_ERR, "pcap_dispatch(): %s", pcap_geterr(pd)); + else if (pcap_rc > 0) + packets_recv += pcap_rc; + } while(! alarm_fired && !quit_requested); + + return packets_recv; +} + + int main(int argc, char **argv) { @@ -84,7 +106,6 @@ struct sigaction sa; FILE *pid_fp; char *cwd = CWD; - int i; int write_packet_len; int bytes_written; @@ -98,9 +119,6 @@ int linktype; char pcap_errbuf[PCAP_ERRBUF_SIZE], pcap_errbuf2[PCAP_ERRBUF_SIZE]; - /* for libnet */ - char libnet_errbuf[LIBNET_ERRBUF_SIZE]; - /* get progname = last component of argv[0] */ prog = strrchr(argv[0], '/'); if (prog) @@ -265,6 +283,8 @@ reread_config_file = 0; /* set by signal handler */ reopen_log_file = 0; /* set by signal handler */ reopen_capture_file = 0; /* set by signal handler */ + quit_requested = 0; + alarm_fired = 0; ifname = strdup(argv[optind]); /* interface name is a required final argument */ @@ -332,6 +352,13 @@ report(LOG_ERR, "sigaction: %s", get_errmsg()); my_exit(1, 1, 1); } + sigemptyset(&sa.sa_mask); + sa.sa_handler = catcher; + sa.sa_flags = 0; + if (sigaction(SIGALRM, &sa, NULL) < 0) { + report(LOG_ERR, "sigaction: %s", get_errmsg()); + my_exit(1, 1, 1); + } @@ -479,8 +506,9 @@ for (l = libnet_cq_head(); libnet_cq_last(); l = libnet_cq_next()) { /* write one flavor packet and listen for answers */ - int pcap_rc; + int packets_recv; int pcap_open_retries; + /* We set up for packet capture BEFORE writing our packet, to minimize the delay between our writing and when we are able to start capturing. (I cannot tell from @@ -569,33 +597,16 @@ report(LOG_DEBUG, "listening for answers for %d milliseconds", GetResponse_wait_time()); - /* XXX I often find that pcap_dispatch() returns well before the timeout specified earlier. - I ensure that there's no alarm() still left over before we start, and also ensure we don't - get interrupted by SIGCHLD (possible since process_response() could fork an alert_program or alert_program2 child). - But we STILL often return from pcap_dispatch() too soon! - April 2001: An update to the pcap(3) man page around version 0.6 (?), along with postings - on the tcpdump workers mailing list explains what's going on. The timeout specified in - pcap_open_live() isn't a timeout in the sense one might expect. The pcap_dispatch() call - can return sooner than expected (even immediately), or if no packets are received, might - never return at all; the behavior is platform-dependant. I don't have a way to work - around this issue; it means this program just won't work reliably (or at all) on some - platforms. - */ - - alarm(0); /* just in case a previous alarm was still left */ - sigemptyset(&new_sigset); sigaddset(&new_sigset, SIGCHLD); sigprocmask(SIG_BLOCK, &new_sigset, &old_sigset); /* block SIGCHLD */ - pcap_rc = pcap_dispatch(pd, -1, process_response, NULL); + packets_recv = loop_for_packets(GetResponse_wait_time() / 1000);; sigprocmask(SIG_SETMASK, &old_sigset, NULL); /* unblock SIGCHLD */ - if (pcap_rc < 0) - report(LOG_ERR, "pcap_dispatch(): %s", pcap_geterr(pd)); - else if (debug > 10) - report(LOG_DEBUG, "done listening, captured %d packets", pcap_rc); + if (debug > 10) + report(LOG_DEBUG, "done listening, captured %d packets", packets_recv); /* I was hoping that perhaps pcap_stats() would return a nonzero number of packets dropped when the buffer size specified to pcap_open_live() turns out to be too small -- so we could @@ -688,6 +699,7 @@ pcap_close(pd_template); my_exit(0, 1, 1); + return 0; /* make gcc happy */ } @@ -986,9 +998,6 @@ /* Perform all necessary functions to handle a request to reconfigure. Must not be called until after initial configuration is complete. */ - - int i; - if (! read_configfile(config_file)) { my_exit(1, 1, 1); } @@ -1050,8 +1059,14 @@ if ((sig == SIGINT) || (sig == SIGTERM) || (sig == SIGQUIT)) { /* quit gracefully */ quit_requested = 1; + /* pcap wraps the socket read inside a loop, so the signal doesn't + interrupt it without an explicit call to pcap_breakloop */ + pcap_breakloop(pd); + return; + } else if (sig == SIGALRM) { /* timer */ + pcap_breakloop(pd); + alarm_fired = 1; return; - } else if (sig == SIGHUP) { /* re-read config file */ /* Doing the reread while in the signal handler is way too dangerous. We'll do it at the start or end of the next main event loop. --4Ckj6UjgE2iN1+kY Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="dhcp-probe-02-keep-pcap.patch" --- dhcp-probe/src/dhcp_probe.c.01 2009-08-16 11:52:26.000000000 +0300 +++ dhcp-probe/src/dhcp_probe.c 2009-08-16 12:31:22.000000000 +0300 @@ -49,6 +49,7 @@ */ int snaplen = CAPTURE_BUFSIZE; int socket_receive_timeout_feature = 0; +int keep_pcap = 0; char *prog = NULL; char *logfile_name = NULL; @@ -75,6 +76,89 @@ int use_8021q = 0; int vlan_id = 0; +int need_promiscuous(void) +{ + /* If we're going to claim a chaddr different than my_eaddr, some of the responses + may come back to chaddr (as opposed to my_eaddr or broadcast), so we'll need to + listen promiscuously. + If we're going to claim an ether_src different than my_eaddr, in theory that should + make no difference; bootp/dhcp servers should rely on chaddr, not ether_src. Still, + it's possible there's a server out there that does it wrong, and might therefore mistakenly + send responses to ether_src. So lets also listen promiscuously if ether_src != my_eaddr. + */ + int promiscuous = 0; + if (bcmp(GetChaddr(), &my_eaddr, sizeof(struct ether_addr)) || + bcmp(GetEther_src(), &my_eaddr, sizeof(struct ether_addr))) + promiscuous = 1; + return promiscuous; +} + +int init_pcap(int promiscuous, bpf_u_int32 netmask) +{ + /* open packet capture descriptor */ + /* XXX On Solaris 7, sometimes pcap_open_live() fails with a message like: + pcap_open_live qfe0: recv_ack: info unexpected primitive ack 0x8 + It's not clear what causes this, or what the 0x8 code indicates. + The error appears to be transient; retrying sometimes will work, so I've wrapped the call in a retry loop. + I've also added a delay after each failure; perhaps the failure has something to do with the fact that + we call pcap_open_live() so soon after pcap_close() (for the second and succeeding packets in each cycle); + adding a delay might help in that case. + */ + struct bpf_program bpf_code; + char pcap_errbuf[PCAP_ERRBUF_SIZE]; + int linktype; + int pcap_open_retries = PCAP_OPEN_LIVE_RETRY_MAX; + + do { + pcap_errbuf[0] = '\0'; /* so we can tell if a warning was produced on success */ + if ((pd = pcap_open_live(ifname, snaplen, promiscuous, GetResponse_wait_time(), pcap_errbuf)) != NULL) { + break; /* success */ + } else { /* failure */ + if (pcap_open_retries == 0) { + report(LOG_DEBUG, "pcap_open_live(%s): %s; retry count (%d) exceeded, giving up", ifname, pcap_errbuf, PCAP_OPEN_LIVE_RETRY_MAX); + my_exit(1, 1, 1); + } else { + if (debug > 1) + report(LOG_DEBUG, "pcap_open_live(%s): %s; will retry", ifname, pcap_errbuf); + sleep(PCAP_OPEN_LIVE_RETRY_DELAY); /* before next retry */ + } + } /* failure */ + } while (pcap_open_retries--); + + + if (pcap_errbuf[0] != '\0') + /* even on success, a warning may be produced */ + report(LOG_WARNING, "pcap_open_live(%s): succeeded but with warning: %s", ifname, pcap_errbuf); + + /* make sure this interface is ethernet */ + linktype = pcap_datalink(pd); + if (linktype != DLT_EN10MB) { + report(LOG_ERR, "interface %s link layer type %d not ethernet", ifname, linktype); + my_exit(1, 1, 1); + } + /* compile bpf filter to select just udp/ip traffic to udp port bootpc */ + if (pcap_compile(pd, &bpf_code, "udp dst port bootpc", 1, netmask) < 0) { + report(LOG_ERR, "pcap_compile: %s", pcap_geterr(pd)); + my_exit(1, 1, 1); + } + /* install compiled filter */ + if (pcap_setfilter(pd, &bpf_code) < 0) { + report(LOG_ERR, "pcap_setfilter: %s", pcap_geterr(pd)); + my_exit(1, 1, 1); + } + if (socket_receive_timeout_feature) + set_pcap_timeout(pd); + + return 0; +} + +void +reset_pcap() +{ + /* close packet capture descriptor */ + pcap_close(pd); +} + /* capture packets from pcap for timeout seconds */ int loop_for_packets(int timeout) @@ -115,8 +199,6 @@ /* for libpcap */ bpf_u_int32 netnumber, netmask; - struct bpf_program bpf_code; - int linktype; char pcap_errbuf[PCAP_ERRBUF_SIZE], pcap_errbuf2[PCAP_ERRBUF_SIZE]; /* get progname = last component of argv[0] */ @@ -126,7 +208,7 @@ else prog = argv[0]; - while ((c = getopt(argc, argv, "c:d:fhl:o:p:Q:s:Tvw:")) != EOF) { + while ((c = getopt(argc, argv, "c:d:fhkl:o:p:Q:s:Tvw:")) != EOF) { switch (c) { case 'c': if (optarg[0] != '/') { @@ -151,6 +233,9 @@ case 'h': usage(); my_exit(0, 0, 0); + case 'k': + keep_pcap = 1; + break; case 'l': if (optarg[0] != '/') { fprintf(stderr, "%s: invalid log file '%s', must be an absolute pathname\n", prog, optarg); @@ -447,8 +532,10 @@ } } + if (keep_pcap) + init_pcap(need_promiscuous(), netmask); + while (1) { /* MAIN EVENT LOOP */ - int promiscuous; libnet_t *l; /* to iterate through libnet context queue */ /* struct pcap_stat ps; */ /* to hold pcap stats */ @@ -489,26 +576,9 @@ interface in promiscuous mode as little as possible, since that can affect the host's performance. */ - /* If we're going to claim a chaddr different than my_eaddr, some of the responses - may come back to chaddr (as opposed to my_eaddr or broadcast), so we'll need to - listen promiscuously. - If we're going to claim an ether_src different than my_eaddr, in theory that should - make no difference; bootp/dhcp servers should rely on chaddr, not ether_src. Still, - it's possible there's a server out there that does it wrong, and might therefore mistakenly - send responses to ether_src. So lets also listen promiscuously if ether_src != my_eaddr. - */ - if (bcmp(GetChaddr(), &my_eaddr, sizeof(struct ether_addr)) || - bcmp(GetEther_src(), &my_eaddr, sizeof(struct ether_addr))) - promiscuous = 1; - else - promiscuous = 0; - - for (l = libnet_cq_head(); libnet_cq_last(); l = libnet_cq_next()) { /* write one flavor packet and listen for answers */ int packets_recv; - int pcap_open_retries; - /* We set up for packet capture BEFORE writing our packet, to minimize the delay between our writing and when we are able to start capturing. (I cannot tell from @@ -518,54 +588,9 @@ we wanted! */ - /* open packet capture descriptor */ - /* XXX On Solaris 7, sometimes pcap_open_live() fails with a message like: - pcap_open_live qfe0: recv_ack: info unexpected primitive ack 0x8 - It's not clear what causes this, or what the 0x8 code indicates. - The error appears to be transient; retrying sometimes will work, so I've wrapped the call in a retry loop. - I've also added a delay after each failure; perhaps the failure has something to do with the fact that - we call pcap_open_live() so soon after pcap_close() (for the second and succeeding packets in each cycle); - adding a delay might help in that case. - */ - pcap_open_retries = PCAP_OPEN_LIVE_RETRY_MAX; - while (pcap_open_retries--) { - pcap_errbuf[0] = '\0'; /* so we can tell if a warning was produced on success */ - if ((pd = pcap_open_live(ifname, snaplen, promiscuous, GetResponse_wait_time(), pcap_errbuf)) != NULL) { - break; /* success */ - } else { /* failure */ - if (pcap_open_retries == 0) { - report(LOG_DEBUG, "pcap_open_live(%s): %s; retry count (%d) exceeded, giving up", ifname, pcap_errbuf, PCAP_OPEN_LIVE_RETRY_MAX); - my_exit(1, 1, 1); - } else { - if (debug > 1) - report(LOG_DEBUG, "pcap_open_live(%s): %s; will retry", ifname, pcap_errbuf); - sleep(PCAP_OPEN_LIVE_RETRY_DELAY); /* before next retry */ - } - } /* failure */ - } - if (pcap_errbuf[0] != '\0') - /* even on success, a warning may be produced */ - report(LOG_WARNING, "pcap_open_live(%s): succeeded but with warning: %s", ifname, pcap_errbuf); - - /* make sure this interface is ethernet */ - linktype = pcap_datalink(pd); - if (linktype != DLT_EN10MB) { - report(LOG_ERR, "interface %s link layer type %d not ethernet", ifname, linktype); - my_exit(1, 1, 1); - } - /* compile bpf filter to select just udp/ip traffic to udp port bootpc */ - if (pcap_compile(pd, &bpf_code, "udp dst port bootpc", 1, netmask) < 0) { - report(LOG_ERR, "pcap_compile: %s", pcap_geterr(pd)); - my_exit(1, 1, 1); - } - /* install compiled filter */ - if (pcap_setfilter(pd, &bpf_code) < 0) { - report(LOG_ERR, "pcap_setfilter: %s", pcap_geterr(pd)); - my_exit(1, 1, 1); - } - if (socket_receive_timeout_feature) - set_pcap_timeout(pd); - + if (! keep_pcap) + init_pcap(need_promiscuous(), netmask); + /* write one packet */ if (debug > 10) @@ -621,7 +646,8 @@ */ /* close packet capture descriptor */ - pcap_close(pd); + if (! keep_pcap) + reset_pcap(); /* check for 'quit' request after each packet, since waiting until end of probe cycle would impose a substantial delay. */ @@ -669,7 +695,7 @@ reconfigure(write_packet_len); reread_config_file = 0; } - + /* We allow must signals that come in during our sleep() to interrupt us. E.g. we want to cut short our sleep when we're signalled to exit. But we must block SIGCHLD during our sleep. That's because if we forked an alert_program or alert_program2 child above, its termination will likely happen while we're sleeping; @@ -684,7 +710,19 @@ sigaddset(&new_sigset, SIGCHLD); sigprocmask(SIG_BLOCK, &new_sigset, &old_sigset); /* block SIGCHLD */ - sleep(time_to_sleep); + if (keep_pcap) { + /* If we're going to keep the packet capture running, + we might as well read off all the packets received while + waiting. We shouldn't get any since we don't send any requests + but this should prevent any buffers from accidentally filling + with unhandled packets. */ + int packets_recv = loop_for_packets(time_to_sleep); + + if (packets_recv && debug > 10) + report(LOG_DEBUG, "captured %d packets while sleeping", packets_recv); + } else { + sleep(time_to_sleep); + } sigprocmask(SIG_SETMASK, &old_sigset, NULL); /* unblock SIGCHLD */ @@ -692,8 +730,10 @@ } /* MAIN EVENT LOOP */ - /* we only reach here after receiving a signal requesting we quit */ + + if (keep_pcap) + reset_pcap(); if (pd_template) /* only used if a capture file requested */ pcap_close(pd_template); @@ -1142,6 +1182,7 @@ fprintf(stderr, " -d debuglevel enable debugging at specified level\n"); fprintf(stderr, " -f don't fork (only use for debugging)\n"); fprintf(stderr, " -h display this help message then exit\n"); + fprintf(stderr, " -k keep pcap open constantly (don't recreate on each cycle)\n"); fprintf(stderr, " -l log_file log to file instead of syslog\n"); fprintf(stderr, " -o capture_file enable capturing of unexpected answers\n"); fprintf(stderr, " -p pid_file override default pid file [%s]\n", PID_FILE); --4Ckj6UjgE2iN1+kY Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="dhcp-probe-03-drop-privs.patch" --- dhcp-probe/src/dhcp_probe.c.02 2009-08-16 12:31:22.000000000 +0300 +++ dhcp-probe/src/dhcp_probe.c 2009-08-16 13:47:29.000000000 +0300 @@ -26,6 +26,9 @@ #include "report.h" #include "utils.h" +#include +#include + #ifndef lint static const char rcsid[] = "dhcp_probe version " VERSION; static const char copyright[] = "Copyright 2000-2008, The Trustees of Princeton University. All rights reserved."; @@ -50,6 +53,8 @@ int snaplen = CAPTURE_BUFSIZE; int socket_receive_timeout_feature = 0; int keep_pcap = 0; +int drop_privs = 0; +char *username = NULL; char *prog = NULL; char *logfile_name = NULL; @@ -179,6 +184,40 @@ return packets_recv; } +/* drop privileges */ +void +drop_privileges(const char *username) +{ + struct passwd *pw; + pw = getpwnam(username); + if (pw == NULL) { + report(LOG_ERR, "getpwnam: %s", get_errmsg()); + my_exit(1, 1, 1); + } + if (debug > 1) + report(LOG_INFO, "changing to uid %d gid %d", pw->pw_uid, pw->pw_gid); + + if (setregid(pw->pw_gid, pw->pw_gid)) { + report(LOG_ERR, "setregid: %s", get_errmsg()); + my_exit(1, 1, 1); + } + if (setreuid(pw->pw_uid, pw->pw_uid)) { + report(LOG_ERR, "setreuid: %s", get_errmsg()); + my_exit(1, 1, 1); + } +} + +void write_pidfile(void) +{ + FILE *pid_fp; + if ((pid_fp = open_for_writing(pid_file)) == NULL) { + report(LOG_ERR, "could not open pid file %s for writing", pid_file); + my_exit(1, 0, 1); + } else { + fprintf(pid_fp, "%d\n", (int) getpid()); + fclose(pid_fp); + } +} int main(int argc, char **argv) @@ -188,7 +227,6 @@ extern char *optarg; extern int optind, opterr, optopt; struct sigaction sa; - FILE *pid_fp; char *cwd = CWD; int write_packet_len; @@ -208,7 +246,7 @@ else prog = argv[0]; - while ((c = getopt(argc, argv, "c:d:fhkl:o:p:Q:s:Tvw:")) != EOF) { + while ((c = getopt(argc, argv, "c:d:fhkl:o:p:Q:s:Tu:vw:")) != EOF) { switch (c) { case 'c': if (optarg[0] != '/') { @@ -283,6 +321,10 @@ } break; } + case 'u': + drop_privs = 1; + username = optarg; + break; case 'T': socket_receive_timeout_feature = 1; break; @@ -351,16 +393,6 @@ my_exit(1, 0, 1); } - - /* write pid file as soon as possible after (possibly) forking */ - if ((pid_fp = open_for_writing(pid_file)) == NULL) { - report(LOG_ERR, "could not open pid file %s for writing", pid_file); - my_exit(1, 0, 1); - } else { - fprintf(pid_fp, "%d\n", (int) getpid()); - fclose(pid_fp); - } - if (! read_configfile(config_file)) { my_exit(1, 1, 1); } @@ -535,6 +567,12 @@ if (keep_pcap) init_pcap(need_promiscuous(), netmask); + if (drop_privs) + drop_privileges(username); + + /* write the pid file after dropping privileges to be able to remove it later */ + write_pidfile(); + while (1) { /* MAIN EVENT LOOP */ libnet_t *l; /* to iterate through libnet context queue */ /* struct pcap_stat ps; */ /* to hold pcap stats */ @@ -1189,6 +1227,7 @@ fprintf(stderr, " -Q vlan_id tag outgoing frames with an 802.1Q VLAN ID\n"); fprintf(stderr, " -s capture_bufsize override default capture bufsize [%d]\n", CAPTURE_BUFSIZE); fprintf(stderr, " -T enable the socket receive timeout feature\n"); + fprintf(stderr, " -u username change uid after setup (use with -k\n"); fprintf(stderr, " -v display version number then exit\n"); fprintf(stderr, " -w cwd override default working directory [%s]\n", CWD); fprintf(stderr, " interface_name name of ethernet interface\n"); --4Ckj6UjgE2iN1+kY--