[libvirt] [PATCH 0/2] option to disable default gateway in IPv6 RA

In case of DHCPv6 in isolated network, we start dnsmasq which sends Router Advertisements (RA). If RA containts no gateway then the link-local address of the source of RA is considered a gateway (and guest installs a corresponding default route). If a guest has two network interfaces (public and isolated network) and the user installs a default route through "public" interface, the guest will have something like default via fe80::ffff:1:1 dev eth2 metric 1024 default via fe80::5054:ff:fe0a:d808 dev eth3 proto ra metric 1024 expires 1789sec RA route metric may vary, and it is preferred. The validity of default route is controlled by "default [route] lifetime" field in RA. If it is 0, then the default gateway announced is considered invalid, and no default route is installed into guest. dnsmasq 2.67+ supports "ra-param=<interface>,<RA interval>,<default lifetime>" option. We can pass "ra-param=*,0,0" (here, RA_interval=0 means default) to disable default gateway in RA. This patchset adds <network ipv6noDefRoute="yes|no"> option to network xml and sets the above option to dnsmasq config if it is set to yes (default: no). Maxim Perevedentsev (2): dnsmasq: add option to disable IPv6 default gateway in RA docs: add ipv6noDefRoute to schema and html. docs/formatnetwork.html.in | 15 ++++++++++++++- docs/schemas/network.rng | 5 +++++ src/conf/network_conf.c | 17 +++++++++++++++++ src/conf/network_conf.h | 3 +++ src/network/bridge_driver.c | 22 ++++++++++++++++++++++ src/util/virdnsmasq.h | 6 ++++++ 6 files changed, 67 insertions(+), 1 deletion(-) -- 1.8.3.1

If no gateway is specified in RA the a guest will install a default route to link-local address of the source of RA (in this case, virbr*), which may disturb guest's networking e.g. if the 'expected' default route is through another interface. This patch adds an attribute 'ipv6noDefRoute=yes|no' to network definition. If this attribute is set, we add ra-param=*,0,0 // <interface>,<RA interval>,<default gateway lifetime> // here we have "any interface","default interval","0 seconds" to dnsmasq config. This makes the 'default gateway lifetime' to be 0 seconds, which means that receiver of RA must not install a default route from this RA. --- src/conf/network_conf.c | 17 +++++++++++++++++ src/conf/network_conf.h | 3 +++ src/network/bridge_driver.c | 22 ++++++++++++++++++++++ src/util/virdnsmasq.h | 6 ++++++ 4 files changed, 48 insertions(+) diff --git a/src/conf/network_conf.c b/src/conf/network_conf.c index 2d904df..364f3fe 100644 --- a/src/conf/network_conf.c +++ b/src/conf/network_conf.c @@ -2056,6 +2056,7 @@ virNetworkDefParseXML(xmlXPathContextPtr ctxt) xmlNodePtr forwardNode = NULL; char *ipv6nogwStr = NULL; char *trustGuestRxFilters = NULL; + char *ipv6noDefRouteStr = NULL; xmlNodePtr save = ctxt->node; xmlNodePtr bandwidthNode = NULL; xmlNodePtr vlanNode; @@ -2124,6 +2125,19 @@ virNetworkDefParseXML(xmlXPathContextPtr ctxt) VIR_FREE(trustGuestRxFilters); } + ipv6noDefRouteStr = virXPathString("string(./@ipv6noDefRoute)", ctxt); + if (ipv6noDefRouteStr) { + if (STREQ(ipv6noDefRouteStr, "yes")) { + def->ipv6noDefRoute = true; + } else if (STRNEQ(ipv6noDefRouteStr, "no")) { + virReportError(VIR_ERR_XML_ERROR, + _("Invalid ipv6noDefRoute setting '%s' in network '%s'"), + ipv6noDefRouteStr, def->name); + goto error; + } + VIR_FREE(ipv6noDefRouteStr); + } + /* Parse network domain information */ def->domain = virXPathString("string(./domain[1]/@name)", ctxt); tmp = virXPathString("string(./domain[1]/@localOnly)", ctxt); @@ -2401,6 +2415,7 @@ virNetworkDefParseXML(xmlXPathContextPtr ctxt) VIR_FREE(ipNodes); VIR_FREE(portGroupNodes); VIR_FREE(ipv6nogwStr); + VIR_FREE(ipv6noDefRouteStr); VIR_FREE(trustGuestRxFilters); ctxt->node = save; return NULL; @@ -2728,6 +2743,8 @@ virNetworkDefFormatBuf(virBufferPtr buf, if (def->trustGuestRxFilters) virBufferAsprintf(buf, " trustGuestRxFilters='%s'", virTristateBoolTypeToString(def->trustGuestRxFilters)); + if (def->ipv6noDefRoute) + virBufferAddLit(buf, " ipv6noDefRoute='yes'"); virBufferAddLit(buf, ">\n"); virBufferAdjustIndent(buf, 2); virBufferEscapeString(buf, "<name>%s</name>\n", def->name); diff --git a/src/conf/network_conf.h b/src/conf/network_conf.h index e7ce674..e2d926f 100644 --- a/src/conf/network_conf.h +++ b/src/conf/network_conf.h @@ -253,6 +253,9 @@ struct _virNetworkDef { virNetDevBandwidthPtr bandwidth; virNetDevVlan vlan; int trustGuestRxFilters; /* enum virTristateBool */ + + /* forbid dnsmasq to announce our link-local address as default gateway */ + bool ipv6noDefRoute; }; typedef struct _virNetworkObj virNetworkObj; diff --git a/src/network/bridge_driver.c b/src/network/bridge_driver.c index 0221a38..a5998bb 100644 --- a/src/network/bridge_driver.c +++ b/src/network/bridge_driver.c @@ -1051,6 +1051,28 @@ networkDnsmasqConfContents(virNetworkObjPtr network, if (network->def->forward.type == VIR_NETWORK_FORWARD_NONE) { virBufferAddLit(&configbuf, "dhcp-option=3\n" "no-resolv\n"); + if (network->def->ipv6noDefRoute) { + /* Set RA interval to 0 + * (= default, we cannot set 3rd parameter without setting 2nd) + * and default route lifetime to 0 seconds i.e. + * do not announce our link-local address as default gateway + */ + if (!DNSMASQ_RA_PARAM_SUPPORT(caps)) { + unsigned long version = dnsmasqCapsGetVersion(caps); + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("The version of dnsmasq on this host " + "(%d.%d) doesn't support 'ra-param' option " + "which is needed to disable " + "IPv6 default route. " + "Version %d.%d or later is required."), + (int)version / 1000000, + (int)(version % 1000000) / 1000, + DNSMASQ_RA_PARAM_MAJOR_REQD, + DNSMASQ_RA_PARAM_MINOR_REQD); + goto cleanup; + } + virBufferAddLit(&configbuf, "ra-param=*,0,0\n"); + } } for (i = 0; i < dns->ntxts; i++) { diff --git a/src/util/virdnsmasq.h b/src/util/virdnsmasq.h index ed560da..95f4488 100644 --- a/src/util/virdnsmasq.h +++ b/src/util/virdnsmasq.h @@ -109,6 +109,8 @@ unsigned long dnsmasqCapsGetVersion(dnsmasqCapsPtr caps); # define DNSMASQ_DHCPv6_MINOR_REQD 64 # define DNSMASQ_RA_MAJOR_REQD 2 # define DNSMASQ_RA_MINOR_REQD 64 +# define DNSMASQ_RA_PARAM_MAJOR_REQD 2 +# define DNSMASQ_RA_PARAM_MINOR_REQD 67 # define DNSMASQ_DHCPv6_SUPPORT(CAPS) \ (dnsmasqCapsGetVersion(CAPS) >= \ @@ -118,4 +120,8 @@ unsigned long dnsmasqCapsGetVersion(dnsmasqCapsPtr caps); (dnsmasqCapsGetVersion(CAPS) >= \ (DNSMASQ_RA_MAJOR_REQD * 1000000) + \ (DNSMASQ_RA_MINOR_REQD * 1000)) +# define DNSMASQ_RA_PARAM_SUPPORT(CAPS) \ + (dnsmasqCapsGetVersion(CAPS) >= \ + (DNSMASQ_RA_PARAM_MAJOR_REQD * 1000000) + \ + (DNSMASQ_RA_PARAM_MINOR_REQD * 1000)) #endif /* __DNSMASQ_H__ */ -- 1.8.3.1

--- docs/formatnetwork.html.in | 15 ++++++++++++++- docs/schemas/network.rng | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/formatnetwork.html.in b/docs/formatnetwork.html.in index 1cea931..e710f25 100644 --- a/docs/formatnetwork.html.in +++ b/docs/formatnetwork.html.in @@ -35,7 +35,7 @@ </p> <pre> - <network ipv6='yes' trustGuestRxFilters='no'> + <network ipv6='yes' trustGuestRxFilters='no' ipv6noDefRoute='yes'> <name>default</name> <uuid>3e3fce45-4f53-4fa7-bb32-11f34168b82b</uuid> ...</pre> @@ -71,6 +71,19 @@ more details. Note that an explicit setting of this attribute in a portgroup or the individual domain interface will override the setting in the network.</dd> + <dt><code>ipv6noDefRoute='yes'</code></dt> + <dd>The optional parameter <code>ipv6noDefRoute='yes'</code> + disables installing IPv6 default gateway into guest from + IPv6 Router Advertisements (RA). If no gateway is specified + explicitly, guest learns a default route via the source of RA + (normally, link-local address of virbr*). + This parameter is only considered for isolated networks. + It is useful if a domain has two network interfaces, + one of which is connected to public network and another one + to isolated network. This option prevents IPv6 default route + to isolated network from RA from disturbing normal routing + to public network. + <span class="since">Since 2.0.1</span></dd> </dl> <h3><a name="elementsConnect">Connectivity</a></h3> diff --git a/docs/schemas/network.rng b/docs/schemas/network.rng index 4edb6eb..66c849c 100644 --- a/docs/schemas/network.rng +++ b/docs/schemas/network.rng @@ -29,6 +29,11 @@ <ref name="virYesNo"/> </attribute> </optional> + <optional> + <attribute name="ipv6noDefRoute"> + <ref name="virYesNo"/> + </attribute> + </optional> <interleave> <!-- The name of the network, used to refer to it through the API -- 1.8.3.1

On 06/29/2016 10:17 AM, Maxim Perevedentsev wrote:
In case of DHCPv6 in isolated network, we start dnsmasq which sends Router Advertisements (RA). If RA containts no gateway then the link-local address of the source of RA is considered a gateway (and guest installs a corresponding default route).
If a guest has two network interfaces (public and isolated network) and the user installs a default route through "public" interface, the guest will have something like
default via fe80::ffff:1:1 dev eth2 metric 1024 default via fe80::5054:ff:fe0a:d808 dev eth3 proto ra metric 1024 expires 1789sec
RA route metric may vary, and it is preferred. The validity of default route is controlled by "default [route] lifetime" field in RA. If it is 0, then the default gateway announced is considered invalid, and no default route is installed into guest.
dnsmasq 2.67+ supports "ra-param=<interface>,<RA interval>,<default lifetime>" option. We can pass "ra-param=*,0,0" (here, RA_interval=0 means default) to disable default gateway in RA.
Nice detective work! It's good that somebody is actually using IPv6 and finding things like this. Since I'm in a bit of a rush today and this isn't a detailed nitpicky review anyway, I'm just going to put all my comments here in one place.
This patchset adds <network ipv6noDefRoute="yes|no"> option to network xml and sets the above option to dnsmasq config if it is set to yes (default: no).
* I understand why you would want it defaulted to set the default route (and that you probably got the idea for the name from the existing local variable in virNetworkDefParseXml()[1], but I have a great dislike for double-negative attributes as they tend to confuse me (and others too I hope :-). I would rather than the option was "ipv6DefRoute" and defaulted to "yes" (this takes a bit more setup in the code, but is worth it). * Beyond that, I think it would make more sense to have the option defined in the <ip> element for the IPv6 address rather than at the toplevel (I know there is already an option called "ipv6" at the toplevel, but that is a special case because it's telling what to do wrt IPv6 when there *aren't any* ipv6 <ip> elements in the network definition). A question: would it be possible to set multiple IPv6 addresses, and mark one of them as the default? If so, how would that be configured? * When you're checking for whether or not dnsmasq is able to support the option you're using, you base this on a dnsnasq version number. Is there any chance that the necessary info could be learned from the output of dnsmasq --help? Would it be adequate to just check for the presence of the string "--ra-param=" in the help output? This is already done to check for dnsmasq's use of SO_BINDTODEVICE - see dnsmasqCapsSetFromBuffer(). I'm guessing you based your addition on the existing code for DNSMASQ_DHCPv6_SUPPORT() and DNSMASQ_RA_SUPPORT(), but I think those were probably put in before the patches that added parsing of --help output to learn dnsmasq capabilities. * the split between the two patches is a bit odd compared to the way we normally add new features exposed in XML. Usually there would be 2 or 3 of the following, in the given order: 1) any new low level utilities needed (repeat as necessary), e.g. a patch adding a capabilities bit for the ability of dnsmasq to perform the needed function (it's not needed this time, but following this you could have patches adding new functionality in the utility functions for dnsmasq). 2) add the new config option to the schema, docs, parser/formatter, and xml2xml test cases (in a single patch) 3) add the code in the (in this case) network driver that examines the new config and capabilities flags, and if applicable calls the new low level utility to implement the config (in this case, there isn't a new low level function, you just add an extra argument to the dnsmasq commandline). [1] Although the internal variable is called "ipv6nogwStr" it is used to "enable guest to guest IPv6 communication" and is publicly named simply "ipv6", so there is no double negative visible to the outside world.
Maxim Perevedentsev (2): dnsmasq: add option to disable IPv6 default gateway in RA docs: add ipv6noDefRoute to schema and html.
docs/formatnetwork.html.in | 15 ++++++++++++++- docs/schemas/network.rng | 5 +++++ src/conf/network_conf.c | 17 +++++++++++++++++ src/conf/network_conf.h | 3 +++ src/network/bridge_driver.c | 22 ++++++++++++++++++++++ src/util/virdnsmasq.h | 6 ++++++ 6 files changed, 67 insertions(+), 1 deletion(-)
-- 1.8.3.1
-- libvir-list mailing list libvir-list@redhat.com https://www.redhat.com/mailman/listinfo/libvir-list

Laine, many thanks for such a detailed reply. On 06/29/2016 08:55 PM, Laine Stump wrote:
* Beyond that, I think it would make more sense to have the option defined in the <ip> element for the IPv6 address rather than at the toplevel
Why may we need it? We are talking about isolated networks, so what is the need for a gateway if all guests are in the same subnet? This is just what you fixed in a related commit 013427e6e733f7a662f4e8a9c11f7dad4cd65e3f. As I understand, the difference to IPv4 is that IPv6 RA cannot have empty default gateway. The link-local address of the source of RA is implicitly considered a gateway. And the only thing you can do is to set its lifetime to 0 to disable it. It occured to me that these fixes can be treated as an extension of aforementioned commit, and we should just add "ra-param=*,0,0" to dnsmasq config if we have a new enough version.
(I know there is already an option called "ipv6" at the toplevel, but that is a special case because it's telling what to do wrt IPv6 when there *aren't any* ipv6 <ip> elements in the network definition). A question: would it be possible to set multiple IPv6 addresses, and mark one of them as the default? If so, how would that be configured?
From "man dnsmasq": "When RA is enabled, dnsmasq will advertise a prefix for each dhcp-range, with default router and recursive DNS server as the relevant link-local address on the machine running dnsmasq." So it looks like this is impossible, at least for dnsmasq (I have not manage to make it work). A little of googling gave me that radvd supports default route, but it is not the case.
* When you're checking for whether or not dnsmasq is able to support the option you're using, you base this on a dnsnasq version number. Is there any chance that the necessary info could be learned from the output of dnsmasq --help? Would it be adequate to just check for the presence of the string "--ra-param=" in the help output? This is already done to check for dnsmasq's use of SO_BINDTODEVICE - see dnsmasqCapsSetFromBuffer(). I'm guessing you based your addition on the existing code for DNSMASQ_DHCPv6_SUPPORT() and DNSMASQ_RA_SUPPORT(), but I think those were probably put in before the patches that added parsing of --help output to learn dnsmasq capabilities.
OK -- Your sincerely, Maxim Perevedentsev

On 06/30/2016 08:02 AM, Maxim Perevedentsev wrote:
Laine, many thanks for such a detailed reply.
On 06/29/2016 08:55 PM, Laine Stump wrote:
* Beyond that, I think it would make more sense to have the option defined in the <ip> element for the IPv6 address rather than at the toplevel
Why may we need it? We are talking about isolated networks, so what is the need for a gateway if all guests are in the same subnet? This is just what you fixed in a related commit 013427e6e733f7a662f4e8a9c11f7dad4cd65e3f.
Well, there is no config attached to that at all. And now that you compare your patch to that patch (and remind me that I wrote it - even after reading the commit log, I *still* don't remember doing it! :-O), I don't think yours needs config either. Rather, I think it is *always* a bug that we are causing guests to get a (bogus) default route on a network that is designated as isolated.
As I understand, the difference to IPv4 is that IPv6 RA cannot have empty default gateway. The link-local address of the source of RA is implicitly considered a gateway. And the only thing you can do is to set its lifetime to 0 to disable it.
It occured to me that these fixes can be treated as an extension of aforementioned commit, and we should just add "ra-param=*,0,0" to dnsmasq config if we have a new enough version.
Yes, I agree. Current behavior is a bug that nobody could possibly want (the entire point of a network being "isolated" is that nothing can escape via that network; we even force the dns server on that network to never forward unresolvable requests), so libvirt should always disable it if dnsmasq allows.
(I know there is already an option called "ipv6" at the toplevel, but that is a special case because it's telling what to do wrt IPv6 when there *aren't any* ipv6 <ip> elements in the network definition). A question: would it be possible to set multiple IPv6 addresses, and mark one of them as the default? If so, how would that be configured?
From "man dnsmasq": "When RA is enabled, dnsmasq will advertise a prefix for each dhcp-range, with default router and recursive DNS server as the relevant link-local address on the machine running dnsmasq."
I guess I should spend some time brushing on on IPv6; I had thought that the link-local address on any interface was only used for things like address discovery, not for forwarding traffic.
So it looks like this is impossible, at least for dnsmasq (I have not manage to make it work). A little of googling gave me that radvd supports default route, but it is not the case.
* When you're checking for whether or not dnsmasq is able to support the option you're using, you base this on a dnsnasq version number. Is there any chance that the necessary info could be learned from the output of dnsmasq --help? Would it be adequate to just check for the presence of the string "--ra-param=" in the help output? This is already done to check for dnsmasq's use of SO_BINDTODEVICE - see dnsmasqCapsSetFromBuffer(). I'm guessing you based your addition on the existing code for DNSMASQ_DHCPv6_SUPPORT() and DNSMASQ_RA_SUPPORT(), but I think those were probably put in before the patches that added parsing of --help output to learn dnsmasq capabilities.
OK
participants (2)
-
Laine Stump
-
Maxim Perevedentsev