[libvirt] [PATCHv3 0/3] network: resolve CVE 2012-3411

(This obsoletes the V2 patches I sent yesterday: https://www.redhat.com/archives/libvir-list/2012-November/msg01216.html ) This patch series resolves the libvirt part of CVE 2012-3411: https://bugzilla.redhat.com/show_bug.cgi?id=833033 Further details are in PATCH 3/3. The changes from V1 to V3: (resulting from Doug Goldstein's review, and a comment in the BZ record from the CVE reporter, David Woodhouse) 1) rework dnsmasqCapsRefresh() to create a new caps object if it's given a NULL object (function now gets dnsmasqCapsPtr* instead of dnsmasCapsPtr). This makes it possible to recover properly if dnsmasq is installed after libvirtd has already been started. 2) Add the following before each run of dnsmasq: virCommandAddEnvPassCommon(cmd); virCommandClearCaps(cmd); 3) Fixed a missing space after comma :-) 4) remove empty if () { } around initial call to dnsmasqCapsNewFromBinary() in bridge_driver.c 5) include FEC0::/10 as a "local" range when checking for private addresses to allow in the absence of an updated dnsmasq.

In order to optionally take advantage of new features in dnsmasq when the host's version of dnsmasq supports them, but still be able to run on hosts that don't support the new features, we need to be able to detect the version of dnsmasq running on the host, and possibly determine from the help output what options are in this dnsmasq. This patch implements a greatly simplified version of the capabilities code we already have for qemu. A dnsmasqCaps device can be created and populated either from running a program on disk, reading a file with the concatenated output of "dnsmasq --version; dnsmasq --help", or examining a buffer in memory that contains the concatenated output of those two commands. Simple functions to retrieve capabilities flags, the version number, and the path of the binary are also included. bridge_driver.c creates a single dnsmasqCaps object at driver startup, and disposes of it at driver shutdown. Any time it must be used, the dnsmasqCapsRefresh method is called - it checks the mtime of the binary, and re-runs the checks if the binary has changed. networkxml2argvtest.c creates 2 "artificial" dnsmasqCaps objects at startup - one "restricted" (doesn't support --bind-dynamic) and one "full" (does support --bind-dynamic). Some of the test cases use one and some the other, to make sure both code pathes are tested. --- src/libvirt_private.syms | 8 +- src/network/bridge_driver.c | 58 +++++---- src/network/bridge_driver.h | 7 +- src/util/dnsmasq.c | 281 ++++++++++++++++++++++++++++++++++++++++++++ src/util/dnsmasq.h | 22 +++- tests/networkxml2argvtest.c | 57 ++++++--- 6 files changed, 388 insertions(+), 45 deletions(-) diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index 2573b8a..f57ee1c 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -246,13 +246,19 @@ virDevicePCIAddressParseXML; # dnsmasq.h dnsmasqAddDhcpHost; dnsmasqAddHost; +dnsmasqCapsGet; +dnsmasqCapsGetBinaryPath; +dnsmasqCapsGetVersion; +dnsmasqCapsNewFromBuffer; +dnsmasqCapsNewFromFile; +dnsmasqCapsNewFromBinary; +dnsmasqCapsRefresh; dnsmasqContextFree; dnsmasqContextNew; dnsmasqDelete; dnsmasqReload; dnsmasqSave; - # domain_audit.h virDomainAuditCgroup; virDomainAuditCgroupMajor; diff --git a/src/network/bridge_driver.c b/src/network/bridge_driver.c index fb167dc..eab8ffe 100644 --- a/src/network/bridge_driver.c +++ b/src/network/bridge_driver.c @@ -85,6 +85,7 @@ struct network_driver { char *networkConfigDir; char *networkAutostartDir; char *logDir; + dnsmasqCapsPtr dnsmasqCaps; }; @@ -271,7 +272,8 @@ networkFindActiveConfigs(struct network_driver *driver) { char *radvdpidbase; ignore_value(virPidFileReadIfAlive(NETWORK_PID_DIR, obj->def->name, - &obj->dnsmasqPid, DNSMASQ)); + &obj->dnsmasqPid, + dnsmasqCapsGetBinaryPath(driver->dnsmasqCaps))); if (!(radvdpidbase = networkRadvdPidfileBasename(obj->def->name))) { virReportOOMError(); @@ -389,6 +391,8 @@ networkStartup(int privileged) { goto out_of_memory; } + /* if this fails now, it will be retried later with dnsmasqCapsRefresh() */ + driverState->dnsmasqCaps = dnsmasqCapsNewFromBinary(DNSMASQ); if (virNetworkLoadAllConfigs(&driverState->networks, driverState->networkConfigDir, @@ -514,6 +518,8 @@ networkShutdown(void) { if (driverState->iptables) iptablesContextFree(driverState->iptables); + virObjectUnref(driverState->dnsmasqCaps); + networkDriverUnlock(driverState); virMutexDestroy(&driverState->lock); @@ -616,7 +622,8 @@ networkBuildDnsmasqArgv(virNetworkObjPtr network, virNetworkIpDefPtr ipdef, const char *pidfile, virCommandPtr cmd, - dnsmasqContext *dctx) + dnsmasqContext *dctx, + dnsmasqCapsPtr caps ATTRIBUTE_UNUSED) { int r, ret = -1; int nbleases = 0; @@ -848,7 +855,8 @@ cleanup: int networkBuildDhcpDaemonCommandLine(virNetworkObjPtr network, virCommandPtr *cmdout, - char *pidfile, dnsmasqContext *dctx) + char *pidfile, dnsmasqContext *dctx, + dnsmasqCapsPtr caps) { virCommandPtr cmd = NULL; int ret = -1, ii; @@ -876,8 +884,8 @@ networkBuildDhcpDaemonCommandLine(virNetworkObjPtr network, virCommandPtr *cmdou if (!virNetworkDefGetIpByIndex(network->def, AF_UNSPEC, 0)) return 0; - cmd = virCommandNew(DNSMASQ); - if (networkBuildDnsmasqArgv(network, ipdef, pidfile, cmd, dctx) < 0) { + cmd = virCommandNew(dnsmasqCapsGetBinaryPath(caps)); + if (networkBuildDnsmasqArgv(network, ipdef, pidfile, cmd, dctx, caps) < 0) { goto cleanup; } @@ -891,7 +899,8 @@ cleanup: } static int -networkStartDhcpDaemon(virNetworkObjPtr network) +networkStartDhcpDaemon(struct network_driver *driver, + virNetworkObjPtr network) { virCommandPtr cmd = NULL; char *pidfile = NULL; @@ -935,7 +944,9 @@ networkStartDhcpDaemon(virNetworkObjPtr network) if (dctx == NULL) goto cleanup; - ret = networkBuildDhcpDaemonCommandLine(network, &cmd, pidfile, dctx); + dnsmasqCapsRefresh(&driver->dnsmasqCaps, false); + + ret = networkBuildDhcpDaemonCommandLine(network, &cmd, pidfile, dctx, driver->dnsmasqCaps); if (ret < 0) goto cleanup; @@ -988,7 +999,8 @@ cleanup: * Returns 0 on success, -1 on failure. */ static int -networkRefreshDhcpDaemon(virNetworkObjPtr network) +networkRefreshDhcpDaemon(struct network_driver *driver, + virNetworkObjPtr network) { int ret = -1, ii; virNetworkIpDefPtr ipdef; @@ -996,7 +1008,7 @@ networkRefreshDhcpDaemon(virNetworkObjPtr network) /* if there's no running dnsmasq, just start it */ if (network->dnsmasqPid <= 0 || (kill(network->dnsmasqPid, 0) < 0)) - return networkStartDhcpDaemon(network); + return networkStartDhcpDaemon(driver, network); /* Look for first IPv4 address that has dhcp defined. */ /* We support dhcp config on 1 IPv4 interface only. */ @@ -1038,7 +1050,8 @@ cleanup: * Returns 0 on success, -1 on failure. */ static int -networkRestartDhcpDaemon(virNetworkObjPtr network) +networkRestartDhcpDaemon(struct network_driver *driver, + virNetworkObjPtr network) { /* if there is a running dnsmasq, kill it */ if (network->dnsmasqPid > 0) { @@ -1047,7 +1060,7 @@ networkRestartDhcpDaemon(virNetworkObjPtr network) network->dnsmasqPid = -1; } /* now start dnsmasq if it should be started */ - return networkStartDhcpDaemon(network); + return networkStartDhcpDaemon(driver, network); } static int @@ -1245,7 +1258,8 @@ cleanup: } static int -networkRefreshRadvd(virNetworkObjPtr network) +networkRefreshRadvd(struct network_driver *driver ATTRIBUTE_UNUSED, + virNetworkObjPtr network) { /* if there's no running radvd, just start it */ if (network->radvdPid <= 0 || (kill(network->radvdPid, 0) < 0)) @@ -1265,7 +1279,8 @@ networkRefreshRadvd(virNetworkObjPtr network) #if 0 /* currently unused, so it causes a build error unless we #if it out */ static int -networkRestartRadvd(virNetworkObjPtr network) +networkRestartRadvd(struct network_driver *driver, + virNetworkObjPtr network) { char *radvdpidbase; @@ -1313,8 +1328,8 @@ networkRefreshDaemons(struct network_driver *driver) * dnsmasq and/or radvd, or restart them if they've * disappeared. */ - networkRefreshDhcpDaemon(network); - networkRefreshRadvd(network); + networkRefreshDhcpDaemon(driver, network); + networkRefreshRadvd(driver, network); } virNetworkObjUnlock(network); } @@ -2224,7 +2239,8 @@ networkStartNetworkVirtual(struct network_driver *driver, /* start dnsmasq if there are any IP addresses (v4 or v6) */ - if ((v4present || v6present) && networkStartDhcpDaemon(network) < 0) + if ((v4present || v6present) && + networkStartDhcpDaemon(driver, network) < 0) goto err3; /* start radvd if there are any ipv6 addresses */ @@ -2988,7 +3004,7 @@ networkUpdate(virNetworkPtr net, /* these sections all change things on the dnsmasq commandline, * so we need to kill and restart dnsmasq. */ - if (networkRestartDhcpDaemon(network) < 0) + if (networkRestartDhcpDaemon(driver, network) < 0) goto cleanup; } else if (section == VIR_NETWORK_SECTION_IP_DHCP_HOST) { @@ -3009,8 +3025,8 @@ networkUpdate(virNetworkPtr net, } if ((newDhcpActive != oldDhcpActive && - networkRestartDhcpDaemon(network) < 0) || - networkRefreshDhcpDaemon(network) < 0) { + networkRestartDhcpDaemon(driver, network) < 0) || + networkRefreshDhcpDaemon(driver, network) < 0) { goto cleanup; } @@ -3021,7 +3037,7 @@ networkUpdate(virNetworkPtr net, * can just update the config files and send SIGHUP to * dnsmasq. */ - if (networkRefreshDhcpDaemon(network) < 0) + if (networkRefreshDhcpDaemon(driver, network) < 0) goto cleanup; } @@ -3030,7 +3046,7 @@ networkUpdate(virNetworkPtr net, /* only a change in IP addresses will affect radvd, and all of radvd's * config is stored in the conf file which will be re-read with a SIGHUP. */ - if (networkRefreshRadvd(network) < 0) + if (networkRefreshRadvd(driver, network) < 0) goto cleanup; } diff --git a/src/network/bridge_driver.h b/src/network/bridge_driver.h index 638a6ac..8c16bdd 100644 --- a/src/network/bridge_driver.h +++ b/src/network/bridge_driver.h @@ -1,7 +1,7 @@ /* * network_driver.h: core driver methods for managing networks * - * Copyright (C) 2006, 2007, 2011 Red Hat, Inc. + * Copyright (C) 2006-2012 Red Hat, Inc. * Copyright (C) 2006 Daniel P. Berrange * * This library is free software; you can redistribute it and/or @@ -48,7 +48,8 @@ int networkGetNetworkAddress(const char *netname, char **netaddr) int networkBuildDhcpDaemonCommandLine(virNetworkObjPtr network, virCommandPtr *cmdout, char *pidfile, - dnsmasqContext *dctx) + dnsmasqContext *dctx, + dnsmasqCapsPtr caps) ; # else /* Define no-op replacements that don't drag in any link dependencies. */ @@ -56,7 +57,7 @@ int networkBuildDhcpDaemonCommandLine(virNetworkObjPtr network, # define networkNotifyActualDevice(iface) (iface=iface, 0) # define networkReleaseActualDevice(iface) (iface=iface, 0) # define networkGetNetworkAddress(netname, netaddr) (-2) -# define networkBuildDhcpDaemonCommandLine(network, cmdout, pidfile, dctx) 0 +# define networkBuildDhcpDaemonCommandLine(network, cmdout, pidfile, dctx, caps) 0 # endif typedef char *(*networkDnsmasqLeaseFileNameFunc)(const char *netname); diff --git a/src/util/dnsmasq.c b/src/util/dnsmasq.c index 9d1c07b..501ee68 100644 --- a/src/util/dnsmasq.c +++ b/src/util/dnsmasq.c @@ -39,8 +39,10 @@ #include "internal.h" #include "datatypes.h" +#include "bitmap.h" #include "dnsmasq.h" #include "util.h" +#include "command.h" #include "memory.h" #include "virterror_internal.h" #include "logging.h" @@ -583,3 +585,282 @@ dnsmasqReload(pid_t pid ATTRIBUTE_UNUSED) return 0; } + +/* + * dnsmasqCapabilities functions - provide useful information about the + * version of dnsmasq on this machine. + * + */ +struct _dnsmasqCaps { + virObject object; + char *binaryPath; + bool noRefresh; + time_t mtime; + virBitmapPtr flags; + unsigned long version; +}; + +static virClassPtr dnsmasqCapsClass; + +static void +dnsmasqCapsDispose(void *obj) +{ + dnsmasqCapsPtr caps = obj; + + virBitmapFree(caps->flags); + VIR_FREE(caps->binaryPath); +} + +static int dnsmasqCapsOnceInit(void) +{ + if (!(dnsmasqCapsClass = virClassNew("dnsmasqCaps", + sizeof(dnsmasqCaps), + dnsmasqCapsDispose))) { + return -1; + } + + return 0; +} + +VIR_ONCE_GLOBAL_INIT(dnsmasqCaps) + +static void +dnsmasqCapsSet(dnsmasqCapsPtr caps, + dnsmasqCapsFlags flag) +{ + ignore_value(virBitmapSetBit(caps->flags, flag)); +} + + +#define SKIP_BLANKS(p) do { while ((*(p) == ' ') || (*(p) == '\t')) (p)++; } while (0) +#define DNSMASQ_VERSION_STR "Dnsmasq version " + +static int +dnsmasqCapsSetFromBuffer(dnsmasqCapsPtr caps, const char *buf) +{ + const char *p; + + caps->noRefresh = true; + + p = buf; + if (!STRPREFIX(p, DNSMASQ_VERSION_STR)) + goto fail; + p += strlen(DNSMASQ_VERSION_STR); + SKIP_BLANKS(p); + if (virParseVersionString(p, &caps->version, true) < 0) + goto fail; + + if (strstr(buf, "--bind-dynamic")) + dnsmasqCapsSet(caps, DNSMASQ_CAPS_BIND_DYNAMIC); + + VIR_INFO("dnsmasq version is %d.%d, --bind-dynamic is %s", + (int)caps->version / 1000000, (int)(caps->version % 1000000) / 1000, + dnsmasqCapsGet(caps, DNSMASQ_CAPS_BIND_DYNAMIC) + ? "present" : "NOT present"); + return 0; + +fail: + p = strchr(buf, '\n'); + if (!p) + p = strchr(buf, '\0'); + + virReportError(VIR_ERR_INTERNAL_ERROR, + _("cannot parse %s version number in '%.*s'"), + caps->binaryPath, (int) (p - buf), buf); + return -1; + +} + +static int +dnsmasqCapsSetFromFile(dnsmasqCapsPtr caps, const char *path) +{ + int ret = -1; + char *buf = NULL; + + if (virFileReadAll(path, 1024 * 1024, &buf) < 0) + goto cleanup; + + ret = dnsmasqCapsSetFromBuffer(caps, buf); + +cleanup: + VIR_FREE(buf); + return ret; +} + +static int +dnsmasqCapsRefreshInternal(dnsmasqCapsPtr caps, bool force) +{ + int ret = -1; + struct stat sb; + virCommandPtr cmd = NULL; + char *help = NULL, *version = NULL, *complete = NULL; + + if (!caps || caps->noRefresh) + return 0; + + if (stat(caps->binaryPath, &sb) < 0) { + virReportSystemError(errno, _("Cannot check dnsmasq binary %s"), + caps->binaryPath); + return -1; + } + if (!force && caps->mtime == sb.st_mtime) { + return 0; + } + caps->mtime = sb.st_mtime; + + /* Make sure the binary we are about to try exec'ing exists. + * Technically we could catch the exec() failure, but that's + * in a sub-process so it's hard to feed back a useful error. + */ + if (!virFileIsExecutable(caps->binaryPath)) { + virReportSystemError(errno, _("dnsmasq binary %s is not executable"), + caps->binaryPath); + goto cleanup; + } + + cmd = virCommandNewArgList(caps->binaryPath, "--version", NULL); + virCommandSetOutputBuffer(cmd, &version); + virCommandSetErrorBuffer(cmd, &version); + virCommandAddEnvPassCommon(cmd); + virCommandClearCaps(cmd); + if (virCommandRun(cmd, NULL) < 0) { + virReportSystemError(errno, _("failed to run '%s --version': %s"), + caps->binaryPath, version); + goto cleanup; + } + virCommandFree(cmd); + + cmd = virCommandNewArgList(caps->binaryPath, "--help", NULL); + virCommandSetOutputBuffer(cmd, &help); + virCommandSetErrorBuffer(cmd, &help); + virCommandAddEnvPassCommon(cmd); + virCommandClearCaps(cmd); + if (virCommandRun(cmd, NULL) < 0) { + virReportSystemError(errno, _("failed to run '%s --help': %s"), + caps->binaryPath, help); + goto cleanup; + } + + if (virAsprintf(&complete, "%s\n%s", version, help) < 0) { + virReportOOMError(); + goto cleanup; + } + + ret = dnsmasqCapsSetFromBuffer(caps, complete); + +cleanup: + virCommandFree(cmd); + VIR_FREE(help); + VIR_FREE(version); + VIR_FREE(complete); + return ret; +} + +/** dnsmasqCapsRefresh: + * + * Refresh an existing caps object if the binary has changed. If + * there isn't yet a caps object (if it's NULL), create a new one. + * + * Returns 0 on success, -1 on failure + */ +static dnsmasqCapsPtr +dnsmasqCapsNewEmpty(const char *binaryPath) +{ + dnsmasqCapsPtr caps; + + if (dnsmasqCapsInitialize() < 0) + return NULL; + if (!(caps = virObjectNew(dnsmasqCapsClass))) + return NULL; + if (!(caps->flags = virBitmapNew(DNSMASQ_CAPS_LAST))) + goto error; + if (!(caps->binaryPath = strdup(binaryPath ? binaryPath : DNSMASQ))) + goto error; + return caps; + +error: + virReportOOMError(); + virObjectUnref(caps); + return NULL; +} + +dnsmasqCapsPtr +dnsmasqCapsNewFromBuffer(const char *buf, const char *binaryPath) +{ + dnsmasqCapsPtr caps = dnsmasqCapsNewEmpty(binaryPath); + + if (!caps) + return NULL; + + if (dnsmasqCapsSetFromBuffer(caps, buf) < 0) { + virObjectUnref(caps); + return NULL; + } + return caps; +} + +dnsmasqCapsPtr +dnsmasqCapsNewFromFile(const char *dataPath, const char *binaryPath) +{ + dnsmasqCapsPtr caps = dnsmasqCapsNewEmpty(binaryPath); + + if (!caps) + return NULL; + + if (dnsmasqCapsSetFromFile(caps, dataPath) < 0) { + virObjectUnref(caps); + return NULL; + } + return caps; +} + +dnsmasqCapsPtr +dnsmasqCapsNewFromBinary(const char *binaryPath) +{ + dnsmasqCapsPtr caps = dnsmasqCapsNewEmpty(binaryPath); + + if (!caps) + return NULL; + + if (dnsmasqCapsRefreshInternal(caps, true) < 0) { + virObjectUnref(caps); + return NULL; + } + return caps; +} + +int +dnsmasqCapsRefresh(dnsmasqCapsPtr *caps, const char *binaryPath) +{ + if (!*caps) { + *caps = dnsmasqCapsNewFromBinary(binaryPath); + return *caps ? 0 : -1; + } + return dnsmasqCapsRefreshInternal(*caps, false); +} + +const char * +dnsmasqCapsGetBinaryPath(dnsmasqCapsPtr caps) +{ + return caps ? caps->binaryPath : DNSMASQ; +} + +unsigned long +dnsmasqCapsGetVersion(dnsmasqCapsPtr caps) +{ + if (caps) + return caps->version; + else + return 0; +} + +bool +dnsmasqCapsGet(dnsmasqCapsPtr caps, dnsmasqCapsFlags flag) +{ + bool b; + + if (!caps || virBitmapGetBit(caps->flags, flag, &b) < 0) + return false; + else + return b; +} diff --git a/src/util/dnsmasq.h b/src/util/dnsmasq.h index ad612be..e8881a0 100644 --- a/src/util/dnsmasq.h +++ b/src/util/dnsmasq.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007-2010 Red Hat, Inc. + * Copyright (C) 2007-2012 Red Hat, Inc. * Copyright (C) 2010 Satoru SATOH <satoru.satoh@gmail.com> * * This library is free software; you can redistribute it and/or @@ -22,6 +22,7 @@ #ifndef __DNSMASQ_H__ # define __DNSMASQ_H__ +# include "virobject.h" # include "virsocketaddr.h" typedef struct @@ -65,6 +66,16 @@ typedef struct dnsmasqAddnHostsfile *addnhostsfile; } dnsmasqContext; +typedef enum { + DNSMASQ_CAPS_BIND_DYNAMIC = 0, /* support for --bind-dynamic */ + + DNSMASQ_CAPS_LAST, /* this must always be the last item */ +} dnsmasqCapsFlags; + +typedef struct _dnsmasqCaps dnsmasqCaps; +typedef dnsmasqCaps *dnsmasqCapsPtr; + + dnsmasqContext * dnsmasqContextNew(const char *network_name, const char *config_dir); void dnsmasqContextFree(dnsmasqContext *ctx); @@ -79,4 +90,13 @@ int dnsmasqSave(const dnsmasqContext *ctx); int dnsmasqDelete(const dnsmasqContext *ctx); int dnsmasqReload(pid_t pid); +dnsmasqCapsPtr dnsmasqCapsNewFromBuffer(const char *buf, + const char *binaryPath); +dnsmasqCapsPtr dnsmasqCapsNewFromFile(const char *dataPath, + const char *binaryPath); +dnsmasqCapsPtr dnsmasqCapsNewFromBinary(const char *binaryPath); +int dnsmasqCapsRefresh(dnsmasqCapsPtr *caps, const char *binaryPath); +bool dnsmasqCapsGet(dnsmasqCapsPtr caps, dnsmasqCapsFlags flag); +const char *dnsmasqCapsGetBinaryPath(dnsmasqCapsPtr caps); +unsigned long dnsmasqCapsGetVersion(dnsmasqCapsPtr caps); #endif /* __DNSMASQ_H__ */ diff --git a/tests/networkxml2argvtest.c b/tests/networkxml2argvtest.c index 87519e4..69cbd1c 100644 --- a/tests/networkxml2argvtest.c +++ b/tests/networkxml2argvtest.c @@ -46,7 +46,9 @@ static int replaceTokens(char **buf, const char *token, const char *replacement) return 0; } -static int testCompareXMLToArgvFiles(const char *inxml, const char *outargv) { +static int +testCompareXMLToArgvFiles(const char *inxml, const char *outargv, dnsmasqCapsPtr caps) +{ char *inXmlData = NULL; char *outArgvData = NULL; char *actual = NULL; @@ -78,7 +80,7 @@ static int testCompareXMLToArgvFiles(const char *inxml, const char *outargv) { if (dctx == NULL) goto fail; - if (networkBuildDhcpDaemonCommandLine(obj, &cmd, pidfile, dctx) < 0) + if (networkBuildDhcpDaemonCommandLine(obj, &cmd, pidfile, dctx, caps) < 0) goto fail; if (!(actual = virCommandToString(cmd))) @@ -102,21 +104,27 @@ static int testCompareXMLToArgvFiles(const char *inxml, const char *outargv) { return ret; } +typedef struct { + const char *name; + dnsmasqCapsPtr caps; +} testInfo; + static int testCompareXMLToArgvHelper(const void *data) { int result = -1; + const testInfo *info = data; char *inxml = NULL; char *outxml = NULL; if (virAsprintf(&inxml, "%s/networkxml2argvdata/%s.xml", - abs_srcdir, (const char*)data) < 0 || + abs_srcdir, info->name) < 0 || virAsprintf(&outxml, "%s/networkxml2argvdata/%s.argv", - abs_srcdir, (const char*)data) < 0) { + abs_srcdir, info->name) < 0) { goto cleanup; } - result = testCompareXMLToArgvFiles(inxml, outxml); + result = testCompareXMLToArgvFiles(inxml, outxml, info->caps); cleanup: VIR_FREE(inxml); @@ -140,23 +148,34 @@ static int mymain(void) { int ret = 0; + dnsmasqCapsPtr restricted + = dnsmasqCapsNewFromBuffer("Dnsmasq version 2.48", DNSMASQ); + dnsmasqCapsPtr full + = dnsmasqCapsNewFromBuffer("Dnsmasq version 2.63\n--bind-dynamic", DNSMASQ); networkDnsmasqLeaseFileName = testDnsmasqLeaseFileName; -#define DO_TEST(name) \ - if (virtTestRun("Network XML-2-Argv " name, \ - 1, testCompareXMLToArgvHelper, (name)) < 0) \ - ret = -1 - - DO_TEST("isolated-network"); - DO_TEST("routed-network"); - DO_TEST("nat-network"); - DO_TEST("netboot-network"); - DO_TEST("netboot-proxy-network"); - DO_TEST("nat-network-dns-txt-record"); - DO_TEST("nat-network-dns-srv-record"); - DO_TEST("nat-network-dns-srv-record-minimal"); - DO_TEST("nat-network-dns-hosts"); +#define DO_TEST(xname, xcaps) \ + do { \ + static testInfo info; \ + \ + info.name = xname; \ + info.caps = xcaps; \ + if (virtTestRun("Network XML-2-Argv " xname, \ + 1, testCompareXMLToArgvHelper, &info) < 0) { \ + ret = -1; \ + } \ + } while (0) + + DO_TEST("isolated-network", restricted); + DO_TEST("netboot-network", restricted); + DO_TEST("netboot-proxy-network", restricted); + DO_TEST("nat-network-dns-srv-record-minimal", restricted); + DO_TEST("routed-network", full); + DO_TEST("nat-network", full); + DO_TEST("nat-network-dns-txt-record", full); + DO_TEST("nat-network-dns-srv-record", full); + DO_TEST("nat-network-dns-hosts", full); return ret==0 ? EXIT_SUCCESS : EXIT_FAILURE; } -- 1.7.11.7

In order to optionally take advantage of new features in dnsmasq when the host's version of dnsmasq supports them, but still be able to run on hosts that don't support the new features, we need to be able to detect the version of dnsmasq running on the host, and possibly determine from the help output what options are in this dnsmasq.
networkxml2argvtest.c creates 2 "artificial" dnsmasqCaps objects at startup - one "restricted" (doesn't support --bind-dynamic) and one "full" (does support --bind-dynamic). Some of the test cases use one and some the other, to make sure both code pathes are tested.
Should we also follow the lead of tests/qemuhelpdata/ and do an actual capture from released dnsmasq binaries, to ensure that our parser for those files matches our artificial dnsmasqCaps objects? More on this below...[1]
+++ b/src/libvirt_private.syms @@ -246,13 +246,19 @@ virDevicePCIAddressParseXML;
dnsmasqSave;
- # domain_audit.h
Spurious whitespace change? Then again, we haven't been very consistent on one vs. two blank lines between .h files.
@@ -891,7 +899,8 @@ cleanup: }
static int -networkStartDhcpDaemon(virNetworkObjPtr network) +networkStartDhcpDaemon(struct network_driver *driver,
Given Dan's recent rename of 'struct qemud_driver *driver' to the friendlier 'virQEMUDriverPtr driver', should we do a similar change here? But if so, separate it to its own patch.
@@ -935,7 +944,9 @@ networkStartDhcpDaemon(virNetworkObjPtr network) if (dctx == NULL) goto cleanup;
- ret = networkBuildDhcpDaemonCommandLine(network, &cmd, pidfile, dctx); + dnsmasqCapsRefresh(&driver->dnsmasqCaps, false); + + ret = networkBuildDhcpDaemonCommandLine(network, &cmd, pidfile, dctx, driver->dnsmasqCaps);
Long line; worth wrapping?
+++ b/src/network/bridge_driver.h @@ -1,7 +1,7 @@ /* * network_driver.h: core driver methods for managing networks * - * Copyright (C) 2006, 2007, 2011 Red Hat, Inc. + * Copyright (C) 2006-2012 Red Hat, Inc.
You asked me about this one on IRC. If libvirt had a single copyright holder, then I'd say this is okay (it follows the convention[2] given by the FSF, where if a repository is publicly available, then touching any one file in that repository counts as a copyrightable claim on the package as a whole, and that all files in the package may claim the same range of copyright years as the package as a whole, even for files that were not touched in a particular year). It is a bit more questionable here (since we have NOT required anyone to assign copyright, libvirt has a multitude of copyright holders), but I still think you can get away with this change (Red Hat does indeed own the copyrights on a vast majority of the code base, and has made copyrightable contributions in every single year in this range). I guess it all boils down to how likely we are to ever try and defend the copyright in court (FSF has a much easier job of that task, thanks to them requiring copyright assignment; whereas with libvirt, it is up to individual copyright holders over their individual portions of the overall work to decide what they would defend in court). [2] https://www.gnu.org/prep/maintain/maintain.html#Copyright-Notices "To update the list of year numbers, add each year in which you have made nontrivial changes to the package. (Here we assume you’re using a publicly accessible revision control server, so that every revision installed is also immediately and automatically published.) When you add the new year, it is not required to keep track of which files have seen significant changes in the new year and which have not. It is recommended and simpler to add the new year to all files in the package, and be done with it for the rest of the year." "You can use a range (‘2008-2010’) instead of listing individual years (‘2008, 2009, 2010’) if and only if: 1) every year in the range, inclusive, really is a “copyrightable” year that would be listed individually; and 2) you make an explicit statement in a README file about this usage."
+ +#define SKIP_BLANKS(p) do { while ((*(p) == ' ') || (*(p) == '\t')) (p)++; } while (0)
Could we use virSkipSpaces() instead of open-coding this?
+ + p = buf; + if (!STRPREFIX(p, DNSMASQ_VERSION_STR)) + goto fail; + p += strlen(DNSMASQ_VERSION_STR);
Slightly more compact as: p = STRSKIP(buf, DNSMASQ_VERSION_STR); if (!p) goto fail;
+fail: + p = strchr(buf, '\n'); + if (!p) + p = strchr(buf, '\0');
Slightly more efficient as: p = strchrnul(buf, '\n')
+ +/** dnsmasqCapsRefresh: + * + * Refresh an existing caps object if the binary has changed. If + * there isn't yet a caps object (if it's NULL), create a new one. + * + * Returns 0 on success, -1 on failure + */ +static dnsmasqCapsPtr +dnsmasqCapsNewEmpty(const char *binaryPath)
Comment attached to the wrong function.
+++ b/tests/networkxml2argvtest.c @@ -140,23 +148,34 @@ static int mymain(void) { int ret = 0; + dnsmasqCapsPtr restricted + = dnsmasqCapsNewFromBuffer("Dnsmasq version 2.48", DNSMASQ); + dnsmasqCapsPtr full + = dnsmasqCapsNewFromBuffer("Dnsmasq version 2.63\n--bind-dynamic", DNSMASQ);
[1] Here is where I'm worried that we might want to also test sample actual output, in separate data files (which are also tagged as immune to syntax checking). But that would be a separate test (much as qemuhelptest is separate from qemuxml2argvtest), and could therefore be delayed to a later patch, so I'm okay with the amount of testing done in this particular test. I had some points you might want to clean up, but they are all minor (no logic bugs, just style or micro-optimizations), so I'm okay giving ACK. I've seen enough patches from you that I trust you to make the cleanups and/or post followups without needing to see a v4 just to address my findings.

On 11/28/2012 06:13 PM, Eric Blake wrote:
In order to optionally take advantage of new features in dnsmasq when the host's version of dnsmasq supports them, but still be able to run on hosts that don't support the new features, we need to be able to detect the version of dnsmasq running on the host, and possibly determine from the help output what options are in this dnsmasq.
networkxml2argvtest.c creates 2 "artificial" dnsmasqCaps objects at startup - one "restricted" (doesn't support --bind-dynamic) and one "full" (does support --bind-dynamic). Some of the test cases use one and some the other, to make sure both code pathes are tested. Should we also follow the lead of tests/qemuhelpdata/ and do an actual capture from released dnsmasq binaries, to ensure that our parser for those files matches our artificial dnsmasqCaps objects? More on this below...[1]
I had thought about that when making this patch, but wanted to keep things as simple as possible, since I'll probably have to backport it all the way to 0.8.7 :-/ Because of that, I'd like to keep things as they are for this patch, and expand it later.
+++ b/src/libvirt_private.syms @@ -246,13 +246,19 @@ virDevicePCIAddressParseXML; dnsmasqSave;
- # domain_audit.h Spurious whitespace change? Then again, we haven't been very consistent on one vs. two blank lines between .h files.
Fixed.
@@ -891,7 +899,8 @@ cleanup: }
static int -networkStartDhcpDaemon(virNetworkObjPtr network) +networkStartDhcpDaemon(struct network_driver *driver, Given Dan's recent rename of 'struct qemud_driver *driver' to the friendlier 'virQEMUDriverPtr driver', should we do a similar change here? But if so, separate it to its own patch.
Yes, but as you say, in a separate patch.
@@ -935,7 +944,9 @@ networkStartDhcpDaemon(virNetworkObjPtr network) if (dctx == NULL) goto cleanup;
- ret = networkBuildDhcpDaemonCommandLine(network, &cmd, pidfile, dctx); + dnsmasqCapsRefresh(&driver->dnsmasqCaps, false); + + ret = networkBuildDhcpDaemonCommandLine(network, &cmd, pidfile, dctx, driver->dnsmasqCaps); Long line; worth wrapping?
Fixed.
+++ b/src/network/bridge_driver.h @@ -1,7 +1,7 @@ /* * network_driver.h: core driver methods for managing networks * - * Copyright (C) 2006, 2007, 2011 Red Hat, Inc. + * Copyright (C) 2006-2012 Red Hat, Inc. You asked me about this one on IRC. If libvirt had a single copyright holder, then I'd say this is okay (it follows the convention[2] given by the FSF, where if a repository is publicly available, then touching any one file in that repository counts as a copyrightable claim on the package as a whole, and that all files in the package may claim the same range of copyright years as the package as a whole, even for files that were not touched in a particular year). It is a bit more questionable here (since we have NOT required anyone to assign copyright, libvirt has a multitude of copyright holders), but I still think you can get away with this change (Red Hat does indeed own the copyrights on a vast majority of the code base, and has made copyrightable contributions in every single year in this range).
That last bit is the important part - the change is actually correct since (as I confirmed by looking through the git history) this file (or one of its predecessors, as it was moved/split a couple times) *was* modified by someone from Red Hat at least once in every year in that range.
I guess it all boils down to how likely we are to ever try and defend the copyright in court (FSF has a much easier job of that task, thanks to them requiring copyright assignment; whereas with libvirt, it is up to individual copyright holders over their individual portions of the overall work to decide what they would defend in court).
[2] https://www.gnu.org/prep/maintain/maintain.html#Copyright-Notices "To update the list of year numbers, add each year in which you have made nontrivial changes to the package. (Here we assume you’re using a publicly accessible revision control server, so that every revision installed is also immediately and automatically published.) When you add the new year, it is not required to keep track of which files have seen significant changes in the new year and which have not. It is recommended and simpler to add the new year to all files in the package, and be done with it for the rest of the year." "You can use a range (‘2008-2010’) instead of listing individual years (‘2008, 2009, 2010’) if and only if: 1) every year in the range, inclusive, really is a “copyrightable” year that would be listed individually; and 2) you make an explicit statement in a README file about this usage."
+ +#define SKIP_BLANKS(p) do { while ((*(p) == ' ') || (*(p) == '\t')) (p)++; } while (0) Could we use virSkipSpaces() instead of open-coding this?
I was just copying exactly what's already in qemu_capabilities.c, without even looking for alternatives. Looking at virSkipSpaces, it would work correctly in its current form. Prior to 0.9.4 (when you fixed it), it would also skip backslashes, which isn't technically correct for us, but would still coincidentally work (since dnsmasq doesn't have any backslashes in its version string), so I can safely change it to use virSkipSpaces.
+ + p = buf; + if (!STRPREFIX(p, DNSMASQ_VERSION_STR)) + goto fail; + p += strlen(DNSMASQ_VERSION_STR); Slightly more compact as:
p = STRSKIP(buf, DNSMASQ_VERSION_STR); if (!p) goto fail;
Sure. Again, caused by just copying from qemu_capabilities.c.
+fail: + p = strchr(buf, '\n'); + if (!p) + p = strchr(buf, '\0'); Slightly more efficient as:
p = strchrnul(buf, '\n')
Likewise.
+ +/** dnsmasqCapsRefresh: + * + * Refresh an existing caps object if the binary has changed. If + * there isn't yet a caps object (if it's NULL), create a new one. + * + * Returns 0 on success, -1 on failure + */ +static dnsmasqCapsPtr +dnsmasqCapsNewEmpty(const char *binaryPath) Comment attached to the wrong function.
Oops. I had decided at the end that function wasn't self-explanatory enough and went back in to add the comment. I guess I wasn't paying close attention to where I was :-P
+++ b/tests/networkxml2argvtest.c @@ -140,23 +148,34 @@ static int mymain(void) { int ret = 0; + dnsmasqCapsPtr restricted + = dnsmasqCapsNewFromBuffer("Dnsmasq version 2.48", DNSMASQ); + dnsmasqCapsPtr full + = dnsmasqCapsNewFromBuffer("Dnsmasq version 2.63\n--bind-dynamic", DNSMASQ); [1] Here is where I'm worried that we might want to also test sample actual output, in separate data files (which are also tagged as immune to syntax checking). But that would be a separate test (much as qemuhelptest is separate from qemuxml2argvtest), and could therefore be delayed to a later patch, so I'm okay with the amount of testing done in this particular test.
I had some points you might want to clean up, but they are all minor (no logic bugs, just style or micro-optimizations), so I'm okay giving ACK. I've seen enough patches from you that I trust you to make the cleanups and/or post followups without needing to see a v4 just to address my findings.
Thanks! I'm pushing with the fixes you indicated.

This new function returns true if the given address is in the range of any "private" or "local" networks as defined in RFC1918 (IPv4) or RFC3484/RFC4193 (IPv6), otherwise they return false. These ranges are: 192.168.0.0/16 172.16.0.0/16 10.0.0.0/24 FC00::/7 FEC0::/10 --- src/libvirt_private.syms | 1 + src/util/virsocketaddr.c | 34 +++++++++++++++++++++++++++++++++- src/util/virsocketaddr.h | 3 ++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index f57ee1c..d602090 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -1784,6 +1784,7 @@ virSocketAddrFormatFull; virSocketAddrGetPort; virSocketAddrGetRange; virSocketAddrIsNetmask; +virSocketAddrIsPrivate; virSocketAddrMask; virSocketAddrMaskByPrefix; virSocketAddrParse; diff --git a/src/util/virsocketaddr.c b/src/util/virsocketaddr.c index b6cc16f..11cc706 100644 --- a/src/util/virsocketaddr.c +++ b/src/util/virsocketaddr.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2011 Red Hat, Inc. + * Copyright (C) 2009-2012 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -194,6 +194,38 @@ virSocketAddrEqual(const virSocketAddrPtr s1, const virSocketAddrPtr s2) } /* + * virSocketAddrIsPrivate: + * @s: the location of the IP address + * + * Return true if this address is in its family's defined + * "private/local" address space. For IPv4, private addresses are in + * the range of 192.168.0.0/16, 172.16.0.0/16, or 10.0.0.0/8. For + * IPv6, local addresses are in the range of FC00::/7. + * + * See RFC1918 and RFC4193 for details. + */ +bool +virSocketAddrIsPrivate(const virSocketAddrPtr addr) +{ + unsigned long val; + + switch (addr->data.stor.ss_family) { + case AF_INET: + val = ntohl(addr->data.inet4.sin_addr.s_addr); + + return ((val & 0xFFFF0000) == ((192L << 24) + (168 << 16)) || + (val & 0xFFFF0000) == ((172L << 24) + (16 << 16)) || + (val & 0xFF000000) == ((10L << 24))); + + case AF_INET6: + return ((addr->data.inet6.sin6_addr.s6_addr[0] & 0xFC) == 0xFC || + ((addr->data.inet6.sin6_addr.s6_addr[0] & 0xFF) == 0xFE && + (addr->data.inet6.sin6_addr.s6_addr[1] & 0xC0) == 0xC0)); + } + return false; +} + +/* * virSocketAddrFormat: * @addr: an initialized virSocketAddrPtr * diff --git a/src/util/virsocketaddr.h b/src/util/virsocketaddr.h index 51ddd2d..66d4265 100644 --- a/src/util/virsocketaddr.h +++ b/src/util/virsocketaddr.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2011 Red Hat, Inc. + * Copyright (C) 2009-2012 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -104,5 +104,6 @@ int virSocketAddrPrefixToNetmask(unsigned int prefix, int family); bool virSocketAddrEqual(const virSocketAddrPtr s1, const virSocketAddrPtr s2); +bool virSocketAddrIsPrivate(const virSocketAddrPtr addr); #endif /* __VIR_SOCKETADDR_H__ */ -- 1.7.11.7

----- Original Message -----
This new function returns true if the given address is in the range of any "private" or "local" networks as defined in RFC1918 (IPv4) or RFC3484/RFC4193 (IPv6), otherwise they return false.
These ranges are:
192.168.0.0/16 172.16.0.0/16 10.0.0.0/24 FC00::/7 FEC0::/10 --- src/libvirt_private.syms | 1 + src/util/virsocketaddr.c | 34 +++++++++++++++++++++++++++++++++- src/util/virsocketaddr.h | 3 ++- 3 files changed, 36 insertions(+), 2 deletions(-)
/* + * virSocketAddrIsPrivate: + * @s: the location of the IP address + * + * Return true if this address is in its family's defined + * "private/local" address space. For IPv4, private addresses are in + * the range of 192.168.0.0/16, 172.16.0.0/16, or 10.0.0.0/8. For + * IPv6, local addresses are in the range of FC00::/7. + * + * See RFC1918 and RFC4193 for details.
Did you intend to update this comment to match the commit message regarding FEC0::/10?
+ */ +bool +virSocketAddrIsPrivate(const virSocketAddrPtr addr) +{ + unsigned long val; + + switch (addr->data.stor.ss_family) { + case AF_INET: + val = ntohl(addr->data.inet4.sin_addr.s_addr); + + return ((val & 0xFFFF0000) == ((192L << 24) + (168 << 16)) || + (val & 0xFFFF0000) == ((172L << 24) + (16 << 16)) || + (val & 0xFF000000) == ((10L << 24))); + + case AF_INET6: + return ((addr->data.inet6.sin6_addr.s6_addr[0] & 0xFC) == 0xFC ||
Isn't this FC00::/6? You should be using '& 0xFE' for /7. ACK with those fixes.

On 11/28/2012 06:23 PM, Eric Blake wrote:
----- Original Message -----
This new function returns true if the given address is in the range of any "private" or "local" networks as defined in RFC1918 (IPv4) or RFC3484/RFC4193 (IPv6), otherwise they return false.
These ranges are:
192.168.0.0/16 172.16.0.0/16 10.0.0.0/24 FC00::/7 FEC0::/10 --- src/libvirt_private.syms | 1 + src/util/virsocketaddr.c | 34 +++++++++++++++++++++++++++++++++- src/util/virsocketaddr.h | 3 ++- 3 files changed, 36 insertions(+), 2 deletions(-)
/* + * virSocketAddrIsPrivate: + * @s: the location of the IP address + * + * Return true if this address is in its family's defined + * "private/local" address space. For IPv4, private addresses are in + * the range of 192.168.0.0/16, 172.16.0.0/16, or 10.0.0.0/8. For + * IPv6, local addresses are in the range of FC00::/7. + * + * See RFC1918 and RFC4193 for details. Did you intend to update this comment to match the commit message regarding FEC0::/10?
Yes. Thanks for noticing!
+ */ +bool +virSocketAddrIsPrivate(const virSocketAddrPtr addr) +{ + unsigned long val; + + switch (addr->data.stor.ss_family) { + case AF_INET: + val = ntohl(addr->data.inet4.sin_addr.s_addr); + + return ((val & 0xFFFF0000) == ((192L << 24) + (168 << 16)) || + (val & 0xFFFF0000) == ((172L << 24) + (16 << 16)) || + (val & 0xFF000000) == ((10L << 24))); + + case AF_INET6: + return ((addr->data.inet6.sin6_addr.s6_addr[0] & 0xFC) == 0xFC || Isn't this FC00::/6? You should be using '& 0xFE' for /7.
Gulp. Yes. Fixed.
ACK with those fixes.
Pushed with those fixes.

This bug resolves CVE-2012-3411, which is described in the following bugzilla report: https://bugzilla.redhat.com/show_bug.cgi?id=833033 The following report is specifically for libvirt on Fedora: https://bugzilla.redhat.com/show_bug.cgi?id=874702 In short, a dnsmasq instance run with the intention of listening for DHCP/DNS requests only on a libvirt virtual network (which is constructed using a Linux host bridge) would also answer queries sent from outside the virtualization host. This patch takes advantage of a new dnsmasq option "--bind-dynamic", which will cause the listening socket to be setup such that it will only receive those requests that actually come in via the bridge interface. In order for this behavior to actually occur, not only must "--bind-interfaces" be replaced with "--bind-dynamic", but also all "--listen-address" options must be replaced with a single "--interface" option. Fully: --bind-interfaces --except-interface lo --listen-address x.x.x.x ... (with --listen-address possibly repeated) is replaced with: --bind-dynamic --interface virbrX Of course libvirt can't use this new option if the host's dnsmasq doesn't have it, but we still want libvirt to function (because the great majority of libvirt installations, which only have mode='nat' networks using RFC1918 private address ranges (e.g. 192.168.122.0/24), are immune to this vulnerability from anywhere beyond the local subnet of the host), so we use the new dnsmasqCaps API to check if dnsmasq supports the new option and, if not, we use the "old" option style instead. In order to assure that this permissiveness doesn't lead to a vulnerable system, we do check for non-private addresses in this case, and refuse to start the network if both a) we are using the old-style options, and b) the network has a publicly routable IP address. Hopefully this will provide the proper balance of not being disruptive to those not practically affected, and making sure that those who *are* affected get their dnsmasq upgraded. (--bind-dynamic was added to dnsmasq in upstream commit 54dd393f3938fc0c19088fbd319b95e37d81a2b0, which was included in dnsmasq-2.63) --- src/network/bridge_driver.c | 77 +++++++++++++++------- tests/networkxml2argvdata/isolated-network.argv | 5 +- .../networkxml2argvdata/nat-network-dns-hosts.argv | 5 +- .../nat-network-dns-srv-record-minimal.argv | 9 ++- .../nat-network-dns-srv-record-minimal.xml | 4 +- .../nat-network-dns-srv-record.argv | 8 +-- .../nat-network-dns-txt-record.argv | 8 +-- tests/networkxml2argvdata/nat-network.argv | 6 +- tests/networkxml2argvdata/netboot-network.argv | 4 +- .../networkxml2argvdata/netboot-proxy-network.argv | 5 +- tests/networkxml2argvdata/routed-network.argv | 4 +- 11 files changed, 80 insertions(+), 55 deletions(-) diff --git a/src/network/bridge_driver.c b/src/network/bridge_driver.c index eab8ffe..479ff29 100644 --- a/src/network/bridge_driver.c +++ b/src/network/bridge_driver.c @@ -657,7 +657,7 @@ networkBuildDnsmasqArgv(virNetworkObjPtr network, * Needed to ensure dnsmasq uses same algorithm for processing * multiple namedriver entries in /etc/resolv.conf as GLibC. */ - virCommandAddArgList(cmd, "--strict-order", "--bind-interfaces", NULL); + virCommandAddArg(cmd, "--strict-order"); if (network->def->domain) virCommandAddArgPair(cmd, "--domain", network->def->domain); @@ -672,9 +672,60 @@ networkBuildDnsmasqArgv(virNetworkObjPtr network, /* *no* conf file */ virCommandAddArg(cmd, "--conf-file="); - virCommandAddArgList(cmd, - "--except-interface", "lo", - NULL); + if (dnsmasqCapsGet(caps, DNSMASQ_CAPS_BIND_DYNAMIC)) { + /* using --bind-dynamic with only --interface (no + * --listen-address) prevents dnsmasq from responding to dns + * queries that arrive on some interface other than our bridge + * interface (in other words, requests originating somewhere + * other than one of the virtual guests connected directly to + * this network). This was added in response to CVE 2012-3411. + */ + virCommandAddArgList(cmd, + "--bind-dynamic", + "--interface", network->def->bridge, + NULL); + } else { + virCommandAddArgList(cmd, + "--bind-interfaces", + "--except-interface", "lo", + NULL); + /* + * --interface does not actually work with dnsmasq < 2.47, + * due to DAD for ipv6 addresses on the interface. + * + * virCommandAddArgList(cmd, "--interface", network->def->bridge, NULL); + * + * So listen on all defined IPv[46] addresses + */ + for (ii = 0; + (tmpipdef = virNetworkDefGetIpByIndex(network->def, AF_UNSPEC, ii)); + ii++) { + char *ipaddr = virSocketAddrFormat(&tmpipdef->address); + + if (!ipaddr) + goto cleanup; + /* also part of CVE 2012-3411 - if the host's version of + * dnsmasq doesn't have --bind-dynamic, only allow listening on + * private/local IP addresses (see RFC1918/RFC3484/RFC4193) + */ + if (!virSocketAddrIsPrivate(&tmpipdef->address)) { + unsigned long version = dnsmasqCapsGetVersion(caps); + + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("Publicly routable address %s is prohibited. " + "The version of dnsmasq on this host (%d.%d) doesn't " + "support the --bind-dynamic option, which is required " + "for safe operation on a publicly routable subnet " + "(see CVE-2012-3411). You must either upgrade dnsmasq, " + "or use a private/local subnet range for this network " + "(as described in RFC1918/RFC3484/RFC4193)."), ipaddr, + (int)version / 1000000, (int)(version % 1000000) / 1000); + goto cleanup; + } + virCommandAddArgList(cmd, "--listen-address", ipaddr, NULL); + VIR_FREE(ipaddr); + } + } /* If this is an isolated network, set the default route option * (3) to be empty to avoid setting a default route that's @@ -740,24 +791,6 @@ networkBuildDnsmasqArgv(virNetworkObjPtr network, } } - /* - * --interface does not actually work with dnsmasq < 2.47, - * due to DAD for ipv6 addresses on the interface. - * - * virCommandAddArgList(cmd, "--interface", ipdef->bridge, NULL); - * - * So listen on all defined IPv[46] addresses - */ - for (ii = 0; - (tmpipdef = virNetworkDefGetIpByIndex(network->def, AF_UNSPEC, ii)); - ii++) { - char *ipaddr = virSocketAddrFormat(&tmpipdef->address); - if (!ipaddr) - goto cleanup; - virCommandAddArgList(cmd, "--listen-address", ipaddr, NULL); - VIR_FREE(ipaddr); - } - if (ipdef) { for (r = 0 ; r < ipdef->nranges ; r++) { char *saddr = virSocketAddrFormat(&ipdef->ranges[r].start); diff --git a/tests/networkxml2argvdata/isolated-network.argv b/tests/networkxml2argvdata/isolated-network.argv index 13e77b2..3d8601e 100644 --- a/tests/networkxml2argvdata/isolated-network.argv +++ b/tests/networkxml2argvdata/isolated-network.argv @@ -1,7 +1,8 @@ -@DNSMASQ@ --strict-order --bind-interfaces \ +@DNSMASQ@ --strict-order \ --local=// --domain-needed --conf-file= \ ---except-interface lo --dhcp-option=3 --no-resolv \ +--bind-interfaces --except-interface lo \ --listen-address 192.168.152.1 \ +--dhcp-option=3 --no-resolv \ --dhcp-range 192.168.152.2,192.168.152.254 \ --dhcp-leasefile=/var/lib/libvirt/dnsmasq/private.leases --dhcp-lease-max=253 \ --dhcp-no-override \ diff --git a/tests/networkxml2argvdata/nat-network-dns-hosts.argv b/tests/networkxml2argvdata/nat-network-dns-hosts.argv index 03a0676..e5143ac 100644 --- a/tests/networkxml2argvdata/nat-network-dns-hosts.argv +++ b/tests/networkxml2argvdata/nat-network-dns-hosts.argv @@ -1,4 +1,5 @@ -@DNSMASQ@ --strict-order --bind-interfaces --domain=example.com \ +@DNSMASQ@ --strict-order --domain=example.com \ --local=/example.com/ --domain-needed \ ---conf-file= --except-interface lo --listen-address 192.168.122.1 \ +--conf-file= \ +--bind-dynamic --interface virbr0 \ --expand-hosts --addn-hosts=/var/lib/libvirt/dnsmasq/default.addnhosts\ diff --git a/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.argv b/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.argv index 210a60c..031da3f 100644 --- a/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.argv +++ b/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.argv @@ -1,14 +1,13 @@ @DNSMASQ@ \ --strict-order \ ---bind-interfaces \ --local=// --domain-needed --conf-file= \ ---except-interface lo \ ---srv-host=name.tcp.,,,, \ +--bind-interfaces --except-interface lo \ --listen-address 192.168.122.1 \ --listen-address 192.168.123.1 \ ---listen-address 2001:db8:ac10:fe01::1 \ ---listen-address 2001:db8:ac10:fd01::1 \ +--listen-address fc00:db8:ac10:fe01::1 \ +--listen-address fc00:db8:ac10:fd01::1 \ --listen-address 10.24.10.1 \ +--srv-host=name.tcp.,,,, \ --dhcp-range 192.168.122.2,192.168.122.254 \ --dhcp-leasefile=/var/lib/libvirt/dnsmasq/default.leases \ --dhcp-lease-max=253 \ diff --git a/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.xml b/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.xml index e9b7680..f6f24e1 100644 --- a/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.xml +++ b/tests/networkxml2argvdata/nat-network-dns-srv-record-minimal.xml @@ -17,9 +17,9 @@ </ip> <ip family='ipv4' address='192.168.123.1' netmask='255.255.255.0'> </ip> - <ip family='ipv6' address='2001:db8:ac10:fe01::1' prefix='64'> + <ip family='ipv6' address='fc00:db8:ac10:fe01::1' prefix='64'> </ip> - <ip family='ipv6' address='2001:db8:ac10:fd01::1' prefix='64'> + <ip family='ipv6' address='fc00:db8:ac10:fd01::1' prefix='64'> </ip> <ip family='ipv4' address='10.24.10.1'> </ip> diff --git a/tests/networkxml2argvdata/nat-network-dns-srv-record.argv b/tests/networkxml2argvdata/nat-network-dns-srv-record.argv index 833d3cd..beff591 100644 --- a/tests/networkxml2argvdata/nat-network-dns-srv-record.argv +++ b/tests/networkxml2argvdata/nat-network-dns-srv-record.argv @@ -1,14 +1,8 @@ @DNSMASQ@ \ --strict-order \ ---bind-interfaces \ --local=// --domain-needed --conf-file= \ ---except-interface lo \ +--bind-dynamic --interface virbr0 \ --srv-host=name.tcp.test-domain-name,.,1024,10,10 \ ---listen-address 192.168.122.1 \ ---listen-address 192.168.123.1 \ ---listen-address 2001:db8:ac10:fe01::1 \ ---listen-address 2001:db8:ac10:fd01::1 \ ---listen-address 10.24.10.1 \ --dhcp-range 192.168.122.2,192.168.122.254 \ --dhcp-leasefile=/var/lib/libvirt/dnsmasq/default.leases \ --dhcp-lease-max=253 \ diff --git a/tests/networkxml2argvdata/nat-network-dns-txt-record.argv b/tests/networkxml2argvdata/nat-network-dns-txt-record.argv index 3481507..fc164f6 100644 --- a/tests/networkxml2argvdata/nat-network-dns-txt-record.argv +++ b/tests/networkxml2argvdata/nat-network-dns-txt-record.argv @@ -1,9 +1,7 @@ -@DNSMASQ@ --strict-order --bind-interfaces \ +@DNSMASQ@ --strict-order \ --local=// --domain-needed --conf-file= \ ---except-interface lo '--txt-record=example,example value' \ ---listen-address 192.168.122.1 --listen-address 192.168.123.1 \ ---listen-address 2001:db8:ac10:fe01::1 \ ---listen-address 2001:db8:ac10:fd01::1 --listen-address 10.24.10.1 \ +--bind-dynamic --interface virbr0 \ +'--txt-record=example,example value' \ --dhcp-range 192.168.122.2,192.168.122.254 \ --dhcp-leasefile=/var/lib/libvirt/dnsmasq/default.leases \ --dhcp-lease-max=253 --dhcp-no-override \ diff --git a/tests/networkxml2argvdata/nat-network.argv b/tests/networkxml2argvdata/nat-network.argv index 37fd2fc..6303f76 100644 --- a/tests/networkxml2argvdata/nat-network.argv +++ b/tests/networkxml2argvdata/nat-network.argv @@ -1,8 +1,6 @@ -@DNSMASQ@ --strict-order --bind-interfaces \ +@DNSMASQ@ --strict-order \ --local=// --domain-needed --conf-file= \ ---except-interface lo --listen-address 192.168.122.1 \ ---listen-address 192.168.123.1 --listen-address 2001:db8:ac10:fe01::1 \ ---listen-address 2001:db8:ac10:fd01::1 --listen-address 10.24.10.1 \ +--bind-dynamic --interface virbr0 \ --dhcp-range 192.168.122.2,192.168.122.254 \ --dhcp-leasefile=/var/lib/libvirt/dnsmasq/default.leases \ --dhcp-lease-max=253 --dhcp-no-override \ diff --git a/tests/networkxml2argvdata/netboot-network.argv b/tests/networkxml2argvdata/netboot-network.argv index 5408eb7..9699e5c 100644 --- a/tests/networkxml2argvdata/netboot-network.argv +++ b/tests/networkxml2argvdata/netboot-network.argv @@ -1,6 +1,6 @@ -@DNSMASQ@ --strict-order --bind-interfaces --domain=example.com \ +@DNSMASQ@ --strict-order --domain=example.com \ --local=/example.com/ --domain-needed --conf-file= \ ---except-interface lo --listen-address 192.168.122.1 \ +--bind-interfaces --except-interface lo --listen-address 192.168.122.1 \ --dhcp-range 192.168.122.2,192.168.122.254 \ --dhcp-leasefile=/var/lib/libvirt/dnsmasq/netboot.leases \ --dhcp-lease-max=253 --dhcp-no-override --expand-hosts \ diff --git a/tests/networkxml2argvdata/netboot-proxy-network.argv b/tests/networkxml2argvdata/netboot-proxy-network.argv index 21e01e3..9ac3018 100644 --- a/tests/networkxml2argvdata/netboot-proxy-network.argv +++ b/tests/networkxml2argvdata/netboot-proxy-network.argv @@ -1,6 +1,7 @@ -@DNSMASQ@ --strict-order --bind-interfaces --domain=example.com \ +@DNSMASQ@ --strict-order --domain=example.com \ --local=/example.com/ --domain-needed --conf-file= \ ---except-interface lo --listen-address 192.168.122.1 \ +--bind-interfaces --except-interface lo \ +--listen-address 192.168.122.1 \ --dhcp-range 192.168.122.2,192.168.122.254 \ --dhcp-leasefile=/var/lib/libvirt/dnsmasq/netboot.leases \ --dhcp-lease-max=253 --dhcp-no-override --expand-hosts \ diff --git a/tests/networkxml2argvdata/routed-network.argv b/tests/networkxml2argvdata/routed-network.argv index 9fedb2b..700c904 100644 --- a/tests/networkxml2argvdata/routed-network.argv +++ b/tests/networkxml2argvdata/routed-network.argv @@ -1,4 +1,4 @@ -@DNSMASQ@ --strict-order --bind-interfaces \ +@DNSMASQ@ --strict-order \ --local=// --domain-needed --conf-file= \ ---except-interface lo --listen-address 192.168.122.1 \ +--bind-dynamic --interface virbr1 \ --addn-hosts=/var/lib/libvirt/dnsmasq/local.addnhosts\ -- 1.7.11.7

This bug resolves CVE-2012-3411, which is described in the following bugzilla report:
https://bugzilla.redhat.com/show_bug.cgi?id=833033
The following report is specifically for libvirt on Fedora:
https://bugzilla.redhat.com/show_bug.cgi?id=874702
In short, a dnsmasq instance run with the intention of listening for DHCP/DNS requests only on a libvirt virtual network (which is constructed using a Linux host bridge) would also answer queries sent from outside the virtualization host.
<snip> It's always nice to fully explain things in the commit message, as you have done here - not only does it make the reviewer's job easier today, but down the road, it will make it much easier to answer what the CVE was all about and who is impacted (or more specifically, that default installation is NOT impacted). Thanks for taking the time to write it up. ACK. And let's get this in, so distros can start backporting the CVE fix for the sake of those people who ARE impacted.

On 11/28/2012 06:32 PM, Eric Blake wrote:
This bug resolves CVE-2012-3411, which is described in the following bugzilla report:
https://bugzilla.redhat.com/show_bug.cgi?id=833033
The following report is specifically for libvirt on Fedora:
https://bugzilla.redhat.com/show_bug.cgi?id=874702
In short, a dnsmasq instance run with the intention of listening for DHCP/DNS requests only on a libvirt virtual network (which is constructed using a Linux host bridge) would also answer queries sent from outside the virtualization host.
<snip>
It's always nice to fully explain things in the commit message, as you have done here - not only does it make the reviewer's job easier today, but down the road, it will make it much easier to answer what the CVE was all about and who is impacted (or more specifically, that default installation is NOT impacted). Thanks for taking the time to write it up.
ACK. And let's get this in, so distros can start backporting the CVE fix for the sake of those people who ARE impacted.
Thanks! I've pushed the entire series. I suppose I should now get to the backports...
participants (2)
-
Eric Blake
-
Laine Stump