{"action":"created","comment":{"url":"https://api.github.com/repos/weaveworks/weave/pulls/comments/37286917","id":37286917,"diff_hunk":"@@ -0,0 +1,1117 @@\n+// This contains the Overlay implementation for weave's own UDP\n+// encapsulation protocol (\"sleeve\" because a sleeve encapsulates\n+// something, it's often woven, it rhymes with \"weave\", make up your\n+// own cheesy reason).\n+\n+package router\n+\n+import (\n+\t\"bytes\"\n+\t\"encoding/binary\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"net\"\n+\t\"sync\"\n+\t\"syscall\"\n+\t\"time\"\n+\n+\t\"github.com/google/gopacket\"\n+\t\"github.com/google/gopacket/layers\"\n+)\n+\n+const (\n+\tEthernetOverhead    = 14\n+\tUDPOverhead         = 28 // 20 bytes for IPv4, 8 bytes for UDP\n+\tDefaultPMTU         = 65535\n+\tFragTestSize        = 60001\n+\tPMTUDiscoverySize   = 60000\n+\tFastHeartbeat       = 500 * time.Millisecond\n+\tSlowHeartbeat       = 10 * time.Second\n+\tFragTestInterval    = 5 * time.Minute\n+\tPMTUVerifyAttempts  = 8\n+\tPMTUVerifyTimeout   = 10 * time.Millisecond // doubled with each attempt\n+\tMaxMissedHeartbeats = 6\n+\tHeartbeatTimeout    = MaxMissedHeartbeats * SlowHeartbeat\n+)\n+\n+type SleeveOverlay struct {\n+\tlocalPort int\n+\n+\t// These fields are set in ConsumePackets, and not\n+\t// subsequently modified\n+\tlocalPeer    *Peer\n+\tlocalPeerBin []byte\n+\tconsumer     OverlayConsumer\n+\tpeers        *Peers\n+\tconn         *net.UDPConn\n+\n+\tlock       sync.Mutex\n+\tforwarders map[PeerName]*sleeveForwarder\n+}\n+\n+func NewSleeveOverlay(localPort int) Overlay {\n+\treturn &SleeveOverlay{localPort: localPort}\n+}\n+\n+func (sleeve *SleeveOverlay) ConsumePackets(localPeer *Peer, peers *Peers,\n+\tconsumer OverlayConsumer) error {\n+\tlocalAddr, err := net.ResolveUDPAddr(\"udp4\",\n+\t\tfmt.Sprint(\":\", sleeve.localPort))\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tconn, err := net.ListenUDP(\"udp4\", localAddr)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tf, err := conn.File()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tdefer f.Close()\n+\tfd := int(f.Fd())\n+\n+\t// This makes sure all packets we send out do not have DF set\n+\t// on them.\n+\terr = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP,\n+\t\tsyscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_DONT)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tsleeve.lock.Lock()\n+\tdefer sleeve.lock.Unlock()\n+\n+\tif sleeve.localPeer != nil {\n+\t\tconn.Close()\n+\t\treturn fmt.Errorf(\"ConsumePackets already called\")\n+\t}\n+\n+\tsleeve.localPeer = localPeer\n+\tsleeve.localPeerBin = localPeer.NameByte\n+\tsleeve.consumer = consumer\n+\tsleeve.peers = peers\n+\tsleeve.conn = conn\n+\tsleeve.forwarders = make(map[PeerName]*sleeveForwarder)\n+\tgo sleeve.readUDP()\n+\treturn nil\n+}\n+\n+func (*SleeveOverlay) InvalidateRoutes() {\n+\t// no cached information, so nothing to do\n+}\n+\n+func (sleeve *SleeveOverlay) lookupForwarder(peer PeerName) *sleeveForwarder {\n+\tsleeve.lock.Lock()\n+\tdefer sleeve.lock.Unlock()\n+\treturn sleeve.forwarders[peer]\n+}\n+\n+func (sleeve *SleeveOverlay) addForwarder(peer PeerName,\n+\tfwd *sleeveForwarder) error {\n+\tsleeve.lock.Lock()\n+\tdefer sleeve.lock.Unlock()\n+\tif sleeve.forwarders[peer] != nil {\n+\t\treturn fmt.Errorf(\"already have a forwarder for %s\", peer)\n+\t}\n+\n+\tsleeve.forwarders[peer] = fwd\n+\treturn nil\n+}\n+\n+func (sleeve *SleeveOverlay) readUDP() {\n+\tdefer sleeve.conn.Close()\n+\tdec := NewEthernetDecoder()\n+\tbuf := make([]byte, MaxUDPPacketSize)\n+\n+\tfor {\n+\t\tn, sender, err := sleeve.conn.ReadFromUDP(buf)\n+\t\tif err == io.EOF {\n+\t\t\treturn\n+\t\t} else if err != nil {\n+\t\t\tlog.Println(\"ignoring UDP read error\", err)\n+\t\t\tcontinue\n+\t\t} else if n < NameSize {\n+\t\t\tlog.Println(\"ignoring too short UDP packet from\", sender)\n+\t\t\tcontinue\n+\t\t}\n+\n+\t\tfwdName := PeerNameFromBin(buf[:NameSize])\n+\t\tfwd := sleeve.lookupForwarder(fwdName)\n+\t\tif fwd == nil {\n+\t\t\tcontinue\n+\t\t}\n+\n+\t\tpacket := make([]byte, n-NameSize)\n+\t\tcopy(packet, buf[NameSize:n])\n+\n+\t\terr = fwd.crypto.Dec.IterateFrames(packet,\n+\t\t\tfunc(src []byte, dst []byte, frame []byte) {\n+\t\t\t\tsleeve.handleFrame(sender, fwd,\n+\t\t\t\t\tsrc, dst, frame, dec)\n+\t\t\t})\n+\t\tif err != nil {\n+\t\t\t// Errors during UDP packet decoding /\n+\t\t\t// processing are non-fatal. One common cause\n+\t\t\t// is that we receive and attempt to decrypt a\n+\t\t\t// \"stray\" packet. This can actually happen\n+\t\t\t// quite easily if there is some connection\n+\t\t\t// churn between two peers. After all, UDP\n+\t\t\t// isn't a connection-oriented protocol, yet\n+\t\t\t// we pretend it is.\n+\t\t\t//\n+\t\t\t// If anything really is seriously,\n+\t\t\t// unrecoverably amiss with a connection, that\n+\t\t\t// will typically result in missed heartbeats\n+\t\t\t// and the connection getting shut down\n+\t\t\t// because of that.\n+\t\t\tlog.Println(fwd.logPrefixFor(sender), err)\n+\t\t}\n+\t}\n+}\n+\n+func (sleeve *SleeveOverlay) handleFrame(sender *net.UDPAddr,\n+\tfwd *sleeveForwarder, src []byte, dst []byte, frame []byte,\n+\tdec *EthernetDecoder) {\n+\tdec.DecodeLayers(frame)\n+\tdecodedLen := len(dec.decoded)\n+\tif decodedLen == 0 {\n+\t\treturn\n+\t}\n+\n+\tsrcPeer := sleeve.peers.Fetch(PeerNameFromBin(src))\n+\tdstPeer := sleeve.peers.Fetch(PeerNameFromBin(dst))\n+\tif srcPeer == nil || dstPeer == nil {\n+\t\treturn\n+\t}\n+\n+\t// Handle special frames produced internally (rather than\n+\t// captured/forwarded) by the remote router.\n+\t//\n+\t// We really shouldn't be decoding these above, since they are\n+\t// not genuine Ethernet frames. However, it is actually more\n+\t// efficient to do so, as we want to optimise for the common\n+\t// (i.e. non-special) frames. These always need decoding, and\n+\t// detecting special frames is cheaper post decoding than pre.\n+\tif decodedLen == 1 && dec.IsSpecial() {\n+\t\tif srcPeer == fwd.remotePeer && dstPeer == fwd.sleeve.localPeer {\n+\t\t\tselect {\n+\t\t\tcase fwd.specialChan <- specialFrame{sender, frame}:\n+\t\t\tcase <-fwd.finishedChan:\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn\n+\t}\n+\n+\tsleeve.sendToConsumer(srcPeer, dstPeer, frame, dec)\n+}\n+\n+func (sleeve *SleeveOverlay) sendToConsumer(srcPeer, dstPeer *Peer,\n+\tframe []byte, dec *EthernetDecoder) {\n+\tif sleeve.consumer == nil {\n+\t\treturn\n+\t}\n+\n+\tfop := sleeve.consumer(ForwardPacketKey{\n+\t\tSrcPeer:   srcPeer,\n+\t\tDstPeer:   dstPeer,\n+\t\tPacketKey: dec.PacketKey(),\n+\t})\n+\tif fop != nil {\n+\t\tfop.Send(frame, dec, false)\n+\t}\n+}\n+\n+type udpSender interface {\n+\tsend([]byte, *net.UDPAddr) error\n+}\n+\n+func (sleeve *SleeveOverlay) send(msg []byte, raddr *net.UDPAddr) error {\n+\tsleeve.lock.Lock()\n+\tconn := sleeve.conn\n+\tsleeve.lock.Unlock()\n+\n+\tif conn == nil {\n+\t\t// Consume wasn't called yet\n+\t\treturn nil\n+\t}\n+\n+\t_, err := conn.WriteToUDP(msg, raddr)\n+\treturn err\n+}\n+\n+type sleeveForwarder struct {\n+\t// Immutable\n+\tsleeve         *SleeveOverlay\n+\tremotePeer     *Peer\n+\tremotePeerBin  []byte\n+\tsendControlMsg func([]byte) error\n+\tconnUID        uint64\n+\n+\t// Channels to communicate with the aggregator goroutine\n+\taggregatorChan   chan<- aggregatorFrame\n+\taggregatorDFChan chan<- aggregatorFrame\n+\tspecialChan      chan<- specialFrame\n+\tconfirmedChan    chan<- struct{}\n+\tfinishedChan     <-chan struct{}\n+\n+\t// Explicitly locked state\n+\tlock       sync.RWMutex\n+\tlistener   OverlayForwarderListener\n+\tremoteAddr *net.UDPAddr\n+\n+\t// These fields are accessed and updated independently, so no\n+\t// locking needed\n+\teffectivePMTU int\n+\tstackFrag     bool\n+\n+\t// dec is only used from the readUDP goroutine, enc and encDF\n+\t// are only used in the forwarder goroutine\n+\tcrypto OverlayCrypto\n+\n+\t// State only used within the forwarder goroutine\n+\tsenderDF   *udpSenderDF\n+\tmaxPayload int\n+\n+\t// How many bytes of overhead it takes to turn an IP packet on\n+\t// the overlay network into an encapsulated packet on the underlay\n+\t// network\n+\toverheadDF int\n+\n+\theartbeatInterval time.Duration\n+\theartbeatTimer    *time.Timer\n+\theartbeatTimeout  *time.Timer\n+\tfragTestTicker    *time.Ticker\n+\tackedHeartbeat    bool\n+\n+\tpmtuTestTimeout  *time.Timer\n+\tpmtuTestsSent    uint\n+\tepmtuHighestGood int\n+\tepmtuLowestBad   int\n+\tepmtuCandidate   int\n+}\n+\n+type aggregatorFrame struct {\n+\tsrc   []byte\n+\tdst   []byte\n+\tframe []byte\n+}\n+\n+// A \"special\" message over UDP, or a control message.  The sender is\n+// nil for control messages.\n+type specialFrame struct {\n+\tsender *net.UDPAddr\n+\tframe  []byte\n+}\n+\n+func (sleeve *SleeveOverlay) MakeForwarder(params ForwarderParams) (OverlayForwarder, error) {\n+\tvar crypto OverlayCrypto\n+\tif params.Crypto != nil {\n+\t\tcrypto = *params.Crypto\n+\t} else {\n+\t\tname := sleeve.localPeer.NameByte\n+\t\tcrypto = OverlayCrypto{\n+\t\t\tDec:   NewNonDecryptor(),\n+\t\t\tEnc:   NewNonEncryptor(name),\n+\t\t\tEncDF: NewNonEncryptor(name),\n+\t\t}\n+\t}\n+\n+\taggChan := make(chan aggregatorFrame, ChannelSize)\n+\taggDFChan := make(chan aggregatorFrame, ChannelSize)\n+\tspecialChan := make(chan specialFrame, 1)\n+\tconfirmedChan := make(chan struct{})\n+\tfinishedChan := make(chan struct{})\n+\n+\tfwd := &sleeveForwarder{\n+\t\tsleeve:           sleeve,\n+\t\tremotePeer:       params.RemotePeer,\n+\t\tremotePeerBin:    params.RemotePeer.NameByte,\n+\t\tsendControlMsg:   params.SendControlMessage,\n+\t\tconnUID:          params.ConnUID,\n+\t\taggregatorChan:   aggChan,\n+\t\taggregatorDFChan: aggDFChan,\n+\t\tspecialChan:      specialChan,\n+\t\tconfirmedChan:    confirmedChan,\n+\t\tfinishedChan:     finishedChan,\n+\t\tremoteAddr:       params.RemoteAddr,\n+\t\teffectivePMTU:    DefaultPMTU,\n+\t\tcrypto:           crypto,\n+\t\tmaxPayload:       DefaultPMTU - UDPOverhead,\n+\t\toverheadDF: UDPOverhead + crypto.EncDF.PacketOverhead() +\n+\t\t\tcrypto.EncDF.FrameOverhead() + EthernetOverhead,\n+\t\tsenderDF: newUDPSenderDF(params.LocalIP, sleeve.localPort),\n+\t}\n+\n+\tif err := sleeve.addForwarder(params.RemotePeer.Name, fwd); err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tgo fwd.run(aggChan, aggDFChan, specialChan, confirmedChan,\n+\t\tfinishedChan)\n+\treturn fwd, nil\n+}\n+\n+func (fwd *sleeveForwarder) logPrefixFor(sender *net.UDPAddr) string {\n+\treturn fmt.Sprintf(\"->[%s|%s]: \", sender, fwd.remotePeer)\n+}\n+\n+func (fwd *sleeveForwarder) logPrefix() string {\n+\tfwd.lock.RLock()\n+\tremoteAddr := fwd.remoteAddr\n+\tfwd.lock.RUnlock()\n+\treturn fwd.logPrefixFor(remoteAddr)\n+}\n+\n+func (fwd *sleeveForwarder) SetListener(listener OverlayForwarderListener) {\n+\tlog.Debug(fwd.logPrefix(), \"SetListener\", listener)\n+\n+\tfwd.lock.Lock()\n+\tfwd.listener = listener\n+\tfwd.lock.Unlock()\n+\n+\t// Setting the listener confirms that the forwarder is really\n+\t// wanted\n+\tif listener != nil {\n+\t\tselect {\n+\t\tcase fwd.confirmedChan <- struct{}{}:\n+\t\tcase <-fwd.finishedChan:\n+\t\t}\n+\t}\n+}\n+\n+type curriedForward struct {\n+\tfwd *sleeveForwarder\n+\tkey ForwardPacketKey\n+}\n+\n+func (fwd *sleeveForwarder) Forward(key ForwardPacketKey) FlowOp {\n+\treturn curriedForward{fwd, key}\n+}\n+\n+func (f curriedForward) Send(frame []byte, dec *EthernetDecoder,\n+\tbroadcast bool) {\n+\tfwd := f.fwd\n+\tfwd.lock.RLock()\n+\thaveContact := (fwd.remoteAddr != nil)\n+\teffectivePMTU := fwd.effectivePMTU\n+\tstackFrag := fwd.stackFrag\n+\tfwd.lock.RUnlock()\n+\n+\tif !haveContact {\n+\t\tlog.Println(fwd.logPrefix(),\n+\t\t\t\"Cannot forward frame yet - awaiting contact\")\n+\t\treturn\n+\t}\n+\n+\tsrcName := f.key.SrcPeer.NameByte\n+\tdstName := f.key.DstPeer.NameByte\n+\n+\t// We could use non-blocking channel sends here, i.e. drop frames\n+\t// on the floor when the forwarder is busy. This would allow our\n+\t// caller - the capturing loop in the router - to read frames more\n+\t// quickly when under load, i.e. we'd drop fewer frames on the\n+\t// floor during capture. And we could maximise CPU utilisation\n+\t// since we aren't stalling a thread. However, a lot of work has\n+\t// already been done by the time we get here. Since any packet we\n+\t// drop will likely get re-transmitted we end up paying that cost\n+\t// multiple times. So it's better to drop things at the beginning\n+\t// of our pipeline.\n+\tif dec.DF() {\n+\t\tif !frameTooBig(frame, effectivePMTU) {\n+\t\t\tfwd.aggregate(fwd.aggregatorDFChan, srcName, dstName,\n+\t\t\t\tframe)\n+\t\t\treturn\n+\t\t}\n+\n+\t\t// Why do we need an explicit broadcast hint here,\n+\t\t// rather than just checking the frame for a broadcast\n+\t\t// destination MAC address?  Because even\n+\t\t// non-broadcast frames can be broadcast, if the\n+\t\t// destination MAC was not in our MAC cache.\n+\t\tif broadcast {\n+\t\t\tlog.Printf(\"dropping too big DF broadcast frame (%v -> %v): PMTU= %v\\n\", dec.IP.DstIP, dec.IP.SrcIP, effectivePMTU)\n+\t\t\treturn\n+\t\t}\n+\n+\t\t// Send an ICMP back to where the frame came from\n+\t\tfragNeededPacket, err := dec.makeICMPFragNeeded(effectivePMTU)\n+\t\tif err != nil {\n+\t\t\tlog.Println(err)\n+\t\t\treturn\n+\t\t}\n+\n+\t\tdec.DecodeLayers(fragNeededPacket)\n+\n+\t\t// The frag-needed packet does not have DF set, so the\n+\t\t// potential recursion here is bounded.\n+\t\tfwd.sleeve.sendToConsumer(f.key.DstPeer, f.key.SrcPeer,\n+\t\t\tfragNeededPacket, dec)\n+\t\treturn\n+\t}\n+\n+\tif stackFrag || len(dec.decoded) < 2 {\n+\t\tfwd.aggregate(fwd.aggregatorChan, srcName, dstName, frame)\n+\t\treturn\n+\t}\n+\n+\t// Don't have trustworthy stack, so we're going to have to\n+\t// send it DF in any case.\n+\tif !frameTooBig(frame, effectivePMTU) {\n+\t\tfwd.aggregate(fwd.aggregatorDFChan, srcName, dstName, frame)\n+\t\treturn\n+\t}\n+\n+\t// We can't trust the stack to fragment, we have IP, and we\n+\t// have a frame that's too big for the MTU, so we have to\n+\t// fragment it ourself.\n+\tcheckWarn(fragment(dec.Eth, dec.IP, effectivePMTU,\n+\t\tfunc(segFrame []byte) {\n+\t\t\tfwd.aggregate(fwd.aggregatorDFChan, srcName, dstName,\n+\t\t\t\tsegFrame)\n+\t\t}))\n+}\n+\n+func (fwd *sleeveForwarder) aggregate(ch chan<- aggregatorFrame, src []byte,\n+\tdst []byte, frame []byte) {\n+\tselect {\n+\tcase ch <- aggregatorFrame{src, dst, frame}:\n+\tcase <-fwd.finishedChan:\n+\t}\n+}\n+\n+func fragment(eth layers.Ethernet, ip layers.IPv4, pmtu int,\n+\tforward func([]byte)) error {\n+\t// We are not doing any sort of NAT, so we don't need to worry\n+\t// about checksums of IP payload (eg UDP checksum).\n+\theaderSize := int(ip.IHL) * 4\n+\t// &^ is bit clear (AND NOT). So here we're clearing the lowest 3\n+\t// bits.\n+\tmaxSegmentSize := (pmtu - headerSize) &^ 7\n+\topts := gopacket.SerializeOptions{\n+\t\tFixLengths:       false,\n+\t\tComputeChecksums: true}\n+\tpayloadSize := int(ip.Length) - headerSize\n+\tpayload := ip.BaseLayer.Payload[:payloadSize]\n+\toffsetBase := int(ip.FragOffset) << 3\n+\torigFlags := ip.Flags\n+\tip.Flags = ip.Flags | layers.IPv4MoreFragments\n+\tip.Length = uint16(headerSize + maxSegmentSize)\n+\tif eth.EthernetType == layers.EthernetTypeLLC {\n+\t\t// using LLC, so must set eth length correctly. eth length\n+\t\t// is just the length of the payload\n+\t\teth.Length = ip.Length\n+\t} else {\n+\t\teth.Length = 0\n+\t}\n+\tfor offset := 0; offset < payloadSize; offset += maxSegmentSize {\n+\t\tvar segmentPayload []byte\n+\t\tif len(payload) <= maxSegmentSize {\n+\t\t\t// last one\n+\t\t\tsegmentPayload = payload\n+\t\t\tip.Length = uint16(len(payload) + headerSize)\n+\t\t\tip.Flags = origFlags\n+\t\t\tif eth.EthernetType == layers.EthernetTypeLLC {\n+\t\t\t\teth.Length = ip.Length\n+\t\t\t} else {\n+\t\t\t\teth.Length = 0\n+\t\t\t}\n+\t\t} else {\n+\t\t\tsegmentPayload = payload[:maxSegmentSize]\n+\t\t\tpayload = payload[maxSegmentSize:]\n+\t\t}\n+\t\tip.FragOffset = uint16((offset + offsetBase) >> 3)\n+\t\tbuf := gopacket.NewSerializeBuffer()\n+\t\tsegPayload := gopacket.Payload(segmentPayload)\n+\t\terr := gopacket.SerializeLayers(buf, opts, &eth, &ip,\n+\t\t\t&segPayload)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\tforward(buf.Bytes())\n+\t}\n+\treturn nil\n+}\n+\n+func frameTooBig(frame []byte, effectivePMTU int) bool {\n+\t// We capture/forward complete ethernet frames. Therefore the\n+\t// frame length includes the ethernet header. However, MTUs\n+\t// operate at the IP layer and thus do not include the ethernet\n+\t// header. To put it another way, when a sender that was told an\n+\t// MTU of M sends an IP packet of exactly that length, we will\n+\t// capture/forward M + EthernetOverhead bytes of data.\n+\treturn len(frame) > effectivePMTU+EthernetOverhead\n+}\n+\n+func (fwd *sleeveForwarder) ControlMessage(msg []byte) {\n+\tselect {\n+\tcase fwd.specialChan <- specialFrame{nil, msg}:\n+\tcase <-fwd.finishedChan:\n+\t}\n+}\n+\n+func (fwd *sleeveForwarder) Close() {\n+\tsleeve := fwd.sleeve\n+\tsleeve.lock.Lock()\n+\tif sleeve.forwarders[fwd.remotePeer.Name] == fwd {\n+\t\tdelete(sleeve.forwarders, fwd.remotePeer.Name)\n+\t}\n+\tsleeve.lock.Unlock()\n+\tfwd.SetListener(nil)\n+\n+\t// Tell the forwarder goroutine to finish.  We don't need to\n+\t// wait for it.\n+\tclose(fwd.confirmedChan)\n+}\n+\n+func (fwd *sleeveForwarder) run(aggChan <-chan aggregatorFrame,\n+\taggDFChan <-chan aggregatorFrame,\n+\tspecialChan <-chan specialFrame,\n+\tconfirmedChan <-chan struct{},\n+\tfinishedChan chan<- struct{}) {\n+\tdefer close(finishedChan)\n+\n+\tvar err error\n+loop:\n+\tfor err == nil {\n+\t\tselect {\n+\t\tcase frame := <-aggChan:\n+\t\t\terr = fwd.aggregateAndSend(frame, aggChan,\n+\t\t\t\tfwd.crypto.Enc, fwd.sleeve,\n+\t\t\t\tMaxUDPPacketSize-UDPOverhead)\n+\n+\t\tcase frame := <-aggDFChan:\n+\t\t\terr = fwd.aggregateAndSend(frame, aggDFChan,\n+\t\t\t\tfwd.crypto.EncDF, fwd.senderDF, fwd.maxPayload)\n+\n+\t\tcase special := <-specialChan:\n+\t\t\terr = fwd.handleSpecialFrame(special)\n+\n+\t\tcase _, ok := <-confirmedChan:\n+\t\t\tif !ok {\n+\t\t\t\t// specialChan is closed to indicate\n+\t\t\t\t// the forwarder is being closed\n+\t\t\t\tbreak loop\n+\t\t\t}\n+\n+\t\t\terr = fwd.confirmed()\n+\n+\t\tcase <-timerChan(fwd.heartbeatTimer):\n+\t\t\terr = fwd.sendHeartbeat()\n+\n+\t\tcase <-timerChan(fwd.heartbeatTimeout):\n+\t\t\terr = fmt.Errorf(\"timed out waiting for UDP heartbeat\")\n+\n+\t\tcase <-tickerChan(fwd.fragTestTicker):\n+\t\t\terr = fwd.sendFragTest()\n+\n+\t\tcase <-timerChan(fwd.pmtuTestTimeout):\n+\t\t\terr = fwd.handlePMTUTestFailure()\n+\t\t}\n+\t}\n+\n+\tif fwd.heartbeatTimer != nil {\n+\t\tfwd.heartbeatTimer.Stop()\n+\t}\n+\tif fwd.heartbeatTimeout != nil {\n+\t\tfwd.heartbeatTimeout.Stop()\n+\t}\n+\tif fwd.fragTestTicker != nil {\n+\t\tfwd.fragTestTicker.Stop()\n+\t}\n+\tif fwd.pmtuTestTimeout != nil {\n+\t\tfwd.pmtuTestTimeout.Stop()\n+\t}\n+\n+\tcheckWarn(fwd.senderDF.close())\n+\n+\tfwd.lock.RLock()\n+\tdefer fwd.lock.RUnlock()\n+\tif fwd.listener != nil {\n+\t\tfwd.listener.Error(err)\n+\t}\n+}\n+\n+func (fwd *sleeveForwarder) aggregateAndSend(frame aggregatorFrame,\n+\taggChan <-chan aggregatorFrame, enc Encryptor, sender udpSender,\n+\tlimit int) error {\n+\t// Give up after processing N frames, to avoid starving the\n+\t// other activities of the forwarder goroutine.\n+\ti := 0\n+\n+\tfor {\n+\t\t// Adding the first frame to an empty buffer\n+\t\tif !fits(frame, enc, limit) {\n+\t\t\tlog.Println(fwd.logPrefix(), \"Dropping too big frame during forwarding: frame len\", len(frame.frame), \", limit \", limit)\n+\t\t\treturn nil\n+\t\t}\n+\n+\t\tfor {\n+\t\t\tenc.AppendFrame(frame.src, frame.dst, frame.frame)\n+\t\t\ti++\n+\n+\t\t\tgotOne := false\n+\t\t\tif i < 100 {\n+\t\t\t\tselect {\n+\t\t\t\tcase frame = <-aggChan:\n+\t\t\t\t\tgotOne = true\n+\t\t\t\tdefault:\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif !gotOne {\n+\t\t\t\treturn fwd.flushEncryptor(enc, sender)\n+\t\t\t}\n+\n+\t\t\t// Accumulate frames until doing so would\n+\t\t\t// exceed the PMTU.  Even in the non-DF case,\n+\t\t\t// it doesn't seem worth adding a frame where\n+\t\t\t// that would lead to fragmentation,\n+\t\t\t// potentially delaying or risking other\n+\t\t\t// frames.\n+\t\t\tif !fits(frame, enc, fwd.maxPayload) {\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n+\n+\t\tif err := fwd.flushEncryptor(enc, sender); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t}\n+}\n+\n+func fits(frame aggregatorFrame, enc Encryptor, limit int) bool {\n+\treturn enc.TotalLen()+enc.FrameOverhead()+len(frame.frame) <= limit\n+}\n+\n+func (fwd *sleeveForwarder) flushEncryptor(enc Encryptor,\n+\tsender udpSender) error {\n+\tmsg, err := enc.Bytes()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\treturn fwd.processSendError(sender.send(msg, fwd.remoteAddr))\n+}\n+\n+func (fwd *sleeveForwarder) sendSpecial(enc Encryptor, sender udpSender,\n+\tdata []byte) error {\n+\tenc.AppendFrame(fwd.sleeve.localPeerBin, fwd.remotePeerBin, data)\n+\treturn fwd.flushEncryptor(enc, sender)\n+}\n+\n+func (fwd *sleeveForwarder) handleSpecialFrame(special specialFrame) error {\n+\tif special.sender == nil {\n+\t\treturn fwd.handleControlMsg(special.frame)\n+\t}\n+\n+\t// The special frame types are distinguished by length\n+\tswitch len(special.frame) {\n+\tcase EthernetOverhead + 8:\n+\t\treturn fwd.handleHeartbeat(special)\n+\n+\tcase FragTestSize:\n+\t\treturn fwd.handleFragTest(special.frame)\n+\n+\tdefault:\n+\t\treturn fwd.handlePMTUTest(special.frame)\n+\t}\n+}\n+\n+const (\n+\tHeartbeatAck = iota\n+\tFragTestAck\n+\tPMTUTestAck\n+)\n+\n+func (fwd *sleeveForwarder) handleControlMsg(msg []byte) error {\n+\tif len(msg) == 0 {\n+\t\tlog.Println(fwd.logPrefix(),\n+\t\t\t\"Received zero-length control message\")\n+\t\treturn nil\n+\t}\n+\n+\tswitch msg[0] {\n+\tcase HeartbeatAck:\n+\t\treturn fwd.handleHeartbeatAck()\n+\n+\tcase FragTestAck:\n+\t\treturn fwd.handleFragTestAck()\n+\n+\tcase PMTUTestAck:\n+\t\treturn fwd.handlePMTUTestAck(msg)\n+\n+\tdefault:\n+\t\tlog.Println(fwd.logPrefix(),\n+\t\t\t\"Ignoring unknown control message:\", msg[0])\n+\t\treturn nil\n+\t}\n+}\n+\n+func (fwd *sleeveForwarder) confirmed() error {\n+\tlog.Debug(fwd.logPrefix(), \"confirmed\")\n+\n+\tif fwd.heartbeatInterval != 0 {\n+\t\t// already confirmed\n+\t\treturn nil\n+\t}\n+\n+\t// heartbeatInterval flags that we want to send heartbeats,\n+\t// even if we don't do sendHeartbeat() yet due to lacking the\n+\t// remote address.\n+\tfwd.heartbeatInterval = time.Duration(FastHeartbeat)\n+\tif fwd.remoteAddr != nil {\n+\t\tif err := fwd.sendHeartbeat(); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t}\n+\n+\tfwd.heartbeatTimeout = time.NewTimer(HeartbeatTimeout)\n+\treturn nil\n+}\n+\n+func (fwd *sleeveForwarder) sendHeartbeat() error {\n+\tlog.Debug(fwd.logPrefix(), \"sendHeartbeat\")\n+\n+\t// Prime the timer for the next heartbeat.  We don't use a\n+\t// ticker because the interval is not constant.\n+\tfwd.heartbeatTimer = setTimer(fwd.heartbeatTimer, fwd.heartbeatInterval)\n+\n+\tbuf := make([]byte, EthernetOverhead+8)\n+\tbinary.BigEndian.PutUint64(buf[EthernetOverhead:], fwd.connUID)\n+\treturn fwd.sendSpecial(fwd.crypto.EncDF, fwd.senderDF, buf)\n+}\n+\n+func (fwd *sleeveForwarder) handleHeartbeat(special specialFrame) error {\n+\tuid := binary.BigEndian.Uint64(special.frame[EthernetOverhead:])\n+\tif uid != fwd.connUID {\n+\t\treturn nil\n+\t}\n+\n+\tlog.Debug(fwd.logPrefix(), \"handleHeartbeat\")\n+\n+\tif fwd.remoteAddr == nil {\n+\t\tfwd.setRemoteAddr(special.sender)\n+\t\tif fwd.heartbeatInterval != time.Duration(0) {\n+\t\t\tif err := fwd.sendHeartbeat(); err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t}\n+\t} else if !udpAddrsEqual(fwd.remoteAddr, special.sender) {\n+\t\tlog.Println(fwd.logPrefix(),\n+\t\t\t\"Peer UDP address changed to\", special.sender)\n+\t\tfwd.setRemoteAddr(special.sender)\n+\t}\n+\n+\tif !fwd.ackedHeartbeat {\n+\t\tfwd.ackedHeartbeat = true\n+\t\tif err := fwd.sendControlMsg([]byte{HeartbeatAck}); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t}\n+\n+\tfwd.heartbeatTimeout.Reset(HeartbeatTimeout)\n+\treturn nil\n+}\n+\n+func (fwd *sleeveForwarder) setRemoteAddr(addr *net.UDPAddr) {\n+\t// Although we don't need to lock when reading remoteAddr,\n+\t// because this thread is the only one that modifies\n+\t// remoteAddr, we do need to lock when writing it, because\n+\t// memory models.\n+\tfwd.lock.Lock()\n+\tfwd.remoteAddr = addr\n+\tfwd.lock.Unlock()\n+}\n+\n+func (fwd *sleeveForwarder) handleHeartbeatAck() error {\n+\t// The connection is nowregarded as established\n+\tfwd.notifyEstablished()\n+\n+\tif fwd.heartbeatInterval != SlowHeartbeat {\n+\t\tfwd.heartbeatInterval = SlowHeartbeat\n+\t\tif fwd.heartbeatTimer != nil {\n+\t\t\tfwd.heartbeatTimer.Reset(fwd.heartbeatInterval)\n+\t\t}\n+\t}\n+\n+\tfwd.fragTestTicker = time.NewTicker(FragTestInterval)\n+\tif err := fwd.sendFragTest(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// Send a large frame down the DF channel in order to prompt\n+\t// PMTU discovery to start.\n+\treturn fwd.sendSpecial(fwd.crypto.EncDF, fwd.senderDF,","path":"router/sleeve.go","position":850,"original_position":850,"commit_id":"f12ad3d8eea5039dbcd8ce6d03a76505c9df86ab","original_commit_id":"f12ad3d8eea5039dbcd8ce6d03a76505c9df86ab","user":{"login":"dpw","id":15074,"avatar_url":"https://avatars.githubusercontent.com/u/15074?v=3","gravatar_id":"","url":"https://api.github.com/users/dpw","html_url":"https://github.com/dpw","followers_url":"https://api.github.com/users/dpw/followers","following_url":"https://api.github.com/users/dpw/following{/other_user}","gists_url":"https://api.github.com/users/dpw/gists{/gist_id}","starred_url":"https://api.github.com/users/dpw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dpw/subscriptions","organizations_url":"https://api.github.com/users/dpw/orgs","repos_url":"https://api.github.com/users/dpw/repos","events_url":"https://api.github.com/users/dpw/events{/privacy}","received_events_url":"https://api.github.com/users/dpw/received_events","type":"User","site_admin":false},"body":"> > By this point, the kernel's TCP code will have established the PMTU to the remote peer's IP, and that works even in the absence of frag-needed responses\r\n> \r\n> Is that Packetization Layer Path MTU Discovery?\r\n\r\nYes.  (Although the kernel code doesn't refer to that RFC, and I wouldn't be surprised if the technique came first and the RFC codified it.)\r\n\r\nAs that RFC notes, TCP doesn't allow you to pad packets.  So TCP-based PMTU discovery can only proceed when the application supplies enough data for the TCP layer to make a segment of the candidate size.  I don't think our connection initiation protocol is guaranteed to do big sends, so it's less of a sure thing than I thought that TCP will force discovery of the PMTU.","created_at":"2015-08-18T11:09:12Z","updated_at":"2015-08-18T11:09:12Z","html_url":"https://github.com/weaveworks/weave/pull/991#discussion_r37286917","pull_request_url":"https://api.github.com/repos/weaveworks/weave/pulls/991","_links":{"self":{"href":"https://api.github.com/repos/weaveworks/weave/pulls/comments/37286917"},"html":{"href":"https://github.com/weaveworks/weave/pull/991#discussion_r37286917"},"pull_request":{"href":"https://api.github.com/repos/weaveworks/weave/pulls/991"}}},"pull_request":{"url":"https://api.github.com/repos/weaveworks/weave/pulls/991","id":38350988,"html_url":"https://github.com/weaveworks/weave/pull/991","diff_url":"https://github.com/weaveworks/weave/pull/991.diff","patch_url":"https://github.com/weaveworks/weave/pull/991.patch","issue_url":"https://api.github.com/repos/weaveworks/weave/issues/991","number":991,"state":"open","locked":false,"title":"Fast datapath preparations","user":{"login":"dpw","id":15074,"avatar_url":"https://avatars.githubusercontent.com/u/15074?v=3","gravatar_id":"","url":"https://api.github.com/users/dpw","html_url":"https://github.com/dpw","followers_url":"https://api.github.com/users/dpw/followers","following_url":"https://api.github.com/users/dpw/following{/other_user}","gists_url":"https://api.github.com/users/dpw/gists{/gist_id}","starred_url":"https://api.github.com/users/dpw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dpw/subscriptions","organizations_url":"https://api.github.com/users/dpw/orgs","repos_url":"https://api.github.com/users/dpw/repos","events_url":"https://api.github.com/users/dpw/events{/privacy}","received_events_url":"https://api.github.com/users/dpw/received_events","type":"User","site_admin":false},"body":"These are the changes to the existing router code to prepare the way for fast datapath.  I didn't include the short peer ID changes, as they are much more tailored to ODP specifics, and so they make more sense as part of the fast datapath PR.\r\n\r\nI think this is clean enough for a review pass.  There is still one point I'm not quite happy with yet:  The names \"IntraHost\" and \"InterHost\" were intended as stand-ins, until I came up with better names.  But I still haven't come up with replacements that seem obviously better (the best so far is PacketCapture and PacketTransport but they are longer, and PacketCapture isn't right because that interface includes sending, plus PacketCapture has too much affinity with pcap).  I'm hoping inspiration will strike by the time this is merge-ready, and welcome any suggestions.","created_at":"2015-06-23T15:41:47Z","updated_at":"2015-08-18T11:09:12Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cecf55d81e985c71e39e656e93de533f71c0ae85","assignee":{"login":"awh","id":1504438,"avatar_url":"https://avatars.githubusercontent.com/u/1504438?v=3","gravatar_id":"","url":"https://api.github.com/users/awh","html_url":"https://github.com/awh","followers_url":"https://api.github.com/users/awh/followers","following_url":"https://api.github.com/users/awh/following{/other_user}","gists_url":"https://api.github.com/users/awh/gists{/gist_id}","starred_url":"https://api.github.com/users/awh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/awh/subscriptions","organizations_url":"https://api.github.com/users/awh/orgs","repos_url":"https://api.github.com/users/awh/repos","events_url":"https://api.github.com/users/awh/events{/privacy}","received_events_url":"https://api.github.com/users/awh/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/weaveworks/weave/pulls/991/commits","review_comments_url":"https://api.github.com/repos/weaveworks/weave/pulls/991/comments","review_comment_url":"https://api.github.com/repos/weaveworks/weave/pulls/comments{/number}","comments_url":"https://api.github.com/repos/weaveworks/weave/issues/991/comments","statuses_url":"https://api.github.com/repos/weaveworks/weave/statuses/f12ad3d8eea5039dbcd8ce6d03a76505c9df86ab","head":{"label":"dpw:fdp-prep","ref":"fdp-prep","sha":"f12ad3d8eea5039dbcd8ce6d03a76505c9df86ab","user":{"login":"dpw","id":15074,"avatar_url":"https://avatars.githubusercontent.com/u/15074?v=3","gravatar_id":"","url":"https://api.github.com/users/dpw","html_url":"https://github.com/dpw","followers_url":"https://api.github.com/users/dpw/followers","following_url":"https://api.github.com/users/dpw/following{/other_user}","gists_url":"https://api.github.com/users/dpw/gists{/gist_id}","starred_url":"https://api.github.com/users/dpw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dpw/subscriptions","organizations_url":"https://api.github.com/users/dpw/orgs","repos_url":"https://api.github.com/users/dpw/repos","events_url":"https://api.github.com/users/dpw/events{/privacy}","received_events_url":"https://api.github.com/users/dpw/received_events","type":"User","site_admin":false},"repo":{"id":23701178,"name":"weave","full_name":"dpw/weave","owner":{"login":"dpw","id":15074,"avatar_url":"https://avatars.githubusercontent.com/u/15074?v=3","gravatar_id":"","url":"https://api.github.com/users/dpw","html_url":"https://github.com/dpw","followers_url":"https://api.github.com/users/dpw/followers","following_url":"https://api.github.com/users/dpw/following{/other_user}","gists_url":"https://api.github.com/users/dpw/gists{/gist_id}","starred_url":"https://api.github.com/users/dpw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dpw/subscriptions","organizations_url":"https://api.github.com/users/dpw/orgs","repos_url":"https://api.github.com/users/dpw/repos","events_url":"https://api.github.com/users/dpw/events{/privacy}","received_events_url":"https://api.github.com/users/dpw/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/dpw/weave","description":"The Docker Network","fork":true,"url":"https://api.github.com/repos/dpw/weave","forks_url":"https://api.github.com/repos/dpw/weave/forks","keys_url":"https://api.github.com/repos/dpw/weave/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dpw/weave/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dpw/weave/teams","hooks_url":"https://api.github.com/repos/dpw/weave/hooks","issue_events_url":"https://api.github.com/repos/dpw/weave/issues/events{/number}","events_url":"https://api.github.com/repos/dpw/weave/events","assignees_url":"https://api.github.com/repos/dpw/weave/assignees{/user}","branches_url":"https://api.github.com/repos/dpw/weave/branches{/branch}","tags_url":"https://api.github.com/repos/dpw/weave/tags","blobs_url":"https://api.github.com/repos/dpw/weave/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dpw/weave/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dpw/weave/git/refs{/sha}","trees_url":"https://api.github.com/repos/dpw/weave/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dpw/weave/statuses/{sha}","languages_url":"https://api.github.com/repos/dpw/weave/languages","stargazers_url":"https://api.github.com/repos/dpw/weave/stargazers","contributors_url":"https://api.github.com/repos/dpw/weave/contributors","subscribers_url":"https://api.github.com/repos/dpw/weave/subscribers","subscription_url":"https://api.github.com/repos/dpw/weave/subscription","commits_url":"https://api.github.com/repos/dpw/weave/commits{/sha}","git_commits_url":"https://api.github.com/repos/dpw/weave/git/commits{/sha}","comments_url":"https://api.github.com/repos/dpw/weave/comments{/number}","issue_comment_url":"https://api.github.com/repos/dpw/weave/issues/comments{/number}","contents_url":"https://api.github.com/repos/dpw/weave/contents/{+path}","compare_url":"https://api.github.com/repos/dpw/weave/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dpw/weave/merges","archive_url":"https://api.github.com/repos/dpw/weave/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dpw/weave/downloads","issues_url":"https://api.github.com/repos/dpw/weave/issues{/number}","pulls_url":"https://api.github.com/repos/dpw/weave/pulls{/number}","milestones_url":"https://api.github.com/repos/dpw/weave/milestones{/number}","notifications_url":"https://api.github.com/repos/dpw/weave/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dpw/weave/labels{/name}","releases_url":"https://api.github.com/repos/dpw/weave/releases{/id}","created_at":"2014-09-05T11:47:37Z","updated_at":"2015-04-16T13:03:57Z","pushed_at":"2015-08-03T16:20:04Z","git_url":"git://github.com/dpw/weave.git","ssh_url":"git@github.com:dpw/weave.git","clone_url":"https://github.com/dpw/weave.git","svn_url":"https://github.com/dpw/weave","homepage":"","size":4892,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"weaveworks:master","ref":"master","sha":"281bf02871e62275c50cd7b55e0edb58926b774d","user":{"login":"weaveworks","id":9976052,"avatar_url":"https://avatars.githubusercontent.com/u/9976052?v=3","gravatar_id":"","url":"https://api.github.com/users/weaveworks","html_url":"https://github.com/weaveworks","followers_url":"https://api.github.com/users/weaveworks/followers","following_url":"https://api.github.com/users/weaveworks/following{/other_user}","gists_url":"https://api.github.com/users/weaveworks/gists{/gist_id}","starred_url":"https://api.github.com/users/weaveworks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/weaveworks/subscriptions","organizations_url":"https://api.github.com/users/weaveworks/orgs","repos_url":"https://api.github.com/users/weaveworks/repos","events_url":"https://api.github.com/users/weaveworks/events{/privacy}","received_events_url":"https://api.github.com/users/weaveworks/received_events","type":"Organization","site_admin":false},"repo":{"id":23059575,"name":"weave","full_name":"weaveworks/weave","owner":{"login":"weaveworks","id":9976052,"avatar_url":"https://avatars.githubusercontent.com/u/9976052?v=3","gravatar_id":"","url":"https://api.github.com/users/weaveworks","html_url":"https://github.com/weaveworks","followers_url":"https://api.github.com/users/weaveworks/followers","following_url":"https://api.github.com/users/weaveworks/following{/other_user}","gists_url":"https://api.github.com/users/weaveworks/gists{/gist_id}","starred_url":"https://api.github.com/users/weaveworks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/weaveworks/subscriptions","organizations_url":"https://api.github.com/users/weaveworks/orgs","repos_url":"https://api.github.com/users/weaveworks/repos","events_url":"https://api.github.com/users/weaveworks/events{/privacy}","received_events_url":"https://api.github.com/users/weaveworks/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/weaveworks/weave","description":"Weaving Docker containers into applications","fork":false,"url":"https://api.github.com/repos/weaveworks/weave","forks_url":"https://api.github.com/repos/weaveworks/weave/forks","keys_url":"https://api.github.com/repos/weaveworks/weave/keys{/key_id}","collaborators_url":"https://api.github.com/repos/weaveworks/weave/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/weaveworks/weave/teams","hooks_url":"https://api.github.com/repos/weaveworks/weave/hooks","issue_events_url":"https://api.github.com/repos/weaveworks/weave/issues/events{/number}","events_url":"https://api.github.com/repos/weaveworks/weave/events","assignees_url":"https://api.github.com/repos/weaveworks/weave/assignees{/user}","branches_url":"https://api.github.com/repos/weaveworks/weave/branches{/branch}","tags_url":"https://api.github.com/repos/weaveworks/weave/tags","blobs_url":"https://api.github.com/repos/weaveworks/weave/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/weaveworks/weave/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/weaveworks/weave/git/refs{/sha}","trees_url":"https://api.github.com/repos/weaveworks/weave/git/trees{/sha}","statuses_url":"https://api.github.com/repos/weaveworks/weave/statuses/{sha}","languages_url":"https://api.github.com/repos/weaveworks/weave/languages","stargazers_url":"https://api.github.com/repos/weaveworks/weave/stargazers","contributors_url":"https://api.github.com/repos/weaveworks/weave/contributors","subscribers_url":"https://api.github.com/repos/weaveworks/weave/subscribers","subscription_url":"https://api.github.com/repos/weaveworks/weave/subscription","commits_url":"https://api.github.com/repos/weaveworks/weave/commits{/sha}","git_commits_url":"https://api.github.com/repos/weaveworks/weave/git/commits{/sha}","comments_url":"https://api.github.com/repos/weaveworks/weave/comments{/number}","issue_comment_url":"https://api.github.com/repos/weaveworks/weave/issues/comments{/number}","contents_url":"https://api.github.com/repos/weaveworks/weave/contents/{+path}","compare_url":"https://api.github.com/repos/weaveworks/weave/compare/{base}...{head}","merges_url":"https://api.github.com/repos/weaveworks/weave/merges","archive_url":"https://api.github.com/repos/weaveworks/weave/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/weaveworks/weave/downloads","issues_url":"https://api.github.com/repos/weaveworks/weave/issues{/number}","pulls_url":"https://api.github.com/repos/weaveworks/weave/pulls{/number}","milestones_url":"https://api.github.com/repos/weaveworks/weave/milestones{/number}","notifications_url":"https://api.github.com/repos/weaveworks/weave/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/weaveworks/weave/labels{/name}","releases_url":"https://api.github.com/repos/weaveworks/weave/releases{/id}","created_at":"2014-08-18T05:19:07Z","updated_at":"2015-08-15T21:39:46Z","pushed_at":"2015-08-18T11:05:29Z","git_url":"git://github.com/weaveworks/weave.git","ssh_url":"git@github.com:weaveworks/weave.git","clone_url":"https://github.com/weaveworks/weave.git","svn_url":"https://github.com/weaveworks/weave","homepage":"http://weave.works","size":14623,"stargazers_count":3616,"watchers_count":3616,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":220,"mirror_url":null,"open_issues_count":146,"forks":220,"open_issues":146,"watchers":3616,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/weaveworks/weave/pulls/991"},"html":{"href":"https://github.com/weaveworks/weave/pull/991"},"issue":{"href":"https://api.github.com/repos/weaveworks/weave/issues/991"},"comments":{"href":"https://api.github.com/repos/weaveworks/weave/issues/991/comments"},"review_comments":{"href":"https://api.github.com/repos/weaveworks/weave/pulls/991/comments"},"review_comment":{"href":"https://api.github.com/repos/weaveworks/weave/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/weaveworks/weave/pulls/991/commits"},"statuses":{"href":"https://api.github.com/repos/weaveworks/weave/statuses/f12ad3d8eea5039dbcd8ce6d03a76505c9df86ab"}}}}