Weave Net clusters susceptible to MitM attacks via IPv6 rogue router advertisements
Description
Weave Net before 2.6.3 allows container root to perform IPv6 router advertisement attacks, enabling man-in-the-middle on the host.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Weave Net before 2.6.3 allows container root to perform IPv6 router advertisement attacks, enabling man-in-the-middle on the host.
Root
Cause In Weave Net prior to version 2.6.3, the virtual Ethernet (veth) devices used to connect containers to the Weave bridge did not disable IPv6 Router Advertisement (RA) acceptance. By default, the host sysctl net/ipv6/conf/*/accept_ra is set to 1, and IPv6 forwarding is disabled. This combination allows the host to process and accept RAs from any neighbor [1][3].
Exploitation
An attacker with root privileges in a container can craft and send rogue RAs to the host. Since the host accepts these RAs, the attacker can redirect IPv6 traffic intended for other hosts to their own container. Even if the cluster operates on an IPv4 network, many HTTP libraries will attempt IPv6 connections first if DNS returns AAAA records, providing an opportunity for the attacker to intercept traffic [1][3].
Impact
Successful exploitation enables a man-in-the-middle (MitM) attack, allowing the attacker to intercept, modify, or redirect traffic. If the host has additional vulnerabilities, such as CVE-2019-3462 (RCE in apt), the attacker could escalate to compromise the host entirely [1][3].
Mitigation
Weave Net version 2.6.3 patches the vulnerability by explicitly setting accept_ra to 0 on both ends of every veth pair it creates [2]. As a workaround, users should avoid running containers with CAP_NET_RAW capability, which is needed to send raw packets like RAs [3].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/weaveworks/weaveGo | < 2.6.3 | 2.6.3 |
Affected products
2- Range: < 2.6.3
Patches
115f21f189906Merge pull request #3801 from weaveworks/disable-accept-ra
7 files changed · +47 −35
net/bridge.go+15 −11 modified@@ -63,10 +63,10 @@ const ( ) type Bridge interface { - init(config *BridgeConfig) error // create and initialise bridge device(s) - attach(veth *netlink.Veth) error // attach veth to bridge - IsFastdp() bool // does this bridge use fastdp? - String() string // human-readable type string + init(procPath string, config *BridgeConfig) error // create and initialise bridge device(s) + attach(veth *netlink.Veth) error // attach veth to bridge + IsFastdp() bool // does this bridge use fastdp? + String() string // human-readable type string } // Used to indicate a fallback to the Bridge type @@ -250,7 +250,7 @@ func EnsureBridge(procPath string, config *BridgeConfig, log *logrus.Logger, ips } for { - if err := bridgeType.init(config); err != nil { + if err := bridgeType.init(procPath, config); err != nil { if errors.Cause(err) == errBridgeNotSupported { log.Warnf("Skipping bridge creation of %q due to: %s", bridgeType, err) bridgeType = bridgeImpl{} @@ -279,6 +279,10 @@ func EnsureBridge(procPath string, config *BridgeConfig, log *logrus.Logger, ips return bridgeType, errors.Wrap(err, "setting proxy_arp") } } + // No ipv6 router advertisments please + if err := sysctl(procPath, "net/ipv6/conf/"+config.WeaveBridgeName+"/accept_ra", "0"); err != nil { + return bridgeType, errors.Wrap(err, "setting accept_ra to 0") + } if err := linkSetUpByName(config.WeaveBridgeName); err != nil { return bridgeType, err @@ -345,11 +349,11 @@ func (b bridgeImpl) initPrep(config *BridgeConfig) error { return nil } -func (b bridgeImpl) init(config *BridgeConfig) error { +func (b bridgeImpl) init(procPath string, config *BridgeConfig) error { if err := b.initPrep(config); err != nil { return err } - if _, err := CreateAndAttachVeth(BridgeIfName, PcapIfName, config.WeaveBridgeName, config.MTU, true, false, func(veth netlink.Link) error { + if _, err := CreateAndAttachVeth(procPath, BridgeIfName, PcapIfName, config.WeaveBridgeName, config.MTU, true, false, func(veth netlink.Link) error { return netlink.LinkSetUp(veth) }); err != nil { return errors.Wrap(err, "creating pcap veth pair") @@ -361,7 +365,7 @@ func (b bridgeImpl) init(config *BridgeConfig) error { return nil } -func (f fastdpImpl) init(config *BridgeConfig) error { +func (f fastdpImpl) init(procPath string, config *BridgeConfig) error { odpSupported, err := odp.CreateDatapath(f.datapathName) if !odpSupported { msg := "" @@ -392,14 +396,14 @@ func (f fastdpImpl) init(config *BridgeConfig) error { return nil } -func (bf bridgedFastdpImpl) init(config *BridgeConfig) error { - if err := bf.fastdpImpl.init(config); err != nil { +func (bf bridgedFastdpImpl) init(procPath string, config *BridgeConfig) error { + if err := bf.fastdpImpl.init(procPath, config); err != nil { return err } if err := bf.bridgeImpl.initPrep(config); err != nil { return err } - if _, err := CreateAndAttachVeth(BridgeIfName, DatapathIfName, config.WeaveBridgeName, config.MTU, true, false, func(veth netlink.Link) error { + if _, err := CreateAndAttachVeth(procPath, BridgeIfName, DatapathIfName, config.WeaveBridgeName, config.MTU, true, false, func(veth netlink.Link) error { if err := netlink.LinkSetUp(veth); err != nil { return errors.Wrapf(err, "setting link up on %q", veth.Attrs().Name) }
net/netns.go+3 −3 modified@@ -55,9 +55,9 @@ func WithNetNSByPath(path string, work func() error) error { } func NSPathByPid(pid int) string { - return NSPathByPidWithRoot("/", pid) + return NSPathByPidWithProc("/proc", pid) } -func NSPathByPidWithRoot(root string, pid int) string { - return filepath.Join(root, fmt.Sprintf("/proc/%d/ns/net", pid)) +func NSPathByPidWithProc(procPath string, pid int) string { + return filepath.Join(procPath, fmt.Sprint(pid), "/ns/net") }
net/veth.go+17 −8 modified@@ -13,7 +13,7 @@ import ( ) // create and attach a veth to the Weave bridge -func CreateAndAttachVeth(name, peerName, bridgeName string, mtu int, keepTXOn bool, errIfLinkExist bool, init func(peer netlink.Link) error) (*netlink.Veth, error) { +func CreateAndAttachVeth(procPath, name, peerName, bridgeName string, mtu int, keepTXOn bool, errIfLinkExist bool, init func(peer netlink.Link) error) (*netlink.Veth, error) { bridge, err := netlink.LinkByName(bridgeName) if err != nil { return nil, fmt.Errorf(`bridge "%s" not present; did you launch weave?`, bridgeName) @@ -49,6 +49,13 @@ func CreateAndAttachVeth(name, peerName, bridgeName string, mtu int, keepTXOn bo if err := bridgeType.attach(veth); err != nil { return cleanup("attaching veth %q to %q: %s", name, bridgeName, err) } + // No ipv6 router advertisments please + if err := sysctl(procPath, "net/ipv6/conf/"+name+"/accept_ra", "0"); err != nil { + return cleanup("setting accept_ra to 0: %s", err) + } + if err := sysctl(procPath, "net/ipv6/conf/"+peerName+"/accept_ra", "0"); err != nil { + return cleanup("setting accept_ra to 0: %s", err) + } if !bridgeType.IsFastdp() && !keepTXOn { if err := EthtoolTXOff(veth.PeerName); err != nil { return cleanup(`unable to set tx off on %q: %s`, peerName, err) @@ -112,6 +119,9 @@ func interfaceExistsInNamespace(netNSPath string, ifName string) bool { } func AttachContainer(netNSPath, id, ifName, bridgeName string, mtu int, withMulticastRoute bool, cidrs []*net.IPNet, keepTXOn bool, hairpinMode bool) error { + // AttachContainer expects to be called in host pid namespace + const procPath = "/proc" + ns, err := netns.GetFromPath(netNSPath) if err != nil { return err @@ -124,12 +134,12 @@ func AttachContainer(netNSPath, id, ifName, bridgeName string, mtu int, withMult id = id[:maxIDLen] // trim passed ID if too long } name, peerName := vethPrefix+"pl"+id, vethPrefix+"pg"+id - veth, err := CreateAndAttachVeth(name, peerName, bridgeName, mtu, keepTXOn, true, func(veth netlink.Link) error { + veth, err := CreateAndAttachVeth(procPath, name, peerName, bridgeName, mtu, keepTXOn, true, func(veth netlink.Link) error { if err := netlink.LinkSetNsFd(veth, int(ns)); err != nil { return fmt.Errorf("failed to move veth to container netns: %s", err) } if err := WithNetNS(ns, func() error { - return setupIface(peerName, ifName) + return setupIface(procPath, peerName, ifName) }); err != nil { return fmt.Errorf("error setting up interface: %s", err) } @@ -206,7 +216,7 @@ func setupIfaceAddrs(veth netlink.Link, withMulticastRoute bool, cidrs []*net.IP } // setupIface expects to be called in the container's netns -func setupIface(ifaceName, newIfName string) error { +func setupIface(procPath, ifaceName, newIfName string) error { ipt, err := iptables.New() if err != nil { return err @@ -219,23 +229,22 @@ func setupIface(ifaceName, newIfName string) error { if err := netlink.LinkSetName(link, newIfName); err != nil { return err } - // This is only called by AttachContainer which is only called in host pid namespace - if err := configureARPCache("/proc", newIfName); err != nil { + if err := configureARPCache(procPath, newIfName); err != nil { return err } return ipt.Append("filter", "INPUT", "-i", newIfName, "-d", "224.0.0.0/4", "-j", "DROP") } // configureARP is a helper for the Docker plugin which doesn't set the addresses itself -func ConfigureARP(prefix, rootPath string) error { +func ConfigureARP(prefix, procPath string) error { links, err := netlink.LinkList() if err != nil { return err } for _, link := range links { ifName := link.Attrs().Name if strings.HasPrefix(ifName, prefix) { - configureARPCache(rootPath+"/proc", ifName) + configureARPCache(procPath, ifName) if addrs, err := netlink.AddrList(link, netlink.FAMILY_V4); err == nil { for _, addr := range addrs { arping.GratuitousArpOverIfaceByName(addr.IPNet.IP, ifName)
plugin/net/driver.go+4 −2 modified@@ -39,9 +39,10 @@ type driver struct { // used only by plugin-v2 forceMulticast bool networks map[string]network + procPath string } -func New(client *docker.Client, weave *weaveapi.Client, name, scope string, dns, isPluginV2, forceMulticast bool) (skel.Driver, error) { +func New(client *docker.Client, weave *weaveapi.Client, name, scope string, dns, isPluginV2, forceMulticast bool, procPath string) (skel.Driver, error) { driver := &driver{ name: name, scope: scope, @@ -51,6 +52,7 @@ func New(client *docker.Client, weave *weaveapi.Client, name, scope string, dns, // make sure that it's used only by plugin-v2 forceMulticast: isPluginV2 && forceMulticast, networks: make(map[string]network), + procPath: procPath, } // Do not start watcher in the case of plugin v2, which prevents us from @@ -134,7 +136,7 @@ func (driver *driver) CreateEndpoint(create *api.CreateEndpointRequest) (*api.Cr // create veths. note we assume endpoint IDs are unique in the first 9 chars name, peerName := vethPair(create.EndpointID) - if _, err := weavenet.CreateAndAttachVeth(name, peerName, weavenet.WeaveBridgeName, 0, false, true, nil); err != nil { + if _, err := weavenet.CreateAndAttachVeth(driver.procPath, name, peerName, weavenet.WeaveBridgeName, 0, false, true, nil); err != nil { return nil, driver.error("JoinEndpoint", "%s", err) }
plugin/net/watcher.go+2 −7 modified@@ -43,14 +43,9 @@ func (w *watcher) ContainerStarted(id string) { w.driver.warn("ContainerStarted", "unable to register %s with weaveDNS: %s", id, err) } } - rootDir := "/" - if w.driver.isPluginV2 { - // We bind mount host's /proc to /host/proc for plugin-v2 - rootDir = "/host" - } - netNSPath := weavenet.NSPathByPidWithRoot(rootDir, info.State.Pid) + netNSPath := weavenet.NSPathByPidWithProc(w.driver.procPath, info.State.Pid) if err := weavenet.WithNetNSByPath(netNSPath, func() error { - return weavenet.ConfigureARP(weavenet.VethName, rootDir) + return weavenet.ConfigureARP(weavenet.VethName, w.driver.procPath) }); err != nil { w.driver.warn("ContainerStarted", "unable to configure interfaces: %s", err) }
plugin/plugin.go+5 −4 modified@@ -32,6 +32,7 @@ type Config struct { EnableV2Multicast bool DNS bool DefaultSubnet string + ProcPath string // path to reach host /proc filesystem } type Plugin struct { @@ -62,15 +63,15 @@ func (plugin *Plugin) run(dockerClient *docker.Client, weave *weaveapi.Client, r endChan := make(chan error, 1) if plugin.Socket != "" { - globalListener, err := listenAndServe(dockerClient, weave, plugin.Socket, endChan, "global", false, plugin.DNS, plugin.EnableV2, plugin.EnableV2Multicast) + globalListener, err := listenAndServe(dockerClient, weave, plugin.Socket, endChan, "global", false, plugin.DNS, plugin.EnableV2, plugin.EnableV2Multicast, plugin.ProcPath) if err != nil { return err } defer os.Remove(plugin.Socket) defer globalListener.Close() } if plugin.MeshSocket != "" { - meshListener, err := listenAndServe(dockerClient, weave, plugin.MeshSocket, endChan, "local", true, plugin.DNS, plugin.EnableV2, plugin.EnableV2Multicast) + meshListener, err := listenAndServe(dockerClient, weave, plugin.MeshSocket, endChan, "local", true, plugin.DNS, plugin.EnableV2, plugin.EnableV2Multicast, plugin.ProcPath) if err != nil { return err } @@ -87,15 +88,15 @@ func (plugin *Plugin) run(dockerClient *docker.Client, weave *weaveapi.Client, r return <-endChan } -func listenAndServe(dockerClient *docker.Client, weave *weaveapi.Client, address string, endChan chan<- error, scope string, withIpam, dns bool, isPluginV2, forceMulticast bool) (net.Listener, error) { +func listenAndServe(dockerClient *docker.Client, weave *weaveapi.Client, address string, endChan chan<- error, scope string, withIpam, dns bool, isPluginV2, forceMulticast bool, procPath string) (net.Listener, error) { var name string if isPluginV2 { name = pluginV2Name } else { name = pluginNameFromAddress(address) } - d, err := netplugin.New(dockerClient, weave, name, scope, dns, isPluginV2, forceMulticast) + d, err := netplugin.New(dockerClient, weave, name, scope, dns, isPluginV2, forceMulticast, procPath) if err != nil { return nil, err }
prog/weaver/main.go+1 −0 modified@@ -453,6 +453,7 @@ func main() { pluginConfig.DNS = !noDNS pluginConfig.DefaultSubnet = defaultSubnet.String() + pluginConfig.ProcPath = procPath plugin := plugin.NewPlugin(pluginConfig) // The weave script always waits for a status call to succeed,
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-59qg-grp7-5r73ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-11091ghsaADVISORY
- github.com/weaveworks/weave/commit/15f21f1899060f7716c70a8555a084e836f39a60ghsax_refsource_MISCWEB
- github.com/weaveworks/weave/security/advisories/GHSA-59qg-grp7-5r73ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.