diff --git a/CHANGELOG.md b/CHANGELOG.md index 59351392..9b9cbc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## Release 1.0.2 + +This release fixes a number of bugs, including some security +vulnerabilities in the Weave Docker API proxy, hangs and failures in +address allocation, and sporadic failures in name resolution. + +More details in the +[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.0.2). + +The release is fully compatible with other 1.0.x versions, so existing +clusters can be upgraded incrementally. + ## Release 1.0.1 This is a bug fix release, addressing the following issue: diff --git a/Makefile b/Makefile index bfd649ee..8973e892 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ all: $(EXPORTS) travis: $(EXES) -update: $(EXES) +update: go get -u -f -v -tags -netgo $(addprefix ./,$(dir $(EXES))) $(WEAVER_EXE) $(WEAVEDNS_EXE) $(WEAVEPROXY_EXE) $(NETCHECK_EXE): common/*.go common/*/*.go net/*.go diff --git a/circle.yml b/circle.yml index 2a01b19f..b4d1b47e 100644 --- a/circle.yml +++ b/circle.yml @@ -26,6 +26,7 @@ dependencies: docker save weaveworks/weave-build >$WEAVE_BUILD; fi post: + - sudo apt-get update - sudo apt-get install bc - pip install requests - curl https://sdk.cloud.google.com | bash diff --git a/ipam/allocate.go b/ipam/allocate.go index 130380e9..0e4a9e4b 100644 --- a/ipam/allocate.go +++ b/ipam/allocate.go @@ -2,6 +2,7 @@ package ipam import ( "fmt" + "github.com/weaveworks/weave/ipam/address" ) @@ -42,9 +43,14 @@ func (g *allocate) Try(alloc *Allocator) bool { } // out of space - if donor, err := alloc.ring.ChoosePeerToAskForSpace(g.r.Start, g.r.End); err == nil { - alloc.debugln("Decided to ask peer", donor, "for space in range", g.r) - alloc.sendSpaceRequest(donor, g.r) + donors := alloc.ring.ChoosePeersToAskForSpace(g.r.Start, g.r.End) + for _, donor := range donors { + if err := alloc.sendSpaceRequest(donor, g.r); err != nil { + alloc.debugln("Problem asking peer", donor, "for space:", err) + } else { + alloc.debugln("Decided to ask peer", donor, "for space in range", g.r) + break + } } return false diff --git a/ipam/allocator.go b/ipam/allocator.go index 8b609b96..9dee326a 100644 --- a/ipam/allocator.go +++ b/ipam/allocator.go @@ -202,10 +202,9 @@ func (alloc *Allocator) Lookup(ident string, r address.Range) (address.Address, } // Claim an address that we think we should own (Sync) -func (alloc *Allocator) Claim(ident string, addr address.Address, cancelChan <-chan bool) error { +func (alloc *Allocator) Claim(ident string, addr address.Address) error { resultChan := make(chan error) - op := &claim{resultChan: resultChan, ident: ident, addr: addr, - hasBeenCancelled: hasBeenCancelled(cancelChan)} + op := &claim{resultChan: resultChan, ident: ident, addr: addr} alloc.doOperation(op, &alloc.pendingClaims) return <-resultChan } @@ -575,14 +574,14 @@ func (alloc *Allocator) propose() { alloc.gossip.GossipBroadcast(alloc.Gossip()) } -func (alloc *Allocator) sendSpaceRequest(dest router.PeerName, r address.Range) { +func (alloc *Allocator) sendSpaceRequest(dest router.PeerName, r address.Range) error { buf := new(bytes.Buffer) enc := gob.NewEncoder(buf) if err := enc.Encode(r); err != nil { panic(err) } msg := router.Concat([]byte{msgSpaceRequest}, buf.Bytes()) - alloc.gossip.GossipUnicast(dest, msg) + return alloc.gossip.GossipUnicast(dest, msg) } func (alloc *Allocator) sendRingUpdate(dest router.PeerName) { diff --git a/ipam/allocator_test.go b/ipam/allocator_test.go index d32e9174..8f6683ce 100644 --- a/ipam/allocator_test.go +++ b/ipam/allocator_test.go @@ -143,7 +143,7 @@ func TestAllocatorClaim(t *testing.T) { alloc.claimRingForTesting() addr1, _ := address.ParseIP(testAddr1) - err := alloc.Claim(container3, addr1, nil) + err := alloc.Claim(container3, addr1) wt.AssertNoErr(t, err) // Check we get this address back if we try an allocate addr3, _ := alloc.Allocate(container3, subnet, nil) @@ -168,7 +168,7 @@ func TestCancel(t *testing.T) { CIDR = "10.0.1.7/26" ) - router := TestGossipRouter{make(map[router.PeerName]chan gossipMessage), 0.0} + router := TestGossipRouter{make(map[router.PeerName]chan interface{}), 0.0} alloc1, subnet := makeAllocator("01:00:00:02:00:00", CIDR, 2) alloc1.SetInterfaces(router.connect(alloc1.ourName, alloc1)) @@ -256,13 +256,17 @@ func TestTransfer(t *testing.T) { wt.AssertTrue(t, err == nil, "Failed to get address") router.GossipBroadcast(alloc2.Gossip()) + router.flush() router.GossipBroadcast(alloc3.Gossip()) + router.flush() router.removePeer(alloc2.ourName) router.removePeer(alloc3.ourName) alloc2.Stop() alloc3.Stop() + router.flush() wt.AssertSuccess(t, alloc1.AdminTakeoverRanges(alloc2.ourName.String())) wt.AssertSuccess(t, alloc1.AdminTakeoverRanges(alloc3.ourName.String())) + router.flush() wt.AssertEquals(t, alloc1.NumFreeAddresses(subnet), address.Offset(1022)) @@ -282,7 +286,8 @@ func TestFakeRouterSimple(t *testing.T) { alloc1 := allocs[0] //alloc2 := allocs[1] - alloc1.Allocate("foo", subnet, nil) + _, err := alloc1.Allocate("foo", subnet, nil) + wt.AssertTrue(t, err == nil, "Failed to get address") } func TestAllocatorFuzz(t *testing.T) { diff --git a/ipam/claim.go b/ipam/claim.go index 8d96e6e2..c1dbc529 100644 --- a/ipam/claim.go +++ b/ipam/claim.go @@ -3,28 +3,35 @@ package ipam import ( "fmt" + "github.com/weaveworks/weave/common" "github.com/weaveworks/weave/ipam/address" "github.com/weaveworks/weave/router" ) type claim struct { - resultChan chan<- error - ident string - addr address.Address - hasBeenCancelled func() bool + resultChan chan<- error + ident string + addr address.Address +} + +func (c *claim) sendResult(result error) { + // Make sure we only send a result once, since listener stops listening after that + if c.resultChan != nil { + c.resultChan <- result + close(c.resultChan) + c.resultChan = nil + } + if result != nil { + common.Error.Println("[allocator] " + result.Error()) + } } // Try returns true for success (or failure), false if we need to try again later func (c *claim) Try(alloc *Allocator) bool { - if (c.hasBeenCancelled)() { - c.Cancel() - return true - } - if !alloc.ring.Contains(c.addr) { // Address not within our universe; assume user knows what they are doing alloc.infof("Ignored address %s claimed by %s - not in our universe\n", c.addr, c.ident) - c.resultChan <- nil + c.sendResult(nil) return true } @@ -34,6 +41,7 @@ func (c *claim) Try(alloc *Allocator) bool { owner := alloc.ring.Owner(c.addr) if owner == router.UnknownPeerName { alloc.infof("Ring is empty; will try later.\n", c.addr, owner) + c.sendResult(nil) // don't make the caller wait return false } if owner != alloc.ourName { @@ -41,34 +49,34 @@ func (c *claim) Try(alloc *Allocator) bool { if found { name = " (" + name + ")" } - c.resultChan <- fmt.Errorf("address %s is owned by other peer %s%s", c.addr.String(), owner, name) + c.sendResult(fmt.Errorf("address %s is owned by other peer %s%s", c.addr.String(), owner, name)) return true } // We are the owner, check we haven't given it to another container existingIdent := alloc.findOwner(c.addr) if existingIdent == c.ident { // same identifier is claiming same address; that's OK - c.resultChan <- nil + c.sendResult(nil) return true } if existingIdent == "" { err := alloc.space.Claim(c.addr) if err != nil { - c.resultChan <- err + c.sendResult(err) return true } alloc.debugln("Claimed", c.addr, "for", c.ident) alloc.addOwned(c.ident, c.addr) - c.resultChan <- nil + c.sendResult(nil) return true } // Addr already owned by container on this machine - c.resultChan <- fmt.Errorf("address %s is already owned by %s", c.addr.String(), existingIdent) + c.sendResult(fmt.Errorf("address %s is already owned by %s", c.addr.String(), existingIdent)) return true } func (c *claim) Cancel() { - c.resultChan <- fmt.Errorf("Operation cancelled.") + c.sendResult(fmt.Errorf("Operation cancelled.")) } func (c *claim) String() string { diff --git a/ipam/http.go b/ipam/http.go index 8164d8c8..c81ee2ba 100644 --- a/ipam/http.go +++ b/ipam/http.go @@ -19,14 +19,13 @@ func badRequest(w http.ResponseWriter, err error) { // HandleHTTP wires up ipams HTTP endpoints to the provided mux. func (alloc *Allocator) HandleHTTP(router *mux.Router, defaultSubnet address.CIDR, dockerCli *docker.Client) { router.Methods("PUT").Path("/ip/{id}/{ip}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - closedChan := w.(http.CloseNotifier).CloseNotify() vars := mux.Vars(r) ident := vars["id"] ipStr := vars["ip"] if ip, err := address.ParseIP(ipStr); err != nil { badRequest(w, err) return - } else if err := alloc.Claim(ident, ip, closedChan); err != nil { + } else if err := alloc.Claim(ident, ip); err != nil { badRequest(w, fmt.Errorf("Unable to claim: %s", err)) return } diff --git a/ipam/paxos/paxos.go b/ipam/paxos/paxos.go index a69b91da..61fda181 100644 --- a/ipam/paxos/paxos.go +++ b/ipam/paxos/paxos.go @@ -2,6 +2,7 @@ package paxos import ( "fmt" + "github.com/weaveworks/weave/router" ) diff --git a/ipam/paxos/paxos_test.go b/ipam/paxos/paxos_test.go index cd704c5c..c1d5bfe0 100644 --- a/ipam/paxos/paxos_test.go +++ b/ipam/paxos/paxos_test.go @@ -2,10 +2,11 @@ package paxos import ( "fmt" - "github.com/weaveworks/weave/router" "math/rand" "testing" "time" + + "github.com/weaveworks/weave/router" ) type TestNode struct { diff --git a/ipam/ring/ring.go b/ipam/ring/ring.go index fcdb362e..ac964b8d 100644 --- a/ipam/ring/ring.go +++ b/ipam/ring/ring.go @@ -348,9 +348,9 @@ func (r *Ring) String() string { return buffer.String() } -// ReportFree is used by the allocator to tell the ring -// how many free ips are in a given range, so that ChoosePeerToAskForSpace -// can make more intelligent decisions. +// ReportFree is used by the allocator to tell the ring how many free +// ips are in a given range, so that ChoosePeersToAskForSpace can make +// more intelligent decisions. func (r *Ring) ReportFree(freespace map[address.Address]address.Offset) { r.assertInvariants() defer r.assertInvariants() @@ -392,9 +392,20 @@ func (r *Ring) ReportFree(freespace map[address.Address]address.Offset) { } } -// ChoosePeerToAskForSpace chooses a weighted-random peer to ask -// for space in the range [start, end). Assumes start so that bigger weights sort earlier +func (ws weightedPeers) Less(i, j int) bool { return ws[i].weight > ws[j].weight } +func (ws weightedPeers) Len() int { return len(ws) } +func (ws weightedPeers) Swap(i, j int) { ws[i], ws[j] = ws[j], ws[i] } + +// ChoosePeersToAskForSpace returns all peers we can ask for space in +// the range [start, end), in weighted-random order. Assumes start (1.0 - grouter.loss) { continue } + if err := gossiper.OnGossipUnicast(message.sender, message.buf); err != nil { + panic(fmt.Sprintf("Error doing gossip unicast to %s: %s", message.sender, err)) + } - if message.isUnicast { - if err := gossiper.OnGossipUnicast(*message.sender, message.buf); err != nil { - panic(fmt.Sprintf("Error doing gossip unicast to %s: %s", sender, err)) - } - } else { - for _, msg := range message.data.Encode() { - if _, err := gossiper.OnGossipBroadcast(msg); err != nil { - panic(fmt.Sprintf("Error doing gossip broadcast to %s: %s", sender, err)) - } + case broadcastMessage: + if rand.Float32() > (1.0 - grouter.loss) { + continue + } + for _, msg := range message.data.Encode() { + if _, err := gossiper.OnGossipBroadcast(msg); err != nil { + panic(fmt.Sprintf("Error doing gossip broadcast: %s", err)) } } - case <-gossipTimer: - grouter.GossipBroadcast(gossiper.Gossip()) } + case <-gossipTimer: + grouter.GossipBroadcast(gossiper.Gossip()) } - }() + } +} + +func (grouter *TestGossipRouter) connect(sender router.PeerName, gossiper router.Gossiper) router.Gossip { + gossipChan := make(chan interface{}, 100) + + go grouter.run(gossiper, gossipChan) grouter.gossipChans[sender] = gossipChan return TestGossipRouterClient{grouter, sender} @@ -276,7 +298,7 @@ func (grouter *TestGossipRouter) connect(sender router.PeerName, gossiper router func (client TestGossipRouterClient) GossipUnicast(dstPeerName router.PeerName, buf []byte) error { select { - case client.router.gossipChans[dstPeerName] <- gossipMessage{isUnicast: true, sender: &client.sender, buf: buf}: + case client.router.gossipChans[dstPeerName] <- unicastMessage{sender: client.sender, buf: buf}: default: // drop the message if we cannot send it common.Error.Printf("Dropping message") } @@ -289,7 +311,7 @@ func (client TestGossipRouterClient) GossipBroadcast(update router.GossipData) e func makeNetworkOfAllocators(size int, cidr string) ([]*Allocator, TestGossipRouter, address.Range) { - gossipRouter := TestGossipRouter{make(map[router.PeerName]chan gossipMessage), 0.0} + gossipRouter := TestGossipRouter{make(map[router.PeerName]chan interface{}), 0.0} allocs := make([]*Allocator, size) var subnet address.Range @@ -303,7 +325,7 @@ func makeNetworkOfAllocators(size int, cidr string) ([]*Allocator, TestGossipRou } gossipRouter.GossipBroadcast(allocs[size-1].Gossip()) - time.Sleep(1000 * time.Millisecond) + gossipRouter.flush() return allocs, gossipRouter, subnet } diff --git a/nameserver/server.go b/nameserver/server.go index a7b84bc7..26a0a607 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -395,6 +395,9 @@ func (s *DNSServer) notUsHandler(proto dnsProtocol) dns.HandlerFunc { } Debug.Printf("[dns msgid %d] Given answer by %s for query %s", r.MsgHdr.Id, server, q.Name) + if reply.Len() > maxLen { + reply.Compress = true + } w.WriteMsg(reply) return } diff --git a/nameserver/server_test.go b/nameserver/server_test.go index feba9997..08a3cfdb 100644 --- a/nameserver/server_test.go +++ b/nameserver/server_test.go @@ -3,12 +3,14 @@ package nameserver import ( "encoding/binary" "fmt" + "math/rand" "net" "testing" "time" "github.com/miekg/dns" . "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" wt "github.com/weaveworks/weave/testing" ) @@ -32,11 +34,12 @@ func TestUDPDNSServer(t *testing.T) { setupForTest(t) const ( - successTestName = "test1.weave.local." - failTestName = "fail.weave.local." - nonLocalName = "weave.works." - testAddr1 = "10.2.2.1" - containerID = "somecontainer" + successTestName = "test1.weave.local." + failTestName = "fail.weave.local." + compressedTestName = "compressed.weave.works." + nonLocalName = "weave.works." + testAddr1 = "10.2.2.1" + containerID = "somecontainer" ) testCIDR1 := testAddr1 + "/24" @@ -72,6 +75,15 @@ func TestUDPDNSServer(t *testing.T) { m.Answer[0] = &dns.PTR{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, Ptr: "ns1.google.com."} } else if q.Name == testRDNSfail && q.Qtype == dns.TypePTR { m.Rcode = dns.RcodeNameError + } else if q.Name == compressedTestName { + // Construct a response that is >512 when uncompressed, <512 when compressed + header := makeHeader(m, &q, 300) + for m.Len() <= minUDPSize { + ip := address.Address(rand.Uint32()).IP4() + m.Answer = append(m.Answer, &dns.A{Hdr: *header, A: ip}) + } + m.Compress = true + wt.AssertTrue(t, m.Len() <= minUDPSize, fmt.Sprintf("length: %d", m.Len())) } w.WriteMsg(m) } @@ -125,6 +137,9 @@ func TestUDPDNSServer(t *testing.T) { assertExchange(t, testRDNSnonlocal, dns.TypePTR, testPort, 1, -1, 0) + _, r = assertExchange(t, compressedTestName, dns.TypeA, testPort, -1, -1, 0) + wt.AssertTrue(t, r.Len() > minUDPSize, "unexpected message length") + // Not testing MDNS functionality of server here (yet), since it // needs two servers, each listening on its own address } diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go index 5c5aacd6..00d5aee0 100644 --- a/nameserver/zone_lookup.go +++ b/nameserver/zone_lookup.go @@ -103,9 +103,11 @@ func (zone *ZoneDb) domainLookup(target string, lfun ZoneLookupFunc) (res []Zone for _, answer := range lanswers { r, err := remoteIdent.addIPToName(answer, now) if err != nil { - zone.mx.Unlock() - Warning.Printf("[zonedb] '%s' insertion for %s failed: %s", answer, target, err) - return nil, err + if _, ok := err.(DuplicateError); !ok { + zone.mx.Unlock() + Warning.Printf("[zonedb] '%s' insertion for %s failed: %s", answer, target, err) + return nil, err + } } uniq.add(r) } diff --git a/net/if.go b/net/if.go index 9cea43bc..3cf3baab 100644 --- a/net/if.go +++ b/net/if.go @@ -2,6 +2,7 @@ package net import ( "fmt" + "math" "net" "time" ) @@ -10,11 +11,17 @@ const ( sleepTime = 100 * time.Millisecond ) +// Wait `wait` seconds for an interface to come up. Pass zero to check once +// and return immediately, or a negative value to wait indefinitely. func EnsureInterface(ifaceName string, wait int) (iface *net.Interface, err error) { if iface, err = findInterface(ifaceName); err == nil || wait == 0 { return } - for i := time.Duration(wait) * time.Second / sleepTime; err != nil && i > 0; i-- { + i := int64(math.MaxInt64) + if wait > 0 { + i = int64(time.Duration(wait) * time.Second / sleepTime) + } + for ; err != nil && i > 0; i-- { time.Sleep(sleepTime) iface, err = findInterface(ifaceName) } diff --git a/prog/weavedns/Dockerfile b/prog/weavedns/Dockerfile index d7af8837..883b34b8 100644 --- a/prog/weavedns/Dockerfile +++ b/prog/weavedns/Dockerfile @@ -3,4 +3,4 @@ MAINTAINER Weaveworks Inc WORKDIR /home/weave ADD ./weavedns /home/weave/ EXPOSE 53/udp -ENTRYPOINT ["/home/weave/weavedns", "-wait", "20"] +ENTRYPOINT ["/home/weave/weavedns"] diff --git a/prog/weavedns/main.go b/prog/weavedns/main.go index 21193da6..69832eaf 100644 --- a/prog/weavedns/main.go +++ b/prog/weavedns/main.go @@ -46,7 +46,7 @@ func main() { flag.StringVar(&ifaceName, "iface", "", "name of interface to use for multicast") flag.StringVar(&apiPath, "api", "unix:///var/run/docker.sock", "path to Docker API socket") flag.StringVar(&domain, "domain", weavedns.DefaultLocalDomain, "local domain (ie, 'weave.local.')") - flag.IntVar(&wait, "wait", 0, "number of seconds to wait for interface to be created and come up") + flag.IntVar(&wait, "wait", -1, "number of seconds to wait for interfaces to come up (0=don't wait, -1=wait forever)") flag.IntVar(&dnsPort, "dnsport", weavedns.DefaultServerPort, "port to listen to DNS requests") flag.StringVar(&httpIfaceName, "httpiface", "", "interface on which to listen for HTTP requests (defaults to empty string which listens on all interfaces)") flag.IntVar(&httpPort, "httpport", weavedns.DefaultHTTPPort, "port to listen to HTTP requests") diff --git a/prog/weaveproxy/main.go b/prog/weaveproxy/main.go index c896d872..d110088b 100644 --- a/prog/weaveproxy/main.go +++ b/prog/weaveproxy/main.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "code.google.com/p/getopt" + "github.com/pborman/getopt" . "github.com/weaveworks/weave/common" "github.com/weaveworks/weave/proxy" ) diff --git a/prog/weaver/Dockerfile b/prog/weaver/Dockerfile index 437fc9e0..e91764e0 100644 --- a/prog/weaver/Dockerfile +++ b/prog/weaver/Dockerfile @@ -2,4 +2,4 @@ FROM scratch MAINTAINER Weaveworks Inc WORKDIR /home/weave ADD ./weaver /home/weave/ -ENTRYPOINT ["/home/weave/weaver", "-wait", "20"] +ENTRYPOINT ["/home/weave/weaver"] diff --git a/prog/weaver/main.go b/prog/weaver/main.go index d0ac8596..03f2412c 100644 --- a/prog/weaver/main.go +++ b/prog/weaver/main.go @@ -11,8 +11,8 @@ import ( "runtime" "strings" - "code.google.com/p/gopacket/layers" "github.com/davecheney/profile" + "github.com/google/gopacket/layers" "github.com/gorilla/mux" . "github.com/weaveworks/weave/common" "github.com/weaveworks/weave/common/docker" @@ -64,7 +64,7 @@ func main() { flag.StringVar(&routerName, "name", "", "name of router (defaults to MAC of interface)") flag.StringVar(&nickName, "nickname", "", "nickname of peer (defaults to hostname)") flag.StringVar(&password, "password", "", "network password") - flag.IntVar(&wait, "wait", 0, "number of seconds to wait for interface to be created and come up (0 = don't wait)") + flag.IntVar(&wait, "wait", -1, "number of seconds to wait for interface to come up (0=don't wait, -1=wait forever)") flag.BoolVar(&debug, "debug", false, "enable debug logging") flag.BoolVar(&pktdebug, "pktdebug", false, "enable per-packet debug logging") flag.StringVar(&prof, "profile", "", "enable profiling and write profiles to given path") diff --git a/prog/weavewait/main.go b/prog/weavewait/main.go index 3cb78663..d88e89f3 100644 --- a/prog/weavewait/main.go +++ b/prog/weavewait/main.go @@ -6,33 +6,29 @@ import ( "os/exec" "os/signal" "syscall" - "time" - "github.com/weaveworks/weave/net" + weavenet "github.com/weaveworks/weave/net" + "github.com/weaveworks/weave/proxy" ) func main() { - if len(os.Args) <= 1 { - os.Exit(0) - } - args := os.Args[1:] - signalWait := 20 - if args[0] == "-s" { - signalWait = 0 - args = args[1:] - } - interfaceWait := 20 - signalWait - usr2 := make(chan os.Signal) - signal.Notify(usr2, syscall.SIGUSR2) - select { - case <-usr2: - case <-time.After(time.Duration(signalWait) * time.Second): + if len(args) > 0 && args[0] == "-s" { + args = args[1:] + } else { + usr2 := make(chan os.Signal) + signal.Notify(usr2, syscall.SIGUSR2) + <-usr2 } - _, err := net.EnsureInterface("ethwe", interfaceWait) + + _, err := weavenet.EnsureInterface("ethwe", -1) checkErr(err) + if len(args) == 0 { + checkErr(proxy.ErrNoCommandSpecified) + } + binary, err := exec.LookPath(args[0]) checkErr(err) diff --git a/proxy/common.go b/proxy/common.go index 45ed79d8..7c7775c4 100644 --- a/proxy/common.go +++ b/proxy/common.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" + "os" "os/exec" "regexp" @@ -22,7 +23,13 @@ func callWeave(args ...string) ([]byte, error) { args = append([]string{"--local"}, args...) Debug.Print("Calling weave", args) cmd := exec.Command("./weave", args...) - cmd.Env = []string{"PROCFS=/hostproc", "PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"} + cmd.Env = []string{ + "PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PROCFS=/hostproc", + } + if bridge := os.Getenv("DOCKER_BRIDGE"); bridge != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_BRIDGE=%s", bridge)) + } out, err := cmd.CombinedOutput() return out, err } diff --git a/proxy/create_container_interceptor.go b/proxy/create_container_interceptor.go index e12d5fa4..42b60ed1 100644 --- a/proxy/create_container_interceptor.go +++ b/proxy/create_container_interceptor.go @@ -3,6 +3,7 @@ package proxy import ( "bytes" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -13,6 +14,12 @@ import ( "github.com/weaveworks/weave/nameserver" ) +const MaxDockerHostname = 64 + +var ( + ErrNoCommandSpecified = errors.New("No command specified") +) + type createContainerInterceptor struct{ proxy *Proxy } type createContainerRequestBody struct { @@ -21,9 +28,9 @@ type createContainerRequestBody struct { MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty"` } -// Replacement for docker.NoSuchImage, which does not contain the -// image name, which in turn breaks docker clients post 1.7.0 since -// they expect the image name to be present in errors. +// ErrNoSuchImage replaces docker.NoSuchImage, which does not contain the image +// name, which in turn breaks docker clients post 1.7.0 since they expect the +// image name to be present in errors. type ErrNoSuchImage struct { Name string } @@ -44,15 +51,17 @@ func (i *createContainerInterceptor) InterceptRequest(r *http.Request) error { return err } - if cidrs, ok := i.proxy.weaveCIDRsFromConfig(container.Config); ok { + if cidrs, err := i.proxy.weaveCIDRsFromConfig(container.Config, container.HostConfig); err != nil { + Info.Printf("Ignoring container due to %s", err) + } else { Info.Printf("Creating container with WEAVE_CIDR \"%s\"", strings.Join(cidrs, " ")) if container.HostConfig == nil { container.HostConfig = &docker.HostConfig{} } - container.HostConfig.VolumesFrom = append(container.HostConfig.VolumesFrom, "weaveproxy") if container.Config == nil { container.Config = &docker.Config{} } + i.addWeaveWaitVolume(container.HostConfig) if err := i.setWeaveWaitEntrypoint(container.Config); err != nil { return err } @@ -71,6 +80,18 @@ func (i *createContainerInterceptor) InterceptRequest(r *http.Request) error { return nil } +func (i *createContainerInterceptor) addWeaveWaitVolume(hostConfig *docker.HostConfig) { + var binds []string + for _, bind := range hostConfig.Binds { + s := strings.Split(bind, ":") + if len(s) >= 2 && s[1] == "/w" { + continue + } + binds = append(binds, bind) + } + hostConfig.Binds = append(binds, fmt.Sprintf("%s:/w:ro", i.proxy.weaveWaitVolume)) +} + func (i *createContainerInterceptor) setWeaveWaitEntrypoint(container *docker.Config) error { if len(container.Entrypoint) == 0 { image, err := i.proxy.client.InspectImage(container.Image) @@ -89,7 +110,14 @@ func (i *createContainerInterceptor) setWeaveWaitEntrypoint(container *docker.Co } } - container.Entrypoint = append(weaveWaitEntrypoint, container.Entrypoint...) + if len(container.Entrypoint) == 0 && len(container.Cmd) == 0 { + return ErrNoCommandSpecified + } + + if len(container.Entrypoint) == 0 || container.Entrypoint[0] != weaveWaitEntrypoint[0] { + container.Entrypoint = append(weaveWaitEntrypoint, container.Entrypoint...) + } + return nil } @@ -107,9 +135,14 @@ func (i *createContainerInterceptor) setWeaveDNS(container *createContainerReque name := r.URL.Query().Get("name") if container.Hostname == "" && name != "" { - container.Hostname = name // Strip trailing period because it's unusual to see it used on the end of a host name - container.Domainname = strings.TrimSuffix(dnsDomain, ".") + trimmedDNSDomain := strings.TrimSuffix(dnsDomain, ".") + if len(name)+1+len(trimmedDNSDomain) > MaxDockerHostname { + Warning.Printf("Container name [%s] too long to be used as hostname", name) + } else { + container.Hostname = name + container.Domainname = trimmedDNSDomain + } } if len(container.HostConfig.DNSSearch) == 0 { diff --git a/proxy/create_exec_interceptor.go b/proxy/create_exec_interceptor.go index 72e62730..f15ed56f 100644 --- a/proxy/create_exec_interceptor.go +++ b/proxy/create_exec_interceptor.go @@ -29,7 +29,11 @@ func (i *createExecInterceptor) InterceptRequest(r *http.Request) error { return err } - if cidrs, ok := i.proxy.weaveCIDRsFromConfig(container.Config); ok { + _, hasWeaveWait := container.Volumes["/w"] + cidrs, err := i.proxy.weaveCIDRsFromConfig(container.Config, container.HostConfig) + if err != nil { + Info.Printf("Ignoring container %s due to %s", container.ID, err) + } else if hasWeaveWait { Info.Printf("Exec in container %s with WEAVE_CIDR \"%s\"", container.ID, strings.Join(cidrs, " ")) cmd := append(weaveWaitEntrypoint, "-s") options.Cmd = append(cmd, options.Cmd...) diff --git a/proxy/proxy.go b/proxy/proxy.go index c391431f..83664800 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -2,6 +2,8 @@ package proxy import ( "crypto/tls" + "errors" + "fmt" "net" "net/http" "net/url" @@ -22,14 +24,18 @@ var ( containerCreateRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/create$") containerStartRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/[^/]*/(re)?start$") execCreateRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/[^/]*/exec$") + + ErrWeaveCIDRNone = errors.New("WEAVE_CIDR=none") + ErrNoDefaultIPAM = errors.New("--no-default-ipam option") ) type Proxy struct { Config - dial func() (net.Conn, error) - client *docker.Client - dockerBridgeIP string + dial func() (net.Conn, error) + client *docker.Client + dockerBridgeIP string + weaveWaitVolume string } type Config struct { @@ -75,14 +81,41 @@ func NewProxy(c Config) (*Proxy, error) { Error.Fatalf("Could not configure tls for proxy: %s", err) } - p.client, err = docker.NewClient(p.DockerAddr) + // We pin the protocol version to 1.15 (which corresponds to Docker 1.3.x; + // the earliest version supported by weave) in order to insulate ourselves + // from breaking changes to the API, as happened in 1.20 (Docker 1.8.0) when + // the presentation of volumes changed in inspect. + p.client, err = docker.NewVersionedClient(p.DockerAddr, "1.15") if err != nil { return nil, err } + if err = p.findWeaveWaitVolume(); err != nil { + return nil, err + } + return p, nil } +func (proxy *Proxy) findWeaveWaitVolume() error { + container, err := proxy.client.InspectContainer("weaveproxy") + if err != nil { + return fmt.Errorf("Could not find the weavewait volume: %s", err) + } + + if container.Volumes == nil { + return fmt.Errorf("Could not find the weavewait volume") + } + + volume, ok := container.Volumes["/w"] + if !ok { + return fmt.Errorf("Could not find the weavewait volume") + } + + proxy.weaveWaitVolume = volume + return nil +} + func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { Info.Printf("%s %s", r.Method, r.URL) path := r.URL.Path @@ -115,14 +148,23 @@ func (proxy *Proxy) ListenAndServe() error { return (&http.Server{Handler: proxy}).Serve(listener) } -func (proxy *Proxy) weaveCIDRsFromConfig(config *docker.Config) ([]string, bool) { +func (proxy *Proxy) weaveCIDRsFromConfig(config *docker.Config, hostConfig *docker.HostConfig) ([]string, error) { + if hostConfig != nil && + hostConfig.NetworkMode != "" && + hostConfig.NetworkMode != "default" && + hostConfig.NetworkMode != "bridge" { + return nil, fmt.Errorf("--net option: %q", hostConfig.NetworkMode) + } for _, e := range config.Env { if strings.HasPrefix(e, "WEAVE_CIDR=") { if e[11:] == "none" { - return nil, false + return nil, ErrWeaveCIDRNone } - return strings.Fields(e[11:]), true + return strings.Fields(e[11:]), nil } } - return nil, !proxy.NoDefaultIPAM + if proxy.NoDefaultIPAM { + return nil, ErrNoDefaultIPAM + } + return nil, nil } diff --git a/proxy/proxy_intercept.go b/proxy/proxy_intercept.go index 69f83c83..5dd109db 100644 --- a/proxy/proxy_intercept.go +++ b/proxy/proxy_intercept.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/http/httputil" + "sync" "github.com/fsouza/go-dockerclient" . "github.com/weaveworks/weave/common" @@ -94,25 +95,30 @@ func doRawStream(w http.ResponseWriter, resp *http.Response, client *httputil.Cl return } - upDone := make(chan struct{}) - downDone := make(chan struct{}) - go copyStream(down, io.MultiReader(rem, up), upDone) - go copyStream(up, downBuf, downDone) - <-upDone - <-downDone + var wg sync.WaitGroup + wg.Add(2) + go copyStream(down, io.MultiReader(rem, up), &wg) + go copyStream(up, downBuf, &wg) + wg.Wait() } -func copyStream(dst io.Writer, src io.Reader, done chan struct{}) { - defer close(done) +type closeWriter interface { + CloseWrite() error +} + +func copyStream(dst io.WriteCloser, src io.Reader, wg *sync.WaitGroup) { + defer wg.Done() if _, err := io.Copy(dst, src); err != nil { Warning.Print(err) } - if c, ok := dst.(interface { - CloseWrite() error - }); ok { - if err := c.CloseWrite(); err != nil { - Warning.Printf("Error closing connection: %s", err) - } + var err error + if c, ok := dst.(closeWriter); ok { + err = c.CloseWrite() + } else { + err = dst.Close() + } + if err != nil { + Warning.Printf("Error closing connection: %s", err) } } diff --git a/proxy/start_container_interceptor.go b/proxy/start_container_interceptor.go index 4d54680d..9dc1411d 100644 --- a/proxy/start_container_interceptor.go +++ b/proxy/start_container_interceptor.go @@ -16,14 +16,18 @@ func (i *startContainerInterceptor) InterceptRequest(r *http.Request) error { } func (i *startContainerInterceptor) InterceptResponse(r *http.Response) error { + if r.StatusCode < 200 || r.StatusCode >= 300 { // Docker didn't do the start + return nil + } + container, err := inspectContainerInPath(i.proxy.client, r.Request.URL.Path) if err != nil { return err } - cidrs, ok := i.proxy.weaveCIDRsFromConfig(container.Config) - if !ok { - Debug.Print("No Weave CIDR, ignoring") + cidrs, err := i.proxy.weaveCIDRsFromConfig(container.Config, container.HostConfig) + if err != nil { + Info.Printf("Ignoring container %s due to %s", container.ID, err) return nil } Info.Printf("Attaching container %s with WEAVE_CIDR \"%s\" to weave network", container.ID, strings.Join(cidrs, " ")) diff --git a/router/connection.go b/router/connection.go index adf9e04c..81c74d30 100644 --- a/router/connection.go +++ b/router/connection.go @@ -231,7 +231,7 @@ func (conn *LocalConnection) ReceivedHeartbeat(remoteUDPAddr *net.UDPAddr, connU if oldRemoteUDPAddr == nil { return conn.sendFastHeartbeats() } else if oldRemoteUDPAddr.String() != remoteUDPAddr.String() { - log.Println("Peer", conn.remote, "moved from", old, "to", remoteUDPAddr) + log.Println("Peer", conn.remote, "moved from", oldRemoteUDPAddr, "to", remoteUDPAddr) } return nil }) diff --git a/router/crypto.go b/router/crypto.go index 19b0bcfa..b66d2b73 100644 --- a/router/crypto.go +++ b/router/crypto.go @@ -1,15 +1,16 @@ package router import ( - "code.google.com/p/go-bit/bit" - "code.google.com/p/go.crypto/nacl/box" - "code.google.com/p/go.crypto/nacl/secretbox" "crypto/rand" "crypto/sha256" "encoding/binary" "encoding/gob" "fmt" "sync" + + "github.com/andybalholm/go-bit" + "golang.org/x/crypto/nacl/box" + "golang.org/x/crypto/nacl/secretbox" ) func GenerateKeyPair() (publicKey, privateKey *[32]byte, err error) { diff --git a/router/ethernet_decoder.go b/router/ethernet_decoder.go index 6588c0ad..f04c53e4 100644 --- a/router/ethernet_decoder.go +++ b/router/ethernet_decoder.go @@ -2,10 +2,11 @@ package router import ( "bytes" - "code.google.com/p/gopacket" - "code.google.com/p/gopacket/layers" "log" "net" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" ) type EthernetDecoder struct { diff --git a/router/forwarder.go b/router/forwarder.go index a4cec472..6c55c368 100644 --- a/router/forwarder.go +++ b/router/forwarder.go @@ -1,10 +1,11 @@ package router import ( - "code.google.com/p/gopacket" - "code.google.com/p/gopacket/layers" "syscall" "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" ) type ForwardedFrame struct { diff --git a/router/gossip_channel.go b/router/gossip_channel.go index da7dc0ec..39de093a 100644 --- a/router/gossip_channel.go +++ b/router/gossip_channel.go @@ -3,6 +3,7 @@ package router import ( "bytes" "encoding/gob" + "fmt" "log" "sync" ) @@ -58,7 +59,12 @@ func (c *GossipChannel) deliverUnicast(srcName PeerName, origPayload []byte, dec return err } if c.ourself.Name != destName { - return c.relayUnicast(destName, origPayload) + if err := c.relayUnicast(destName, origPayload); err != nil { + // just log errors from relayUnicast; a problem between us and destination + // is not enough reason to break the connection from the source + c.log(err) + } + return nil } var payload []byte if err := dec.Decode(&payload); err != nil { @@ -160,15 +166,15 @@ func (c *GossipChannel) GossipBroadcast(update GossipData) error { return c.relayBroadcast(c.ourself.Name, update) } -func (c *GossipChannel) relayUnicast(dstPeerName PeerName, buf []byte) error { +func (c *GossipChannel) relayUnicast(dstPeerName PeerName, buf []byte) (err error) { if relayPeerName, found := c.routes.UnicastAll(dstPeerName); !found { - c.log("unknown relay destination:", dstPeerName) + err = fmt.Errorf("unknown relay destination: %s", dstPeerName) } else if conn, found := c.ourself.ConnectionTo(relayPeerName); !found { - c.log("unable to find connection to relay peer", relayPeerName) + err = fmt.Errorf("unable to find connection to relay peer %s", relayPeerName) } else { conn.(ProtocolSender).SendProtocolMsg(ProtocolMsg{ProtocolGossipUnicast, buf}) } - return nil + return err } func (c *GossipChannel) relayBroadcast(srcName PeerName, update GossipData) error { diff --git a/router/handshake_test.go b/router/handshake_test.go index a72bf17c..1630d343 100644 --- a/router/handshake_test.go +++ b/router/handshake_test.go @@ -1,8 +1,9 @@ package router import ( - wt "github.com/weaveworks/weave/testing" "testing" + + wt "github.com/weaveworks/weave/testing" ) func TestFieldValidator(t *testing.T) { diff --git a/router/mocks_test.go b/router/mocks_test.go index 674eaa3a..dc0ec6c3 100644 --- a/router/mocks_test.go +++ b/router/mocks_test.go @@ -6,8 +6,9 @@ package router import ( - wt "github.com/weaveworks/weave/testing" "testing" + + wt "github.com/weaveworks/weave/testing" ) // Add to peers a connection from peers.ourself to p diff --git a/router/pcap.go b/router/pcap.go index 761d4e1d..36962f20 100644 --- a/router/pcap.go +++ b/router/pcap.go @@ -1,8 +1,9 @@ package router import ( - "code.google.com/p/gopacket/pcap" "fmt" + + "github.com/google/gopacket/pcap" ) type PcapIO struct { diff --git a/router/peers_test.go b/router/peers_test.go index daf1e944..bf221642 100644 --- a/router/peers_test.go +++ b/router/peers_test.go @@ -2,9 +2,10 @@ package router import ( "fmt" - wt "github.com/weaveworks/weave/testing" "math/rand" "testing" + + wt "github.com/weaveworks/weave/testing" ) // TODO we should also test: diff --git a/router/router.go b/router/router.go index ab671a0d..b498690b 100644 --- a/router/router.go +++ b/router/router.go @@ -11,21 +11,22 @@ import ( "syscall" "time" - "code.google.com/p/gopacket/layers" + "github.com/google/gopacket/layers" . "github.com/weaveworks/weave/common" ) const ( - macMaxAge = 10 * time.Minute // [1] - tcpAcceptDelay = 1 * time.Millisecond // [2] + macMaxAge = 10 * time.Minute // [1] + acceptMaxTokens = 100 // [2] + acceptTokenDelay = 100 * time.Millisecond // [3] ) // [1] should be greater than typical ARP cache expiries, i.e. > 3/2 * // /proc/sys/net/ipv4_neigh/*/base_reachable_time_ms on Linux -// [2] time to wait between accepting tcp connections. This guards -// against brute-force attacks on the password when encryption is in -// use. It is also a basic DoS defence. +// [2] capacity of token bucket for rate limiting accepts + +// [3] control rate at which new tokens are added to the bucket type LogFrameFunc func(string, []byte, *layers.Ethernet) @@ -50,6 +51,7 @@ type Router struct { gossipChannels GossipChannels TopologyGossip Gossip UDPListener *net.UDPConn + acceptLimiter *TokenBucket } type PacketSource interface { @@ -81,6 +83,7 @@ func NewRouter(config Config, name PeerName, nickName string) *Router { router.Routes = NewRoutes(router.Ourself, router.Peers) router.ConnectionMaker = NewConnectionMaker(router.Ourself, router.Peers, router.Port, router.PeerDiscovery) router.TopologyGossip = router.NewGossip("topology", router) + router.acceptLimiter = NewTokenBucket(acceptMaxTokens, acceptTokenDelay) return router } @@ -212,7 +215,7 @@ func (router *Router) listenTCP(localPort int) { continue } router.acceptTCP(tcpConn) - time.Sleep(tcpAcceptDelay) + router.acceptLimiter.Wait() } }() } diff --git a/router/token_bucket.go b/router/token_bucket.go new file mode 100644 index 00000000..254de6eb --- /dev/null +++ b/router/token_bucket.go @@ -0,0 +1,42 @@ +package router + +import ( + "time" +) + +type TokenBucket struct { + capacity int64 // Maximum capacity of bucket + tokenInterval time.Duration // Token replenishment rate + refillDuration time.Duration // Time to refill from empty + earliestUnspentToken time.Time +} + +func NewTokenBucket(capacity int64, tokenInterval time.Duration) *TokenBucket { + tb := TokenBucket{ + capacity: capacity, + tokenInterval: tokenInterval, + refillDuration: tokenInterval * time.Duration(capacity)} + + tb.earliestUnspentToken = tb.capacityToken() + + return &tb +} + +func (tb *TokenBucket) Wait() { + // If earliest unspent token is in the future, sleep until then + time.Sleep(tb.earliestUnspentToken.Sub(time.Now())) + + // Alternatively, enforce bucket capacity if necessary + capacityToken := tb.capacityToken() + if tb.earliestUnspentToken.Before(capacityToken) { + tb.earliestUnspentToken = capacityToken + } + + // 'Remove' a token from the bucket + tb.earliestUnspentToken = tb.earliestUnspentToken.Add(tb.tokenInterval) +} + +// Determine the historic token timestamp representing a full bucket +func (tb *TokenBucket) capacityToken() time.Time { + return time.Now().Add(-tb.refillDuration).Truncate(tb.tokenInterval) +} diff --git a/router/udp_sender.go b/router/udp_sender.go index 9e91ee0b..28ef917a 100644 --- a/router/udp_sender.go +++ b/router/udp_sender.go @@ -1,11 +1,12 @@ package router import ( - "code.google.com/p/gopacket" - "code.google.com/p/gopacket/layers" "log" "net" "syscall" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" ) type UDPSender interface { diff --git a/site/how-it-works.md b/site/how-it-works.md index 4e570c7c..70b97ff1 100644 --- a/site/how-it-works.md +++ b/site/how-it-works.md @@ -278,7 +278,7 @@ password, and the result is hashed through SHA256, to form the final ephemeral session key. Thus the supplied password is never exchanged directly, and is thoroughly mixed into the shared secret. Furthermore, the rate at which TCP connections are accepted is limited by weave to -1kHz, which twarts online dictionary attacks on reasonably strong +10Hz, which twarts online dictionary attacks on reasonably strong passwords. The shared key formed by Diffie-Hellman is 256 bits long, appending @@ -293,6 +293,39 @@ not be able to form valid ephemeral session keys. The same ephemeral session key is used for both TCP and UDP traffic between two peers. + Generating fresh keys for every connection +provides forward secrecy at the cost of placing a demand on the Linux +CSPRNG (accessed by `GenerateKey` via `/dev/urandom`) proportional to +the number of inbound connection attempts. Weave has accept throttling +to mitigate against denial of service attacks that seek to deplete the +CSPRNG entropy pool, however even at the lower bound of ten requests +per second there may not be enough entropy gathered on a headless +system to keep pace. + +Under such conditions, the consequences will be limited to slowing +down processes reading from the blocking `/dev/random` device as the +kernel waits for enough new entropy to be harvested. It is important +to note that contrary to intuition this low entropy state does not +compromise the ongoing use of `/dev/urandom` - [expert +opinion](http://blog.cr.yp.to/20140205-entropy.html) +asserts that as long as the CSPRNG is seeded with enough entropy (e.g. +256 bits) before random number generation commences then the output is +entirely safe for use as key material. + +By way of comparison, this is exactly how OpenSSL works - it reads 256 +bits of entropy at startup, and uses that to seed an internal CSPRNG +which is used thenceforth to generate keys. Whilst we could have taken +the same approach and built our own CSPRNG to work around the +potential `/dev/random` blocking issue, we thought it was much more +prudent to rely on the [heavily +scrutinised](http://eprint.iacr.org/2012/251.pdf) Linux random number +generator as [advised +here](http://cr.yp.to/highspeed/coolnacl-20120725.pdf) (page 10, +'Centralizing randomness'). The aforementioned notwithstanding, if +weave's demand on `/dev/urandom` is causing you problems with blocking +`/dev/random` reads, please get in touch with us - we'd love to hear +about your use case. + #### TCP TCP connection are only used to exchange topology information between diff --git a/test/270_use_name_as_hostname_test.sh b/test/270_use_name_as_hostname_test.sh index ccb0ff86..9d59bbe9 100755 --- a/test/270_use_name_as_hostname_test.sh +++ b/test/270_use_name_as_hostname_test.sh @@ -33,4 +33,8 @@ assert_expected_fqdn "$HOSTNAME" -h $HOSTNAME --name=$NAME assert_expected_fqdn "$HOSTNAME" --hostname=$HOSTNAME --name=$NAME assert_expected_fqdn "$HOSTNAME" --hostname $HOSTNAME --name=$NAME +# Ensure we can launch a container with a name that is too long for hostname derivation +SIXTY_FIVE_CHARS=01234567890123456789012345678901234567890123456789012345678901234 +assert_raises "start_container $HOST1 --name=$SIXTY_FIVE_CHARS" + end_suite diff --git a/test/600_proxy_docker_py_test.sh b/test/600_proxy_docker_py_test.sh index b61de7a8..84d27608 100755 --- a/test/600_proxy_docker_py_test.sh +++ b/test/600_proxy_docker_py_test.sh @@ -4,13 +4,19 @@ start_suite "Run docker-py test suite against the proxy" +docker_on $HOST1 pull joffrey/docker-py >/dev/null + weave_on $HOST1 launch-proxy --no-default-ipam -assert_raises "docker_on $HOST1 ps | grep weaveproxy" -assert_raises "docker_on $HOST1 run \ - -e NOT_ON_HOST=true \ - -e DOCKER_HOST=tcp://172.17.42.1:12375 \ - -v /tmp:/tmp \ - -v /var/run/docker.sock:/var/run/docker.sock \ - joffrey/docker-py python tests/integration_test.py" + +if docker_on $HOST1 run \ + -e NOT_ON_HOST=true \ + -e DOCKER_HOST=tcp://172.17.42.1:12375 \ + -v /tmp:/tmp \ + -v /var/run/docker.sock:/var/run/docker.sock \ + joffrey/docker-py python tests/integration_test.py ; then + assert_raises "true" +else + assert_raises "false" +fi end_suite diff --git a/test/610_proxy_wait_for_weave_test.sh b/test/610_proxy_wait_for_weave_test.sh index d13d3699..de3af8b4 100755 --- a/test/610_proxy_wait_for_weave_test.sh +++ b/test/610_proxy_wait_for_weave_test.sh @@ -2,6 +2,10 @@ . ./config.sh +entrypoint() { + docker_on $HOST1 inspect --format="{{.Config.Entrypoint}}" "$@" +} + start_suite "Proxy waits for weave to be ready before running container commands" weave_on $HOST1 launch-proxy BASE_IMAGE=busybox @@ -10,6 +14,28 @@ if (docker_on $HOST1 images $BASE_IMAGE | grep -q $BASE_IMAGE); then docker_on $HOST1 rmi $BASE_IMAGE fi -assert_raises "proxy docker_on $HOST1 run -e 'WEAVE_CIDR=10.2.1.1/24' $BASE_IMAGE $CHECK_ETHWE_UP" +assert_raises "proxy docker_on $HOST1 run --name c1 -e 'WEAVE_CIDR=10.2.1.1/24' $BASE_IMAGE $CHECK_ETHWE_UP" + +# Check committed containers only have one weavewait prepended +COMMITTED_IMAGE=$(proxy docker_on $HOST1 commit c1) +assert_raises "proxy docker_on $HOST1 run --name c2 $COMMITTED_IMAGE" +assert "entrypoint c2" "$(entrypoint $COMMITTED_IMAGE)" + +# Check exec works on containers without weavewait +docker_on $HOST1 run -dit --name c3 $SMALL_IMAGE /bin/sh +assert_raises "proxy docker_on $HOST1 exec c3 true" + +# Check we can't modify weavewait +assert_raises "proxy docker_on $HOST1 run -e 'WEAVE_CIDR=10.2.1.2/24' $BASE_IMAGE touch /w/w" 1 + +# Check only user-specified volumes and /w are mounted +dirs_sans_proxy=$(docker_on $HOST1 run --rm $SMALL_IMAGE mount | wc -l) +dirs_with_proxy=$(proxy docker_on $HOST1 run --rm -v /tmp/1:/srv1 -v /tmp/2:/srv2 -e 'WEAVE_CIDR=10.2.1.3/24' $SMALL_IMAGE mount | wc -l) +assert "echo $((dirs_with_proxy-3))" $dirs_sans_proxy + +# Check errors are returned (when docker returns an error code) +assert_raises "proxy docker_on $HOST1 run -e 'WEAVE_CIDR=10.2.1.3/24' $SMALL_IMAGE foo 2>&1 | grep 'exec: \"foo\": executable file not found in \$PATH'" +# Check errors still happen when no command is specified +assert_raises "proxy docker_on $HOST1 run -e 'WEAVE_CIDR=10.2.1.3/24' $SMALL_IMAGE 2>&1 | grep 'Error response from daemon: No command specified'" end_suite diff --git a/test/630_proxy_dns_test.sh b/test/630_proxy_dns_test.sh index d3b9ce04..67a844b7 100755 --- a/test/630_proxy_dns_test.sh +++ b/test/630_proxy_dns_test.sh @@ -8,8 +8,8 @@ C1_NAME=c1.weave.local C2_NAME=seetwo.weave.local boot_containers() { - proxy docker_on $HOST1 run -e WEAVE_CIDR=$C2/24 -dt --name=c2 -h $C2_NAME $DNS_IMAGE /bin/sh - proxy docker_on $HOST1 run -e WEAVE_CIDR=$C1/24 -dt --name=c1 $DNS_IMAGE /bin/sh + proxy_start_container_with_dns $HOST1 -e WEAVE_CIDR=$C2/24 -dt --name=c2 -h $C2_NAME + proxy_start_container_with_dns $HOST1 -e WEAVE_CIDR=$C1/24 -dt --name=c1 } kill_containers() { diff --git a/test/635_proxy_dns_unqualified_test.sh b/test/635_proxy_dns_unqualified_test.sh index b4ffbd18..b3ba410c 100755 --- a/test/635_proxy_dns_unqualified_test.sh +++ b/test/635_proxy_dns_unqualified_test.sh @@ -9,19 +9,15 @@ C4=10.2.0.99 DOMAIN=weave.local NAME=seeone.$DOMAIN -start_container() { - proxy docker_on $HOST1 run "$@" -dt $DNS_IMAGE /bin/sh -} - start_suite "Resolve unqualified names" weave_on $HOST1 launch-dns 10.2.254.1/24 weave_on $HOST1 launch-proxy -start_container -e WEAVE_CIDR=$C1/24 --name=c1 -h $NAME -start_container -e WEAVE_CIDR=$C2/24 --name=c2 -h seetwo.$DOMAIN -start_container -e WEAVE_CIDR=$C3/24 --name=c3 --dns-search=$DOMAIN -container=$(start_container -e WEAVE_CIDR=$C4/24) +proxy_start_container_with_dns $HOST1 -e WEAVE_CIDR=$C1/24 --name=c1 -h $NAME +proxy_start_container_with_dns $HOST1 -e WEAVE_CIDR=$C2/24 --name=c2 -h seetwo.$DOMAIN +proxy_start_container_with_dns $HOST1 -e WEAVE_CIDR=$C3/24 --name=c3 --dns-search=$DOMAIN +container=$(proxy_start_container_with_dns $HOST1 -e WEAVE_CIDR=$C4/24) check() { assert "proxy exec_on $HOST1 $1 getent hosts seeone | tr -s ' '" "$C1 $NAME" diff --git a/test/640_proxy_restart_reattaches_test.sh b/test/640_proxy_restart_reattaches_test.sh index b85e87c4..7f0166ed 100755 --- a/test/640_proxy_restart_reattaches_test.sh +++ b/test/640_proxy_restart_reattaches_test.sh @@ -10,8 +10,8 @@ start_suite "Proxy restart reattaches networking to containers" launch_dns_on $HOST1 10.2.254.1/24 weave_on $HOST1 launch-proxy --with-dns -proxy docker_on $HOST1 run -e WEAVE_CIDR=$C2/24 -dt --name=c2 -h $NAME $SMALL_IMAGE /bin/sh -proxy docker_on $HOST1 run -e WEAVE_CIDR=$C1/24 -dt --name=c1 $DNS_IMAGE /bin/sh +proxy_start_container $HOST1 -e WEAVE_CIDR=$C2/24 -dt --name=c2 -h $NAME +proxy_start_container_with_dns $HOST1 -e WEAVE_CIDR=$C1/24 -dt --name=c1 proxy docker_on $HOST1 restart c2 assert_raises "proxy exec_on $HOST1 c2 $CHECK_ETHWE_UP" diff --git a/test/660_proxy_ipam_test.sh b/test/660_proxy_ipam_test.sh index 11137995..16ea7490 100755 --- a/test/660_proxy_ipam_test.sh +++ b/test/660_proxy_ipam_test.sh @@ -4,12 +4,6 @@ UNIVERSE=10.2.2.0/24 -start() { - host=$1 - shift - proxy docker_on "$host" run "$@" -dt $SMALL_IMAGE /bin/sh -} - assert_no_ethwe() { assert_raises "container_ip $1 $2" 1 assert_raises "proxy exec_on $1 $2 ip link show | grep -v ethwe" @@ -22,17 +16,25 @@ weave_on $HOST2 launch -iprange $UNIVERSE $HOST1 weave_on $HOST1 launch-proxy weave_on $HOST2 launch-proxy --no-default-ipam -start $HOST1 --name=auto -start $HOST1 --name=none -e WEAVE_CIDR=none -start $HOST2 --name=zero -e WEAVE_CIDR= -start $HOST2 --name=no-default +proxy_start_container $HOST1 --name=auto +proxy_start_container $HOST1 --name=none -e WEAVE_CIDR=none +proxy_start_container $HOST2 --name=zero -e WEAVE_CIDR= +proxy_start_container $HOST2 --name=no-default +proxy_start_container $HOST1 --name=bridge --net=bridge +proxy_start_container $HOST1 --name=host --net=host +proxy_start_container $HOST1 --name=other --net=container:auto AUTO=$(container_ip $HOST1 auto) ZERO=$(container_ip $HOST2 zero) +BRIDGE=$(container_ip $HOST1 bridge) +OTHER=$(container_ip $HOST1 other) assert_raises "proxy exec_on $HOST1 auto $PING $ZERO" assert_raises "proxy exec_on $HOST2 zero $PING $AUTO" +assert_raises "proxy exec_on $HOST2 zero $PING $BRIDGE" +assert_raises "proxy exec_on $HOST2 zero $PING $OTHER" assert_no_ethwe $HOST1 none assert_no_ethwe $HOST2 no-default +assert_no_ethwe $HOST1 host end_suite diff --git a/test/680_proxy_weave_run_test.sh b/test/680_proxy_weave_run_test.sh new file mode 100755 index 00000000..1ed61da7 --- /dev/null +++ b/test/680_proxy_weave_run_test.sh @@ -0,0 +1,16 @@ +#! /bin/bash + +. ./config.sh + +C1=10.2.1.13 + +start_suite "Start containers with 'weave run' via proxy" + +weave_on $HOST1 launch +weave_on $HOST1 launch-proxy + +proxy start_container $HOST1 $C1/24 --name=c1 +# check we get exactly one IP back, the one specified +assert "weave_on $HOST1 ps c1 | cut -d ' ' -f 3-" $C1/24 + +end_suite diff --git a/test/config.sh b/test/config.sh index 3c3c0db4..053b3c49 100644 --- a/test/config.sh +++ b/test/config.sh @@ -120,6 +120,18 @@ start_container_with_dns() { weave_on $host run --with-dns "$@" -t $DNS_IMAGE /bin/sh } +proxy_start_container() { + host=$1 + shift 1 + proxy docker_on $host run "$@" -dt $SMALL_IMAGE /bin/sh +} + +proxy_start_container_with_dns() { + host=$1 + shift 1 + proxy docker_on $host run "$@" -dt $DNS_IMAGE /bin/sh +} + rm_containers() { host=$1 shift @@ -170,8 +182,8 @@ assert_dns_ptr_record() { start_suite() { for host in $HOST1 $HOST2; do [ -z "$DEBUG" ] || echo "Cleaning up on $host: removing all containers and resetting weave" - rm_containers $host $(docker_on $host ps -aq 2>/dev/null) weave_on $host reset 2>/dev/null + rm_containers $host $(docker_on $host ps -aq 2>/dev/null) done whitely echo "$@" } diff --git a/weave b/weave index b36721b0..f6e0f667 100755 --- a/weave +++ b/weave @@ -93,6 +93,22 @@ check_docker_version() { fi } +enforce_docker_bridge_addr_assign_type() { + if ! ADDR_ASSIGN_TYPE=$(cat /sys/class/net/$DOCKER_BRIDGE/addr_assign_type 2>/dev/null) ; then + echo "Could not determine address assignment type of $DOCKER_BRIDGE" >&2 + return + fi + # From include/uapi/linux/netdevice.h + # #define NET_ADDR_PERM 0 /* address is permanent (default) */ + # #define NET_ADDR_RANDOM 1 /* address is generated randomly */ + # #define NET_ADDR_STOLEN 2 /* address is stolen from other device */ + # #define NET_ADDR_SET 3 /* address is set using dev_set_mac_address() */ + if [ $ADDR_ASSIGN_TYPE != 3 ] ; then + echo "Setting $DOCKER_BRIDGE MAC (mitigate https://github.com/docker/docker/issues/14908)" >&2 + ip link set dev $DOCKER_BRIDGE address $(random_mac) || true + fi +} + is_cidr() { echo "$1" | grep -E "^$CIDR_REGEXP$" >/dev/null } @@ -142,7 +158,7 @@ if [ "$1" = "run" ] ; then shift $(dns_arg_count "$@") collect_cidr_args "$@" shift $CIDR_ARG_COUNT - CONTAINER=$(docker $DOCKER_CLIENT_ARGS run $DNS_ARGS -d "$@") + CONTAINER=$(docker $DOCKER_CLIENT_ARGS run -e WEAVE_CIDR=none $DNS_ARGS -d "$@") exec_remote attach $CIDR_ARGS --or-die $CONTAINER echo $CONTAINER exit 0 @@ -359,7 +375,7 @@ netnsenter() { } connect_container_to_bridge() { - if [ -h "$PROCFS/$CONTAINER_PID/ns/net" -a -h "$PROCFS/$$/ns/net" -a "$(readlink $PROCFS/$CONTAINER_PID/ns/net)" = "$(readlink $PROCFS/$$/ns/net)" ] ; then + if [ -h "$PROCFS/$CONTAINER_PID/ns/net" -a -h "/proc/$$/ns/net" -a "$(readlink $PROCFS/$CONTAINER_PID/ns/net)" = "$(readlink /proc/$$/ns/net)" ] ; then echo "Container is running in the host network namespace, and therefore cannot be" >&2 echo "connected to weave. Perhaps the container was started with --net=host." >&2 return 1 @@ -433,7 +449,9 @@ attach() { netnsenter ip route add 224.0.0.0/4 dev $CONTAINER_IFNAME fi - arp_update $CONTAINER_IFNAME $1 "netnsenter" + for ADDR in "$@" ; do + arp_update $CONTAINER_IFNAME $ADDR "netnsenter" + done } detach() { @@ -664,8 +682,11 @@ dns_args() { WITHOUT_DNS= while [ $# -gt 0 ] ; do case "$1" in - --dns-search=*) - DNS_SEARCH_SPECIFIED=1 + --with-dns) + WITH_DNS=1 + ;; + --without-dns) + WITHOUT_DNS=1 ;; --name) NAME_ARG="$2" @@ -677,11 +698,8 @@ dns_args() { -h|--hostname|--hostname=*) HOSTNAME_SPECIFIED=1 ;; - --with-dns) - WITH_DNS=1 - ;; - --without-dns) - WITHOUT_DNS=1 + --dns-search|--dns-search=*) + DNS_SEARCH_SPECIFIED=1 ;; esac shift @@ -693,8 +711,13 @@ dns_args() { DNS_ARGS="--dns $DOCKER_BRIDGE_IP" if [ -n "$NAME_ARG" -a -z "$HOSTNAME_SPECIFIED" ] ; then DOMAIN=$(dns_domain) - DNS_ARGS="$DNS_ARGS --hostname=$NAME_ARG.${DOMAIN%.}" - HOSTNAME_SPECIFIED=1 + HOSTNAME="$NAME_ARG.${DOMAIN%.}" + if [ ${#HOSTNAME} -gt 64 ] ; then + echo "Container name too long to be used as hostname" >&2 + else + DNS_ARGS="$DNS_ARGS --hostname=$HOSTNAME" + HOSTNAME_SPECIFIED=1 + fi fi if [ -z "$DNS_SEARCH_SPECIFIED" ] ; then if [ -z "$HOSTNAME_SPECIFIED" ] ; then @@ -859,7 +882,7 @@ ipam_cidrs_or_die() { # TODO: Handle args with spaces tls_arg() { filename=${3:-$2} - PROXY_VOLUMES="$PROXY_VOLUMES -v $1:/home/weave/tls/$filename.pem" + PROXY_VOLUMES="$PROXY_VOLUMES -v $1:/home/weave/tls/$filename.pem:ro" PROXY_ARGS="$PROXY_ARGS --tls$2 /home/weave/tls/$filename.pem" } @@ -971,6 +994,7 @@ case "$COMMAND" in ;; launch) check_not_running $CONTAINER_NAME $BASE_IMAGE + enforce_docker_bridge_addr_assign_type create_bridge # We set the router name to the bridge mac since that is # stable across re-creations of the containers. @@ -1017,10 +1041,15 @@ case "$COMMAND" in if [ -z "$IPRANGE_SPECIFIED" ] ; then IPRANGE="10.128.0.0/10" if command_exists netcheck && ! netcheck $IPRANGE ; then - echo "Default -iprange $IPRANGE overlaps with existing route." >&2 - echo "Please pick another range and set it on all hosts." >&2 + echo "ERROR: Default -iprange $IPRANGE overlaps with existing route on host." >&2 + echo "You must pick another range and set it on all hosts." >&2 exit 1 fi + else + if command_exists netcheck && ! netcheck $IPRANGE ; then + echo "WARNING: Specified -iprange $IPRANGE overlaps with existing route on host." >&2 + echo "Unless this is deliberate, you must pick another range and set it on all hosts." >&2 + fi fi # Set WEAVE_DOCKER_ARGS in the environment in order to supply # additional parameters, such as resource limits, to docker @@ -1071,6 +1100,7 @@ case "$COMMAND" in -v /proc:/hostproc \ -e PROCFS=/hostproc \ -e WEAVE_CIDR=none \ + -e DOCKER_BRIDGE \ --entrypoint=/home/weave/weaveproxy \ $WEAVEPROXY_DOCKER_ARGS $EXEC_IMAGE $PROXY_ARGS) wait_for_log $PROXY_CONTAINER_NAME "proxy listening" @@ -1130,7 +1160,7 @@ case "$COMMAND" in shift $(dns_arg_count "$@") collect_cidr_args "$@" shift $CIDR_ARG_COUNT - CONTAINER=$(docker run $DNS_ARGS -d "$@") + CONTAINER=$(docker run -e WEAVE_CIDR=none $DNS_ARGS -d "$@") create_bridge ipam_cidrs_or_die $CONTAINER $CIDR_ARGS with_container_netns_or_die $CONTAINER attach $ALL_CIDRS