/* * nwfilter_dhcpsnoop.c: support for DHCP snooping used by a VM * on an interface * * Copyright (C) 2012 Red Hat, Inc. * Copyright (C) 2011,2012 IBM Corp. * * Authors: * David L Stevens * Stefan Berger * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see * . * * Based in part on work by Stefan Berger */ /* * Note about testing: * On the host run in a shell: * while :; do kill -SIGHUP `pidof libvirtd` ; echo "HUP $RANDOM"; sleep 20; done * * Inside a couple of VMs that for example use the 'clean-traffic' filter: * while :; do kill -SIGTERM `pidof dhclient`; dhclient eth0 ; ifconfig eth0; done * * On the host check the lease file and that it's periodically shortened: * cat /var/run/libvirt/network/nwfilter.leases ; date +%s * * On the host also check that the ebtables rules 'look' ok: * ebtables -t nat -L */ #include #ifdef HAVE_LIBPCAP # include #endif #include #include #include #include #include #include #include "memory.h" #include "logging.h" #include "datatypes.h" #include "virterror_internal.h" #include "conf/domain_conf.h" #include "nwfilter_gentech_driver.h" #include "nwfilter_dhcpsnoop.h" #include "nwfilter_ipaddrmap.h" #include "virnetdev.h" #include "virfile.h" #include "viratomic.h" #include "threadpool.h" #include "configmake.h" #include "virtime.h" #define VIR_FROM_THIS VIR_FROM_NWFILTER #ifdef HAVE_LIBPCAP # define LEASEFILE LOCALSTATEDIR "/run/libvirt/network/nwfilter.leases" # define TMPLEASEFILE LOCALSTATEDIR "/run/libvirt/network/nwfilter.ltmp" struct virNWFilterSnoopState { /* lease file */ int leaseFD; int nLeases; /* number of active leases */ int wLeases; /* number of written leases */ int nThreads; /* number of running threads */ /* thread management */ virHashTablePtr snoopReqs; virHashTablePtr ifnameToKey; virMutex snoopLock; /* protects SnoopReqs and IfNameToKey */ virHashTablePtr active; virMutex activeLock; /* protects Active */ }; # define virNWFilterSnoopLock() \ do { \ virMutexLock(&virNWFilterSnoopState.snoopLock); \ } while (0) # define virNWFilterSnoopUnlock() \ do { \ virMutexUnlock(&virNWFilterSnoopState.snoopLock); \ } while (0) # define virNWFilterSnoopActiveLock() \ do { \ virMutexLock(&virNWFilterSnoopState.activeLock); \ } while (0) # define virNWFilterSnoopActiveUnlock() \ do { \ virMutexUnlock(&virNWFilterSnoopState.activeLock); \ } while (0) # define VIR_IFKEY_LEN ((VIR_UUID_STRING_BUFLEN) + (VIR_MAC_STRING_BUFLEN)) typedef struct _virNWFilterSnoopReq virNWFilterSnoopReq; typedef virNWFilterSnoopReq *virNWFilterSnoopReqPtr; typedef struct _virNWFilterSnoopIPLease virNWFilterSnoopIPLease; typedef virNWFilterSnoopIPLease *virNWFilterSnoopIPLeasePtr; typedef enum { THREAD_STATUS_NONE, THREAD_STATUS_OK, THREAD_STATUS_FAIL, } virNWFilterSnoopThreadStatus; struct _virNWFilterSnoopReq { /* * reference counter: while the req is on the * publicSnoopReqs hash, the refctr may only * be modified with the SnoopLock held */ int refctr; virNWFilterTechDriverPtr techdriver; char *ifname; int ifindex; const char *linkdev; enum virDomainNetType nettype; char ifkey[VIR_IFKEY_LEN]; virMacAddr macaddr; const char *filtername; virNWFilterHashTablePtr vars; virNWFilterDriverStatePtr driver; /* start and end of lease list, ordered by lease time */ virNWFilterSnoopIPLeasePtr start; virNWFilterSnoopIPLeasePtr end; char *threadkey; virNWFilterSnoopThreadStatus threadStatus; virCond threadStatusCond; int jobCompletionStatus; /* the number of submitted jobs in the worker's queue */ /* * protect those members that can change while the * req is on the public SnoopReq hash and * at least one reference is held: * - ifname * - threadkey * - start * - end * - a lease while it is on the list * - threadStatus * (for refctr, see above) */ virMutex lock; }; /* * Note about lock-order: * 1st: virNWFilterSnoopLock() * 2nd: virNWFilterSnoopReqLock(req) * * Rationale: Former protects the SnoopReqs hash, latter its contents */ struct _virNWFilterSnoopIPLease { virSocketAddr ipAddress; virSocketAddr ipServer; virNWFilterSnoopReqPtr snoopReq; unsigned int timeout; /* timer list */ virNWFilterSnoopIPLeasePtr prev; virNWFilterSnoopIPLeasePtr next; }; typedef struct _virNWFilterSnoopEthHdr virNWFilterSnoopEthHdr; typedef virNWFilterSnoopEthHdr *virNWFilterSnoopEthHdrPtr; struct _virNWFilterSnoopEthHdr { virMacAddr eh_dst; virMacAddr eh_src; uint16_t eh_type; uint8_t eh_data[]; } ATTRIBUTE_PACKED; typedef struct _virNWFilterSnoopDHCPHdr virNWFilterSnoopDHCPHdr; typedef virNWFilterSnoopDHCPHdr *virNWFilterSnoopDHCPHdrPtr; struct _virNWFilterSnoopDHCPHdr { uint8_t d_op; uint8_t d_htype; uint8_t d_hlen; uint8_t d_hops; uint32_t d_xid; uint16_t d_secs; uint16_t d_flags; uint32_t d_ciaddr; uint32_t d_yiaddr; uint32_t d_siaddr; uint32_t d_giaddr; uint8_t d_chaddr[16]; char d_sname[64]; char d_file[128]; uint8_t d_opts[]; } ATTRIBUTE_PACKED; /* DHCP options */ # define DHCPO_PAD 0 # define DHCPO_LEASE 51 /* lease time in secs */ # define DHCPO_MTYPE 53 /* message type */ # define DHCPO_END 255 /* end of options */ /* DHCP message types */ # define DHCPDECLINE 4 # define DHCPACK 5 # define DHCPRELEASE 7 # define MIN_VALID_DHCP_PKT_SIZE \ (offsetof(virNWFilterSnoopEthHdr, eh_data) + \ sizeof(struct udphdr) + \ offsetof(virNWFilterSnoopDHCPHdr, d_opts)) # define PCAP_PBUFSIZE 576 /* >= IP/TCP/DHCP headers */ # define PCAP_READ_MAXERRS 25 /* retries on failing device */ # define PCAP_FLOOD_TIMEOUT_MS 10 /* ms */ typedef struct _virNWFilterDHCPDecodeJob virNWFilterDHCPDecodeJob; typedef virNWFilterDHCPDecodeJob *virNWFilterDHCPDecodeJobPtr; struct _virNWFilterDHCPDecodeJob { unsigned char packet[PCAP_PBUFSIZE]; int caplen; bool fromVM; int *qCtr; }; # define DHCP_PKT_RATE 10 /* pkts/sec */ # define DHCP_PKT_BURST 50 /* pkts/sec */ # define DHCP_BURST_INTERVAL_S 10 /* sec */ # define PCAP_BUFFERSIZE (DHCP_PKT_BURST * PCAP_PBUFSIZE / 2) # define MAX_QUEUED_JOBS (DHCP_PKT_BURST + 2 * DHCP_PKT_RATE) typedef struct _virNWFilterSnoopRateLimitConf virNWFilterSnoopRateLimitConf; typedef virNWFilterSnoopRateLimitConf *virNWFilterSnoopRateLimitConfPtr; struct _virNWFilterSnoopRateLimitConf { time_t prev; unsigned int pkt_ctr; time_t burst; const unsigned int rate; const unsigned int burstRate; const unsigned int burstInterval; }; typedef struct _virNWFilterSnoopPcapConf virNWFilterSnoopPcapConf; typedef virNWFilterSnoopPcapConf *virNWFilterSnoopPcapConfPtr; struct _virNWFilterSnoopPcapConf { pcap_t *handle; const pcap_direction_t dir; const char *filter; virNWFilterSnoopRateLimitConf rateLimit; /* indep. rate limiters */ int qCtr; /* number of jobs in the worker's queue */ const unsigned int maxQSize; unsigned long long penaltyTimeoutAbs; }; /* local function prototypes */ static int virNWFilterSnoopReqLeaseDel(virNWFilterSnoopReqPtr req, virSocketAddrPtr ipaddr, bool update_leasefile, bool instantiate); static void virNWFilterSnoopReqLock(virNWFilterSnoopReqPtr req); static void virNWFilterSnoopReqUnlock(virNWFilterSnoopReqPtr req); static void virNWFilterSnoopLeaseFileLoad(void); static void virNWFilterSnoopLeaseFileSave(virNWFilterSnoopIPLeasePtr ipl); /* local variables */ static struct virNWFilterSnoopState virNWFilterSnoopState = { .leaseFD = -1, }; static const unsigned char dhcp_magic[4] = { 99, 130, 83, 99 }; static char * virNWFilterSnoopActivate(virNWFilterSnoopReqPtr req) { char *key; if (virAsprintf(&key, "%p-%d", req, req->ifindex) < 0) { virReportOOMError(); return NULL; } virNWFilterSnoopActiveLock(); if (virHashAddEntry(virNWFilterSnoopState.active, key, (void *)0x1) < 0) { VIR_FREE(key); } virNWFilterSnoopActiveUnlock(); return key; } static void virNWFilterSnoopCancel(char **threadKey) { if (*threadKey == NULL) return; virNWFilterSnoopActiveLock(); ignore_value(virHashRemoveEntry(virNWFilterSnoopState.active, *threadKey)); VIR_FREE(*threadKey); virNWFilterSnoopActiveUnlock(); } static bool virNWFilterSnoopIsActive(char *threadKey) { void *entry; if (threadKey == NULL) return 0; virNWFilterSnoopActiveLock(); entry = virHashLookup(virNWFilterSnoopState.active, threadKey); virNWFilterSnoopActiveUnlock(); return entry != NULL; } /* * virNWFilterSnoopListAdd - add an IP lease to a list */ static void virNWFilterSnoopListAdd(virNWFilterSnoopIPLeasePtr plnew, virNWFilterSnoopIPLeasePtr *start, virNWFilterSnoopIPLeasePtr *end) { virNWFilterSnoopIPLeasePtr pl; plnew->next = plnew->prev = NULL; if (!*start) { *start = *end = plnew; return; } for (pl = *end; pl && plnew->timeout < pl->timeout; pl = pl->prev) /* empty */ ; if (!pl) { plnew->next = *start; *start = plnew; } else { plnew->next = pl->next; pl->next = plnew; } plnew->prev = pl; if (plnew->next) plnew->next->prev = plnew; else *end = plnew; } /* * virNWFilterSnoopListDel - remove an IP lease from a list */ static void virNWFilterSnoopListDel(virNWFilterSnoopIPLeasePtr ipl, virNWFilterSnoopIPLeasePtr *start, virNWFilterSnoopIPLeasePtr *end) { if (ipl->prev) ipl->prev->next = ipl->next; else *start = ipl->next; if (ipl->next) ipl->next->prev = ipl->prev; else *end = ipl->prev; ipl->next = ipl->prev = NULL; } /* * virNWFilterSnoopLeaseTimerAdd - add an IP lease to the timer list */ static void virNWFilterSnoopIPLeaseTimerAdd(virNWFilterSnoopIPLeasePtr plnew) { virNWFilterSnoopReqPtr req = plnew->snoopReq; /* protect req->start / req->end */ virNWFilterSnoopReqLock(req); virNWFilterSnoopListAdd(plnew, &req->start, &req->end); virNWFilterSnoopReqUnlock(req); } /* * virNWFilterSnoopLeaseTimerDel - remove an IP lease from the timer list */ static void virNWFilterSnoopIPLeaseTimerDel(virNWFilterSnoopIPLeasePtr ipl) { virNWFilterSnoopReqPtr req = ipl->snoopReq; /* protect req->start / req->end */ virNWFilterSnoopReqLock(req); virNWFilterSnoopListDel(ipl, &req->start, &req->end); virNWFilterSnoopReqUnlock(req); ipl->timeout = 0; } /* * virNWFilterSnoopInstallRule - install rule for a lease * * @instantiate: when calling this function in a loop, indicate * the last call with 'true' here so that the * rules all get instantiated * Always calling this with 'true' is fine, but less * efficient. */ static int virNWFilterSnoopIPLeaseInstallRule(virNWFilterSnoopIPLeasePtr ipl, bool instantiate) { char *ipaddr; int rc = -1; virNWFilterSnoopReqPtr req; ipaddr = virSocketAddrFormat(&ipl->ipAddress); if (!ipaddr) return -1; req = ipl->snoopReq; /* protect req->ifname */ virNWFilterSnoopReqLock(req); if (virNWFilterIPAddrMapAddIPAddr(req->ifname, ipaddr) < 0) goto exit_snooprequnlock; /* ipaddr now belongs to the map */ ipaddr = NULL; if (!instantiate) { rc = 0; goto exit_snooprequnlock; } /* instantiate the filters */ if (req->ifname) rc = virNWFilterInstantiateFilterLate(NULL, req->ifname, req->ifindex, req->linkdev, req->nettype, &req->macaddr, req->filtername, req->vars, req->driver); exit_snooprequnlock: virNWFilterSnoopReqUnlock(req); VIR_FREE(ipaddr); return rc; } /* * virNWFilterSnoopIPLeaseUpdate - update the timeout on an IP lease */ static void virNWFilterSnoopIPLeaseUpdate(virNWFilterSnoopIPLeasePtr ipl, time_t timeout) { if (timeout < ipl->timeout) return; /* no take-backs */ virNWFilterSnoopIPLeaseTimerDel(ipl); ipl->timeout = timeout; virNWFilterSnoopIPLeaseTimerAdd(ipl); } /* * virNWFilterSnoopGetByIP - lookup IP lease by IP address */ static virNWFilterSnoopIPLeasePtr virNWFilterSnoopIPLeaseGetByIP(virNWFilterSnoopIPLeasePtr start, virSocketAddrPtr ipaddr) { virNWFilterSnoopIPLeasePtr pl; for (pl = start; pl && !virSocketAddrEqual(&pl->ipAddress, ipaddr); pl = pl->next) /* empty */ ; return pl; } /* * virNWFilterSnoopReqLeaseTimerRun - run the IP lease timeout list */ static unsigned int virNWFilterSnoopReqLeaseTimerRun(virNWFilterSnoopReqPtr req) { time_t now = time(0); bool is_last = false; /* protect req->start */ virNWFilterSnoopReqLock(req); while (req->start && req->start->timeout <= now) { if (req->start->next == NULL || req->start->next->timeout > now) is_last = true; virNWFilterSnoopReqLeaseDel(req, &req->start->ipAddress, true, is_last); } virNWFilterSnoopReqUnlock(req); return 0; } /* * Get a reference to the given Snoop request */ static void virNWFilterSnoopReqGet(virNWFilterSnoopReqPtr req) { virAtomicIntInc(&req->refctr); } /* * Create a new Snoop request. Initialize it with the given * interface key. The caller must release the request with a call * to virNWFilerSnoopReqPut(req). */ static virNWFilterSnoopReqPtr virNWFilterSnoopReqNew(const char *ifkey) { virNWFilterSnoopReqPtr req; if (ifkey == NULL || strlen(ifkey) != VIR_IFKEY_LEN - 1) { virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterSnoopReqNew called with invalid " "key \"%s\" (%zu)"), ifkey ? ifkey : "", strlen(ifkey)); return NULL; } if (VIR_ALLOC(req) < 0) { virReportOOMError(); return NULL; } req->threadStatus = THREAD_STATUS_NONE; if (virStrcpyStatic(req->ifkey, ifkey) == NULL || virMutexInitRecursive(&req->lock) < 0) goto err_free_req; if (virCondInit(&req->threadStatusCond) < 0) goto err_destroy_mutex; virNWFilterSnoopReqGet(req); return req; err_destroy_mutex: virMutexDestroy(&req->lock); err_free_req: VIR_FREE(req); return NULL; } /* * Free a snoop request unless it is still referenced. * All its associated leases are also freed. * The lease file is NOT rewritten. */ static void virNWFilterSnoopReqFree(virNWFilterSnoopReqPtr req) { virNWFilterSnoopIPLeasePtr ipl; if (!req) return; if (virAtomicIntGet(&req->refctr) != 0) return; /* free all leases */ for (ipl = req->start; ipl; ipl = req->start) virNWFilterSnoopReqLeaseDel(req, &ipl->ipAddress, false, false); /* free all req data */ VIR_FREE(req->ifname); VIR_FREE(req->linkdev); VIR_FREE(req->filtername); virNWFilterHashTableFree(req->vars); virMutexDestroy(&req->lock); ignore_value(virCondDestroy(&req->threadStatusCond)); VIR_FREE(req); } /* * Lock a Snoop request 'req' */ static void virNWFilterSnoopReqLock(virNWFilterSnoopReqPtr req) { virMutexLock(&req->lock); } /* * Unlock a Snoop request 'req' */ static void virNWFilterSnoopReqUnlock(virNWFilterSnoopReqPtr req) { virMutexUnlock(&req->lock); } /* * virNWFilterSnoopReqRelease - hash table free function to kill a request */ static void virNWFilterSnoopReqRelease(void *req0, const void *name ATTRIBUTE_UNUSED) { virNWFilterSnoopReqPtr req = req0; if (!req) return; /* protect req->threadkey */ virNWFilterSnoopReqLock(req); if (req->threadkey) virNWFilterSnoopCancel(&req->threadkey); virNWFilterSnoopReqUnlock(req); virNWFilterSnoopReqFree(req); } /* * virNWFilterSnoopReqGetByIFKey * * Get a Snoop request given an interface key; caller must release * the Snoop request with a call to virNWFilterSnoopReqPut() */ static virNWFilterSnoopReqPtr virNWFilterSnoopReqGetByIFKey(const char *ifkey) { virNWFilterSnoopReqPtr req; virNWFilterSnoopLock(); req = virHashLookup(virNWFilterSnoopState.snoopReqs, ifkey); if (req) virNWFilterSnoopReqGet(req); virNWFilterSnoopUnlock(); return req; } /* * Drop the reference to the Snoop request. Don't use the req * after this call. */ static void virNWFilterSnoopReqPut(virNWFilterSnoopReqPtr req) { if (!req) return; virNWFilterSnoopLock(); if (virAtomicIntDecAndTest(&req->refctr)) { /* * delete the request: * - if we don't find req on the global list anymore * (this happens during SIGHUP) * we would keep the request: * - if we still have a valid lease, keep the req for restarts */ if (virHashLookup(virNWFilterSnoopState.snoopReqs, req->ifkey) != req) { virNWFilterSnoopReqRelease(req, NULL); } else if (!req->start || req->start->timeout < time(0)) { ignore_value(virHashRemoveEntry(virNWFilterSnoopState.snoopReqs, req->ifkey)); } } virNWFilterSnoopUnlock(); } /* * virNWFilterSnoopReqLeaseAdd - create or update an IP lease */ static int virNWFilterSnoopReqLeaseAdd(virNWFilterSnoopReqPtr req, virNWFilterSnoopIPLeasePtr plnew, bool update_leasefile) { virNWFilterSnoopIPLeasePtr pl; plnew->snoopReq = req; /* protect req->start and the lease */ virNWFilterSnoopReqLock(req); pl = virNWFilterSnoopIPLeaseGetByIP(req->start, &plnew->ipAddress); if (pl) { virNWFilterSnoopIPLeaseUpdate(pl, plnew->timeout); virNWFilterSnoopReqUnlock(req); goto exit; } virNWFilterSnoopReqUnlock(req); if (VIR_ALLOC(pl) < 0) { virReportOOMError(); return -1; } *pl = *plnew; /* protect req->threadkey */ virNWFilterSnoopReqLock(req); if (req->threadkey && virNWFilterSnoopIPLeaseInstallRule(pl, true) < 0) { virNWFilterSnoopReqUnlock(req); VIR_FREE(pl); return -1; } virNWFilterSnoopReqUnlock(req); /* put the lease on the req's list */ virNWFilterSnoopIPLeaseTimerAdd(pl); virAtomicIntInc(&virNWFilterSnoopState.nLeases); exit: if (update_leasefile) virNWFilterSnoopLeaseFileSave(pl); return 0; } /* * Restore a Snoop request -- walk its list of leases * and re-build the filtering rules with them */ static int virNWFilterSnoopReqRestore(virNWFilterSnoopReqPtr req) { int ret = 0; virNWFilterSnoopIPLeasePtr ipl; /* protect req->start */ virNWFilterSnoopReqLock(req); for (ipl = req->start; ipl; ipl = ipl->next) { /* instantiate the rules at the last lease */ bool is_last = (ipl->next == NULL); if (virNWFilterSnoopIPLeaseInstallRule(ipl, is_last) < 0) { ret = -1; break; } } virNWFilterSnoopReqUnlock(req); return ret; } /* * virNWFilterSnoopReqLeaseDel - delete an IP lease * * @update_leasefile: set to 'true' if the lease expired or the lease * was returned to the DHCP server and therefore * this has to be noted in the lease file. * set to 'false' for any other reason such as for * example when calling only to free the lease's * memory or when calling this function while reading * leases from the file. * * @instantiate: when calling this function in a loop, indicate * the last call with 'true' here so that the * rules all get instantiated * Always calling this with 'true' is fine, but less * efficient. * * Returns 0 on success, -1 if the instantiation of the rules failed */ static int virNWFilterSnoopReqLeaseDel(virNWFilterSnoopReqPtr req, virSocketAddrPtr ipaddr, bool update_leasefile, bool instantiate) { int ret = 0; virNWFilterSnoopIPLeasePtr ipl; char *ipstr = NULL; int ipAddrLeft; /* protect req->start, req->ifname and the lease */ virNWFilterSnoopReqLock(req); ipl = virNWFilterSnoopIPLeaseGetByIP(req->start, ipaddr); if (ipl == NULL) goto lease_not_found; ipstr = virSocketAddrFormat(&ipl->ipAddress); if (!ipstr) { ret = -1; goto lease_not_found; } virNWFilterSnoopIPLeaseTimerDel(ipl); /* lease is off the list now */ if (update_leasefile) virNWFilterSnoopLeaseFileSave(ipl); ipAddrLeft = virNWFilterIPAddrMapDelIPAddr(req->ifname, ipstr); if (!req->threadkey || !instantiate) goto skip_instantiate; if (ipAddrLeft) { ret = virNWFilterInstantiateFilterLate(NULL, req->ifname, req->ifindex, req->linkdev, req->nettype, &req->macaddr, req->filtername, req->vars, req->driver); } else { const virNWFilterVarValuePtr dhcpsrvrs = virHashLookup(req->vars->hashTable, NWFILTER_VARNAME_DHCPSERVER); if (req->techdriver && req->techdriver->applyDHCPOnlyRules(req->ifname, &req->macaddr, dhcpsrvrs, false) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("virNWFilterSnoopListDel failed")); ret = -1; } } skip_instantiate: VIR_FREE(ipl); virAtomicIntDecAndTest(&virNWFilterSnoopState.nLeases); lease_not_found: VIR_FREE(ipstr); virNWFilterSnoopReqUnlock(req); return ret; } static int virNWFilterSnoopDHCPGetOpt(virNWFilterSnoopDHCPHdrPtr pd, int len, uint8_t *pmtype, uint32_t *pleasetime) { int oind, olen; int oend; uint32_t nwint; olen = len - sizeof(*pd); oind = 0; if (olen < 4) /* bad magic */ return -1; if (memcmp(dhcp_magic, pd->d_opts, sizeof(dhcp_magic)) != 0) return -1; /* bad magic */ oind += sizeof(dhcp_magic); oend = 0; *pmtype = 0; *pleasetime = 0; while (oind < olen) { switch (pd->d_opts[oind]) { case DHCPO_LEASE: if (olen - oind < 6) goto malformed; if (*pleasetime) return -1; /* duplicate lease time */ memcpy(&nwint, (char *)pd->d_opts + oind + 2, sizeof(nwint)); *pleasetime = ntohl(nwint); break; case DHCPO_MTYPE: if (olen - oind < 3) goto malformed; if (*pmtype) return -1; /* duplicate message type */ *pmtype = pd->d_opts[oind + 2]; break; case DHCPO_PAD: oind++; continue; case DHCPO_END: oend = 1; break; default: if (olen - oind < 2) goto malformed; } if (oend) break; oind += pd->d_opts[oind + 1] + 2; } return 0; malformed: VIR_WARN("got lost in the options!"); return -1; } /* * Decode the DHCP options * * Returns 0 in case of full success. * Returns -2 in case of some error with the packet. * Returns -1 in case of error with the installation of rules */ static int virNWFilterSnoopDHCPDecode(virNWFilterSnoopReqPtr req, virNWFilterSnoopEthHdrPtr pep, int len, bool fromVM) { struct iphdr *pip; struct udphdr *pup; virNWFilterSnoopDHCPHdrPtr pd; virNWFilterSnoopIPLease ipl; uint8_t mtype; uint32_t leasetime; uint32_t nwint; /* go through the protocol headers */ switch (ntohs(pep->eh_type)) { case ETHERTYPE_IP: pip = (struct iphdr *) pep->eh_data; len -= offsetof(virNWFilterSnoopEthHdr, eh_data); break; default: return -2; } if (len < 0) return -2; pup = (struct udphdr *) ((char *) pip + (pip->ihl << 2)); len -= pip->ihl << 2; if (len < 0) return -2; pd = (virNWFilterSnoopDHCPHdrPtr) ((char *) pup + sizeof(*pup)); len -= sizeof(*pup); if (len < 0) return -2; /* invalid packet length */ /* * some DHCP servers send their responses as MAC broadcast replies * filter messages from the server also by the destination MAC * inside the DHCP response */ if (!fromVM) { if (virMacAddrCmpRaw(&req->macaddr, (unsigned char *)&pd->d_chaddr) != 0) return -2; } if (virNWFilterSnoopDHCPGetOpt(pd, len, &mtype, &leasetime) < 0) return -2; memset(&ipl, 0, sizeof(ipl)); memcpy(&nwint, &pd->d_yiaddr, sizeof(nwint)); virSocketAddrSetIPv4Addr(&ipl.ipAddress, ntohl(nwint)); memcpy(&nwint, &pd->d_siaddr, sizeof(nwint)); virSocketAddrSetIPv4Addr(&ipl.ipServer, ntohl(nwint)); if (leasetime == ~0) ipl.timeout = ~0; else ipl.timeout = time(0) + leasetime; ipl.snoopReq = req; /* check that the type of message comes from the right direction */ switch (mtype) { case DHCPACK: case DHCPDECLINE: if (fromVM) return -2; break; case DHCPRELEASE: if (!fromVM) return -2; break; default: break; } switch (mtype) { case DHCPACK: if (virNWFilterSnoopReqLeaseAdd(req, &ipl, true) < 0) return -1; break; case DHCPDECLINE: case DHCPRELEASE: if (virNWFilterSnoopReqLeaseDel(req, &ipl.ipAddress, true, true) < 0) return -1; break; default: return -2; } return 0; } static pcap_t * virNWFilterSnoopDHCPOpen(const char *ifname, virMacAddr *mac, const char *filter, pcap_direction_t dir) { pcap_t *handle = NULL; struct bpf_program fp; char pcap_errbuf[PCAP_ERRBUF_SIZE]; char *ext_filter = NULL; char macaddr[VIR_MAC_STRING_BUFLEN]; virMacAddrFormat(mac, macaddr); if (dir == PCAP_D_IN /* from VM */) { /* * don't want to hear about another VM's DHCP requests * * extend the filter with the macaddr of the VM; filter the * more unlikely parameters first, then go for the MAC */ if (virAsprintf(&ext_filter, "%s and ether src %s", filter, macaddr) < 0) { virReportOOMError(); return NULL; } } else { /* * Some DHCP servers respond via MAC broadcast; we rely on later * filtering of responses by comparing the MAC address inside the * DHCP response against the one of the VM. Assuming that the * bridge learns the VM's MAC address quickly this should not * generate much more traffic than if we filtered by VM and * braodcast MAC as well */ if (virAsprintf(&ext_filter, "%s", filter) < 0) { virReportOOMError(); return NULL; } } handle = pcap_create(ifname, pcap_errbuf); if (handle == NULL) { virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("pcap_create failed")); goto cleanup_nohandle; } if (pcap_set_snaplen(handle, PCAP_PBUFSIZE) < 0 || pcap_set_buffer_size(handle, PCAP_BUFFERSIZE) < 0 || pcap_activate(handle) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("setup of pcap handle failed")); goto cleanup; } if (pcap_compile(handle, &fp, ext_filter, 1, PCAP_NETMASK_UNKNOWN) != 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("pcap_compile: %s"), pcap_geterr(handle)); goto cleanup; } if (pcap_setfilter(handle, &fp) != 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("pcap_setfilter: %s"), pcap_geterr(handle)); goto cleanup_freecode; } if (pcap_setdirection(handle, dir) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("pcap_setdirection: %s"), pcap_geterr(handle)); goto cleanup_freecode; } pcap_freecode(&fp); VIR_FREE(ext_filter); return handle; cleanup_freecode: pcap_freecode(&fp); cleanup: pcap_close(handle); cleanup_nohandle: VIR_FREE(ext_filter); return NULL; } /* * Worker function to decode the DHCP message and with that * also do the time-consuming work of instantiating the filters */ static void virNWFilterDHCPDecodeWorker(void *jobdata, void *opaque) { virNWFilterSnoopReqPtr req = opaque; virNWFilterDHCPDecodeJobPtr job = jobdata; virNWFilterSnoopEthHdrPtr packet = (virNWFilterSnoopEthHdrPtr)job->packet; if (virNWFilterSnoopDHCPDecode(req, packet, job->caplen, job->fromVM) == -1) { req->jobCompletionStatus = -1; virReportError(VIR_ERR_INTERNAL_ERROR, _("Instantiation of rules failed on " "interface '%s'"), req->ifname); } virAtomicIntDecAndTest(job->qCtr); VIR_FREE(job); } /* * Submit a job to the worker thread doing the time-consuming work... */ static int virNWFilterSnoopDHCPDecodeJobSubmit(virThreadPoolPtr pool, virNWFilterSnoopEthHdrPtr pep, int len, pcap_direction_t dir, int *qCtr) { virNWFilterDHCPDecodeJobPtr job; int ret; if (len <= MIN_VALID_DHCP_PKT_SIZE || len > sizeof(job->packet)) return 0; if (VIR_ALLOC(job) < 0) { virReportOOMError(); return -1; } memcpy(job->packet, pep, len); job->caplen = len; job->fromVM = (dir == PCAP_D_IN); job->qCtr = qCtr; ret = virThreadPoolSendJob(pool, 0, job); if (ret == 0) virAtomicIntInc(qCtr); else VIR_FREE(job); return ret; } /* * virNWFilterSnoopRateLimit -- limit the rate of jobs submitted to the * worker thread * * Help defend the worker thread from being flooded with likely bogus packets * sent by the VM. * * rl: The state of the rate limiter * * Returns the delta of packets compared to the rate, i.e. if the rate * is 4 (pkts/s) and we now have received 5 within a second, it would * return 1. If the number of packets is below the rate, it returns 0. */ static unsigned int virNWFilterSnoopRateLimit(virNWFilterSnoopRateLimitConfPtr rl) { time_t now = time(0); int diff; # define IN_BURST(n,b) ((n)-(b) <= 1) /* bursts span 2 discrete seconds */ if (rl->prev != now && !IN_BURST(now, rl->burst)) { rl->prev = now; rl->pkt_ctr = 1; } else { rl->pkt_ctr++; if (rl->pkt_ctr >= rl->rate) { if (IN_BURST(now, rl->burst)) { /* in a burst */ diff = rl->pkt_ctr - rl->burstRate; if (diff > 0) return diff; return 0; } if (rl->prev - rl->burst > rl->burstInterval) { /* this second will start a new burst */ rl->burst = rl->prev; return 0; } /* previous burst is too close */ return rl->pkt_ctr - rl->rate; } } return 0; } /* * virNWFilterSnoopRatePenalty * * @pc: pointer to the virNWFilterSnoopPcapConf * @diff: the amount of pkts beyond the rate, i.e., if the rate is 10 * and 13 pkts have been received now in one seconds, then * this should be 3. * * Adjusts the timeout the virNWFilterSnooPcapConf will be penalized for * sending too many packets. */ static void virNWFilterSnoopRatePenalty(virNWFilterSnoopPcapConfPtr pc, unsigned int diff, unsigned int limit) { if (diff > limit) { unsigned long long now; if (virTimeMillisNowRaw(&now) < 0) { usleep(PCAP_FLOOD_TIMEOUT_MS); /* 1 ms */ pc->penaltyTimeoutAbs = 0; } else { /* don't listen to the fd for 1 ms */ pc->penaltyTimeoutAbs = now + PCAP_FLOOD_TIMEOUT_MS; } } } static int virNWFilterSnoopAdjustPoll(virNWFilterSnoopPcapConfPtr pc, size_t nPc, struct pollfd *pfd, int *pollTo) { int ret = 0; size_t i; int tmp; unsigned long long now = 0; *pollTo = -1; for (i = 0; i < nPc; i++) { if (pc[i].penaltyTimeoutAbs != 0) { if (now == 0) { if (virTimeMillisNow(&now) < 0) { ret = -1; break; } } if (now < pc[i].penaltyTimeoutAbs) { /* don't listen to incoming data on the fd for some time */ pfd[i].events &= ~POLLIN; /* * calc the max. time to spend in poll() until adjustments * to the pollfd array are needed again. */ tmp = pc[i].penaltyTimeoutAbs - now; if (*pollTo == -1 || tmp < *pollTo) *pollTo = tmp; } else { /* listen again to the fd */ pfd[i].events |= POLLIN; pc[i].penaltyTimeoutAbs = 0; } } } return ret; } /* * The DHCP snooping thread. It spends most of its time in the pcap * library and if it gets suitable packets, it submits them to the worker * thread for processing. */ static void virNWFilterDHCPSnoopThread(void *req0) { virNWFilterSnoopReqPtr req = req0; struct pcap_pkthdr *hdr; virNWFilterSnoopEthHdrPtr packet; int ifindex = 0; int errcount = 0; int tmp = -1, i, rv, n, pollTo; char *threadkey = NULL; virThreadPoolPtr worker = NULL; time_t last_displayed = 0, last_displayed_queue = 0; virNWFilterSnoopPcapConf pcapConf[] = { { .dir = PCAP_D_IN, /* from VM */ .filter = "dst port 67 and src port 68", .rateLimit = { .prev = time(0), .rate = DHCP_PKT_RATE, .burstRate = DHCP_PKT_BURST, .burstInterval = DHCP_BURST_INTERVAL_S, }, .maxQSize = MAX_QUEUED_JOBS, }, { .dir = PCAP_D_OUT, /* to VM */ .filter = "src port 67 and dst port 68", .rateLimit = { .prev = time(0), .rate = DHCP_PKT_RATE, .burstRate = DHCP_PKT_BURST, .burstInterval = DHCP_BURST_INTERVAL_S, }, .maxQSize = MAX_QUEUED_JOBS, }, }; struct pollfd fds[] = { { /* get a POLLERR if interface goes down or disappears */ .events = POLLIN | POLLERR, }, { .events = POLLIN | POLLERR, }, }; bool error = false; /* whoever started us increased the reference counter for the req for us */ /* protect req->ifname & req->threadkey */ virNWFilterSnoopReqLock(req); if (req->ifname && req->threadkey) { for (i = 0; i < ARRAY_CARDINALITY(pcapConf); i++) { pcapConf[i].handle = virNWFilterSnoopDHCPOpen(req->ifname, &req->macaddr, pcapConf[i].filter, pcapConf[i].dir); if (!pcapConf[i].handle) { error = true; break; } fds[i].fd = pcap_fileno(pcapConf[i].handle); } tmp = virNetDevGetIndex(req->ifname, &ifindex); threadkey = strdup(req->threadkey); worker = virThreadPoolNew(1, 1, 0, virNWFilterDHCPDecodeWorker, req); } /* let creator know how well we initialized */ if (error == true || !threadkey || tmp < 0 || !worker || ifindex != req->ifindex) req->threadStatus = THREAD_STATUS_FAIL; else req->threadStatus = THREAD_STATUS_OK; virCondSignal(&req->threadStatusCond); virNWFilterSnoopReqUnlock(req); if (req->threadStatus != THREAD_STATUS_OK) goto exit; while (!error) { if (virNWFilterSnoopAdjustPoll(pcapConf, ARRAY_CARDINALITY(pcapConf), fds, &pollTo) < 0) { break; } n = poll(fds, ARRAY_CARDINALITY(fds), pollTo); if (n < 0) { if (errno != EAGAIN && errno != EINTR) error = true; } virNWFilterSnoopReqLeaseTimerRun(req); /* * Check whether we were cancelled or whether * a previously submitted job failed. */ if (!virNWFilterSnoopIsActive(threadkey) || req->jobCompletionStatus != 0) goto exit; for (i = 0; n > 0 && i < ARRAY_CARDINALITY(fds); i++) { if (!fds[i].revents) continue; fds[i].revents = 0; n--; rv = pcap_next_ex(pcapConf[i].handle, &hdr, (const u_char **)&packet); if (rv < 0) { /* error reading from socket */ tmp = -1; /* protect req->ifname */ virNWFilterSnoopReqLock(req); if (req->ifname) tmp = virNetDevValidateConfig(req->ifname, NULL, ifindex); virNWFilterSnoopReqUnlock(req); if (tmp <= 0) { error = true; break; } if (++errcount > PCAP_READ_MAXERRS) { pcap_close(pcapConf[i].handle); pcapConf[i].handle = NULL; /* protect req->ifname */ virNWFilterSnoopReqLock(req); virReportError(VIR_ERR_INTERNAL_ERROR, _("interface '%s' failing; " "reopening"), req->ifname); if (req->ifname) pcapConf[i].handle = virNWFilterSnoopDHCPOpen(req->ifname, &req->macaddr, pcapConf[i].filter, pcapConf[i].dir); virNWFilterSnoopReqUnlock(req); if (!pcapConf[i].handle) { error = true; break; } } continue; } errcount = 0; if (rv) { unsigned int diff; /* submit packet to worker thread */ if (virAtomicIntGet(&pcapConf[i].qCtr) > pcapConf[i].maxQSize) { if (last_displayed_queue - time(0) > 10) { last_displayed_queue = time(0); VIR_WARN("Worker thread for interface '%s' has a " "job queue that is too long\n", req->ifname); } continue; } diff = virNWFilterSnoopRateLimit(&pcapConf[i].rateLimit); if (diff > 0) { virNWFilterSnoopRatePenalty(&pcapConf[i], diff, DHCP_PKT_RATE); /* rate-limited warnings */ if (time(0) - last_displayed > 10) { last_displayed = time(0); VIR_WARN("Too many DHCP packets on interface '%s'", req->ifname); } continue; } if (virNWFilterSnoopDHCPDecodeJobSubmit(worker, packet, hdr->caplen, pcapConf[i].dir, &pcapConf[i].qCtr) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("Job submission failed on " "interface '%s'"), req->ifname); error = true; break; } } } /* for all fds */ } /* while (!error) */ /* protect IfNameToKey */ virNWFilterSnoopLock(); /* protect req->ifname & req->threadkey */ virNWFilterSnoopReqLock(req); virNWFilterSnoopCancel(&req->threadkey); ignore_value(virHashRemoveEntry(virNWFilterSnoopState.ifnameToKey, req->ifname)); VIR_FREE(req->ifname); virNWFilterSnoopReqUnlock(req); virNWFilterSnoopUnlock(); exit: virThreadPoolFree(worker); virNWFilterSnoopReqPut(req); VIR_FREE(threadkey); for (i = 0; i < ARRAY_CARDINALITY(pcapConf); i++) { if (pcapConf[i].handle) pcap_close(pcapConf[i].handle); } virAtomicIntDecAndTest(&virNWFilterSnoopState.nThreads); return; } static void virNWFilterSnoopIFKeyFMT(char *ifkey, const unsigned char *vmuuid, const virMacAddrPtr macaddr) { virUUIDFormat(vmuuid, ifkey); ifkey[VIR_UUID_STRING_BUFLEN - 1] = '-'; virMacAddrFormat(macaddr, ifkey + VIR_UUID_STRING_BUFLEN); } int virNWFilterDHCPSnoopReq(virNWFilterTechDriverPtr techdriver, const char *ifname, const char *linkdev, enum virDomainNetType nettype, const unsigned char *vmuuid, const virMacAddrPtr macaddr, const char *filtername, virNWFilterHashTablePtr filterparams, virNWFilterDriverStatePtr driver) { virNWFilterSnoopReqPtr req; bool isnewreq; char ifkey[VIR_IFKEY_LEN]; int tmp; virThread thread; virNWFilterVarValuePtr dhcpsrvrs; virNWFilterSnoopIFKeyFMT(ifkey, vmuuid, macaddr); req = virNWFilterSnoopReqGetByIFKey(ifkey); isnewreq = (req == NULL); if (!isnewreq) { if (req->threadkey) { virNWFilterSnoopReqPut(req); return 0; } /* a recycled req may still have filtername and vars */ VIR_FREE(req->filtername); virNWFilterHashTableFree(req->vars); } else { req = virNWFilterSnoopReqNew(ifkey); if (!req) return -1; } req->driver = driver; req->techdriver = techdriver; tmp = virNetDevGetIndex(ifname, &req->ifindex); req->linkdev = linkdev ? strdup(linkdev) : NULL; req->nettype = nettype; req->ifname = strdup(ifname); virMacAddrSet(&req->macaddr, macaddr); req->filtername = strdup(filtername); req->vars = virNWFilterHashTableCreate(0); if (!req->ifname || !req->filtername || !req->vars || tmp < 0 || (linkdev != NULL && req->linkdev == NULL)) { virReportOOMError(); goto exit_snoopreqput; } /* check that all tools are available for applying the filters (late) */ if ( !techdriver->canApplyBasicRules()) { virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s", _("IP parameter must be provided since " "snooping the IP address does not work " "possibly due to missing tools")); goto exit_snoopreqput; } dhcpsrvrs = virHashLookup(filterparams->hashTable, NWFILTER_VARNAME_DHCPSERVER); if (techdriver->applyDHCPOnlyRules(req->ifname, &req->macaddr, dhcpsrvrs, false) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("applyDHCPOnlyRules " "failed - spoofing not protected!")); goto exit_snoopreqput; } if (virNWFilterHashTablePutAll(filterparams, req->vars) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterDHCPSnoopReq: can't copy variables" " on if %s"), ifkey); goto exit_snoopreqput; } virNWFilterSnoopLock(); if (virHashAddEntry(virNWFilterSnoopState.ifnameToKey, ifname, req->ifkey) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterDHCPSnoopReq ifname map failed" " on interface \"%s\" key \"%s\""), ifname, ifkey); goto exit_snoopunlock; } if (isnewreq && virHashAddEntry(virNWFilterSnoopState.snoopReqs, ifkey, req) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterDHCPSnoopReq req add failed on" " interface \"%s\" ifkey \"%s\""), ifname, ifkey); goto exit_rem_ifnametokey; } /* prevent thread from holding req */ virNWFilterSnoopReqLock(req); if (virThreadCreate(&thread, false, virNWFilterDHCPSnoopThread, req) != 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterDHCPSnoopReq virThreadCreate " "failed on interface '%s'"), ifname); goto exit_snoopreq_unlock; } virAtomicIntInc(&virNWFilterSnoopState.nThreads); req->threadkey = virNWFilterSnoopActivate(req); if (!req->threadkey) { virReportError(VIR_ERR_INTERNAL_ERROR, _("Activation of snoop request failed on " "interface '%s'"), req->ifname); goto exit_snoopreq_unlock; } if (virNWFilterSnoopReqRestore(req) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("Restoring of leases failed on " "interface '%s'"), req->ifname); goto exit_snoop_cancel; } /* sync with thread */ if (virCondWait(&req->threadStatusCond, &req->lock) < 0 || req->threadStatus != THREAD_STATUS_OK) goto exit_snoop_cancel; virNWFilterSnoopReqUnlock(req); virNWFilterSnoopUnlock(); /* do not 'put' the req -- the thread will do this */ return 0; exit_snoop_cancel: virNWFilterSnoopCancel(&req->threadkey); exit_snoopreq_unlock: virNWFilterSnoopReqUnlock(req); exit_rem_ifnametokey: virHashRemoveEntry(virNWFilterSnoopState.ifnameToKey, ifname); exit_snoopunlock: virNWFilterSnoopUnlock(); exit_snoopreqput: virNWFilterSnoopReqPut(req); return -1; } static void virNWFilterSnoopLeaseFileClose(void) { VIR_FORCE_CLOSE(virNWFilterSnoopState.leaseFD); } static void virNWFilterSnoopLeaseFileOpen(void) { virNWFilterSnoopLeaseFileClose(); virNWFilterSnoopState.leaseFD = open(LEASEFILE, O_CREAT|O_RDWR|O_APPEND, 0644); } /* * Write a single lease to the given file. * */ static int virNWFilterSnoopLeaseFileWrite(int lfd, const char *ifkey, virNWFilterSnoopIPLeasePtr ipl) { char *lbuf = NULL; char *ipstr, *dhcpstr; int len; int ret = 0; ipstr = virSocketAddrFormat(&ipl->ipAddress); dhcpstr = virSocketAddrFormat(&ipl->ipServer); if (!dhcpstr || !ipstr) { ret = -1; goto cleanup; } /* time intf ip dhcpserver */ len = virAsprintf(&lbuf, "%u %s %s %s\n", ipl->timeout, ifkey, ipstr, dhcpstr); if (len < 0) { virReportOOMError(); ret = -1; goto cleanup; } if (safewrite(lfd, lbuf, len) != len) { virReportSystemError(errno, "%s", _("lease file write failed")); ret = -1; goto cleanup; } ignore_value(fsync(lfd)); cleanup: VIR_FREE(lbuf); VIR_FREE(dhcpstr); VIR_FREE(ipstr); return ret; } /* * Append a single lease to the end of the lease file. * To keep a limited number of dead leases, re-read the lease * file if the threshold of active leases versus written ones * exceeds a threshold. */ static void virNWFilterSnoopLeaseFileSave(virNWFilterSnoopIPLeasePtr ipl) { virNWFilterSnoopReqPtr req = ipl->snoopReq; virNWFilterSnoopLock(); if (virNWFilterSnoopState.leaseFD < 0) virNWFilterSnoopLeaseFileOpen(); if (virNWFilterSnoopLeaseFileWrite(virNWFilterSnoopState.leaseFD, req->ifkey, ipl) < 0) goto err_exit; /* keep dead leases at < ~95% of file size */ if (virAtomicIntInc(&virNWFilterSnoopState.wLeases) >= virAtomicIntGet(&virNWFilterSnoopState.nLeases) * 20) virNWFilterSnoopLeaseFileLoad(); /* load & refresh lease file */ err_exit: virNWFilterSnoopUnlock(); } /* * Have requests removed that have no leases. * Remove all expired leases. * Call this function with the SnoopLock held. */ static int virNWFilterSnoopPruneIter(const void *payload, const void *name ATTRIBUTE_UNUSED, const void *data ATTRIBUTE_UNUSED) { const virNWFilterSnoopReqPtr req = (virNWFilterSnoopReqPtr)payload; bool del_req; /* clean up orphaned, expired leases */ /* protect req->threadkey */ virNWFilterSnoopReqLock(req); if (!req->threadkey) virNWFilterSnoopReqLeaseTimerRun(req); /* * have the entry removed if it has no leases and no one holds a ref */ del_req = ((req->start == NULL) && (virAtomicIntGet(&req->refctr) == 0)); virNWFilterSnoopReqUnlock(req); return del_req; } /* * Iterator to write all leases of a single request to a file. * Call this function with the SnoopLock held. */ static void virNWFilterSnoopSaveIter(void *payload, const void *name ATTRIBUTE_UNUSED, void *data) { virNWFilterSnoopReqPtr req = payload; int tfd = *(int *)data; virNWFilterSnoopIPLeasePtr ipl; /* protect req->start */ virNWFilterSnoopReqLock(req); for (ipl = req->start; ipl; ipl = ipl->next) ignore_value(virNWFilterSnoopLeaseFileWrite(tfd, req->ifkey, ipl)); virNWFilterSnoopReqUnlock(req); } /* * Write all valid leases into a temporary file and then * rename the file to the final file. * Call this function with the SnoopLock held. */ static void virNWFilterSnoopLeaseFileRefresh(void) { int tfd; if (unlink(TMPLEASEFILE) < 0 && errno != ENOENT) virReportSystemError(errno, _("unlink(\"%s\")"), TMPLEASEFILE); /* lease file loaded, delete old one */ tfd = open(TMPLEASEFILE, O_CREAT|O_RDWR|O_TRUNC|O_EXCL, 0644); if (tfd < 0) { virReportSystemError(errno, _("open(\"%s\")"), TMPLEASEFILE); return; } if (virNWFilterSnoopState.snoopReqs) { /* clean up the requests */ virHashRemoveSet(virNWFilterSnoopState.snoopReqs, virNWFilterSnoopPruneIter, NULL); /* now save them */ virHashForEach(virNWFilterSnoopState.snoopReqs, virNWFilterSnoopSaveIter, (void *)&tfd); } if (VIR_CLOSE(tfd) < 0) { virReportSystemError(errno, _("unable to close %s"), TMPLEASEFILE); /* assuming the old lease file is still better, skip the renaming */ goto skip_rename; } if (rename(TMPLEASEFILE, LEASEFILE) < 0) { virReportSystemError(errno, _("rename(\"%s\", \"%s\")"), TMPLEASEFILE, LEASEFILE); ignore_value(unlink(TMPLEASEFILE)); } virAtomicIntSet(&virNWFilterSnoopState.wLeases, 0); skip_rename: virNWFilterSnoopLeaseFileOpen(); } static void virNWFilterSnoopLeaseFileLoad(void) { char line[256], ifkey[VIR_IFKEY_LEN]; char ipstr[INET_ADDRSTRLEN], srvstr[INET_ADDRSTRLEN]; virNWFilterSnoopIPLease ipl; virNWFilterSnoopReqPtr req; time_t now; FILE *fp; int ln = 0, tmp; /* protect the lease file */ virNWFilterSnoopLock(); fp = fopen(LEASEFILE, "r"); time(&now); while (fp && fgets(line, sizeof(line), fp)) { if (line[strlen(line)-1] != '\n') { virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterSnoopLeaseFileLoad lease file " "line %d corrupt"), ln); break; } ln++; /* key len 55 = "VMUUID"+'-'+"MAC" */ if (sscanf(line, "%u %55s %16s %16s", &ipl.timeout, ifkey, ipstr, srvstr) < 4) { virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterSnoopLeaseFileLoad lease file " "line %d corrupt"), ln); break; } if (ipl.timeout && ipl.timeout < now) continue; req = virNWFilterSnoopReqGetByIFKey(ifkey); if (!req) { req = virNWFilterSnoopReqNew(ifkey); if (!req) break; tmp = virHashAddEntry(virNWFilterSnoopState.snoopReqs, ifkey, req); if (tmp < 0) { virNWFilterSnoopReqPut(req); virReportError(VIR_ERR_INTERNAL_ERROR, _("virNWFilterSnoopLeaseFileLoad req add" " failed on interface \"%s\""), ifkey); continue; } } if (virSocketAddrParseIPv4(&ipl.ipAddress, ipstr) < 0) { virReportError(VIR_ERR_INTERNAL_ERROR, _("line %d corrupt ipaddr \"%s\""), ln, ipstr); virNWFilterSnoopReqPut(req); continue; } ignore_value(virSocketAddrParseIPv4(&ipl.ipServer, srvstr)); ipl.snoopReq = req; if (ipl.timeout) virNWFilterSnoopReqLeaseAdd(req, &ipl, false); else virNWFilterSnoopReqLeaseDel(req, &ipl.ipAddress, false, false); virNWFilterSnoopReqPut(req); } VIR_FORCE_FCLOSE(fp); virNWFilterSnoopLeaseFileRefresh(); virNWFilterSnoopUnlock(); } /* * Wait until all threads have ended. */ static void virNWFilterSnoopJoinThreads(void) { while (virAtomicIntGet(&virNWFilterSnoopState.nThreads) != 0) { VIR_WARN("Waiting for snooping threads to terminate: %u\n", virAtomicIntGet(&virNWFilterSnoopState.nThreads)); usleep(1000 * 1000); } } /* * Iterator to remove a request, repeatedly called on one * request after another. * The requests' ifname is freed allowing for an association * of the Snoop request's leases with the same VM under a * different interface name at a later time. */ static int virNWFilterSnoopRemAllReqIter(const void *payload, const void *name ATTRIBUTE_UNUSED, const void *data ATTRIBUTE_UNUSED) { const virNWFilterSnoopReqPtr req = (virNWFilterSnoopReqPtr)payload; /* protect req->ifname */ virNWFilterSnoopReqLock(req); if (req->ifname) { ignore_value(virHashRemoveEntry(virNWFilterSnoopState.ifnameToKey, req->ifname)); /* * Remove all IP addresses known to be associated with this * interface so that a new thread will be started on this * interface */ virNWFilterIPAddrMapDelIPAddr(req->ifname, NULL); VIR_FREE(req->ifname); } virNWFilterSnoopReqUnlock(req); /* removal will call virNWFilterSnoopCancel() */ return 1; } /* * Terminate all threads; keep the SnoopReqs hash allocated */ static void virNWFilterSnoopEndThreads(void) { virNWFilterSnoopLock(); virHashRemoveSet(virNWFilterSnoopState.snoopReqs, virNWFilterSnoopRemAllReqIter, NULL); virNWFilterSnoopUnlock(); } int virNWFilterDHCPSnoopInit(void) { if (virNWFilterSnoopState.snoopReqs) return 0; VIR_DEBUG("Initializing DHCP snooping"); if (virMutexInitRecursive(&virNWFilterSnoopState.snoopLock) < 0 || virMutexInit(&virNWFilterSnoopState.activeLock) < 0) return -1; virNWFilterSnoopState.ifnameToKey = virHashCreate(0, NULL); virNWFilterSnoopState.active = virHashCreate(0, NULL); virNWFilterSnoopState.snoopReqs = virHashCreate(0, virNWFilterSnoopReqRelease); if (!virNWFilterSnoopState.ifnameToKey || !virNWFilterSnoopState.snoopReqs || !virNWFilterSnoopState.active) { virReportOOMError(); goto err_exit; } virNWFilterSnoopLeaseFileLoad(); virNWFilterSnoopLeaseFileOpen(); return 0; err_exit: virHashFree(virNWFilterSnoopState.ifnameToKey); virNWFilterSnoopState.ifnameToKey = NULL; virHashFree(virNWFilterSnoopState.snoopReqs); virNWFilterSnoopState.snoopReqs = NULL; virHashFree(virNWFilterSnoopState.active); virNWFilterSnoopState.active = NULL; return -1; } void virNWFilterDHCPSnoopEnd(const char *ifname) { char *ifkey = NULL; virNWFilterSnoopLock(); if (!virNWFilterSnoopState.snoopReqs) goto cleanup; if (ifname) { ifkey = (char *)virHashLookup(virNWFilterSnoopState.ifnameToKey, ifname); if (!ifkey) { virReportError(VIR_ERR_INTERNAL_ERROR, _("ifname \"%s\" not in key map"), ifname); goto cleanup; } ignore_value(virHashRemoveEntry(virNWFilterSnoopState.ifnameToKey, ifname)); } if (ifkey) { virNWFilterSnoopReqPtr req; req = virNWFilterSnoopReqGetByIFKey(ifkey); if (!req) { virReportError(VIR_ERR_INTERNAL_ERROR, _("ifkey \"%s\" has no req"), ifkey); goto cleanup; } /* protect req->ifname & req->threadkey */ virNWFilterSnoopReqLock(req); /* keep valid lease req; drop interface association */ virNWFilterSnoopCancel(&req->threadkey); VIR_FREE(req->ifname); virNWFilterSnoopReqUnlock(req); virNWFilterSnoopReqPut(req); } else { /* free all of them */ virNWFilterSnoopLeaseFileClose(); virHashRemoveAll(virNWFilterSnoopState.ifnameToKey); /* tell the threads to terminate */ virNWFilterSnoopEndThreads(); virNWFilterSnoopLeaseFileLoad(); } cleanup: virNWFilterSnoopUnlock(); } void virNWFilterDHCPSnoopShutdown(void) { virNWFilterSnoopEndThreads(); virNWFilterSnoopJoinThreads(); virNWFilterSnoopLock(); virNWFilterSnoopLeaseFileClose(); virHashFree(virNWFilterSnoopState.ifnameToKey); virHashFree(virNWFilterSnoopState.snoopReqs); virNWFilterSnoopUnlock(); virNWFilterSnoopActiveLock(); virHashFree(virNWFilterSnoopState.active); virNWFilterSnoopActiveUnlock(); } #else /* HAVE_LIBPCAP */ int virNWFilterDHCPSnoopInit(void) { VIR_DEBUG("No DHCP snooping support available"); return 0; } void virNWFilterDHCPSnoopEnd(const char *ifname ATTRIBUTE_UNUSED) { return; } void virNWFilterDHCPSnoopShutdown(void) { return; } int virNWFilterDHCPSnoopReq(virNWFilterTechDriverPtr techdriver ATTRIBUTE_UNUSED, const char *ifname ATTRIBUTE_UNUSED, const char *linkdev ATTRIBUTE_UNUSED, enum virDomainNetType nettype ATTRIBUTE_UNUSED, const unsigned char *vmuuid ATTRIBUTE_UNUSED, const virMacAddrPtr macaddr ATTRIBUTE_UNUSED, const char *filtername ATTRIBUTE_UNUSED, virNWFilterHashTablePtr filterparams ATTRIBUTE_UNUSED, virNWFilterDriverStatePtr driver ATTRIBUTE_UNUSED) { virReportError(VIR_ERR_INTERNAL_ERROR, _("libvirt was not compiled with libpcap and \"" NWFILTER_VARNAME_CTRL_IP_LEARNING "='dhcp'\" requires it.")); return -1; } #endif /* HAVE_LIBPCAP */