Browse Source

New global discovery protocol over HTTPS (fixes #628, fixes #1907)

Jakob Borg 10 years ago
parent
commit
b0cd7be39b
41 changed files with 1932 additions and 1499 deletions
  1. 3 3
      Godeps/Godeps.json
  2. 19 1
      Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go
  3. 25 1
      Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go
  4. 2 1
      Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go
  5. 2 8
      Godeps/_workspace/src/github.com/thejerf/suture/README.md
  6. 79 15
      cmd/stfinddevice/main.go
  7. 14 2
      cmd/syncthing/addresslister.go
  8. 8 7
      cmd/syncthing/connections.go
  9. 31 19
      cmd/syncthing/gui.go
  10. 46 36
      cmd/syncthing/main.go
  11. 11 0
      cmd/syncthing/verbose.go
  12. 4 0
      gui/assets/css/overrides.css
  13. 13 13
      gui/index.html
  14. 12 11
      gui/syncthing/core/syncthingController.js
  15. 1 1
      gui/syncthing/device/editDeviceModalView.html
  16. 1 1
      lib/auto/gui.files.go
  17. 24 23
      lib/beacon/beacon.go
  18. 43 27
      lib/beacon/broadcast.go
  19. 200 53
      lib/beacon/multicast.go
  20. 27 8
      lib/config/config.go
  21. 1 1
      lib/config/config_test.go
  22. 12 0
      lib/config/wrapper.go
  23. 192 0
      lib/discover/cache.go
  24. 0 54
      lib/discover/client.go
  25. 0 239
      lib/discover/client_test.go
  26. 0 261
      lib/discover/client_udp.go
  27. 29 520
      lib/discover/discover.go
  28. 0 163
      lib/discover/discover_test.go
  29. 69 2
      lib/discover/doc.go
  30. 385 0
      lib/discover/global.go
  31. 253 0
      lib/discover/global_test.go
  32. 270 0
      lib/discover/local.go
  33. 3 3
      lib/discover/localpackets.go
  34. 7 7
      lib/discover/localpackets_xdr.go
  35. 3 0
      lib/events/events.go
  36. 2 0
      lib/ignore/ignore.go
  37. 137 11
      lib/relay/relay.go
  38. 1 2
      test/h1/config.xml
  39. 1 2
      test/h2/config.xml
  40. 1 2
      test/h3/config.xml
  41. 1 2
      test/h4/config.xml

+ 3 - 3
Godeps/Godeps.json

@@ -43,11 +43,11 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/syncthing/relaysrv/client",
 			"ImportPath": "github.com/syncthing/relaysrv/client",
-			"Rev": "7fe1fdd8c751df165ea825bc8d3e895f118bb236"
+			"Rev": "6e126fb97e2ff566d35f8d8824e86793d22b2147"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/syncthing/relaysrv/protocol",
 			"ImportPath": "github.com/syncthing/relaysrv/protocol",
-			"Rev": "7fe1fdd8c751df165ea825bc8d3e895f118bb236"
+			"Rev": "6e126fb97e2ff566d35f8d8824e86793d22b2147"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/syndtr/goleveldb/leveldb",
 			"ImportPath": "github.com/syndtr/goleveldb/leveldb",
@@ -55,7 +55,7 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/thejerf/suture",
 			"ImportPath": "github.com/thejerf/suture",
-			"Rev": "fc7aaeabdc43fe41c5328efa1479ffea0b820978"
+			"Rev": "860b44045335c64a6d54ac7eed22a3aedfc687c9"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/vitrun/qart/coding",
 			"ImportPath": "github.com/vitrun/qart/coding",

+ 19 - 1
Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go

@@ -32,6 +32,7 @@ type ProtocolClient struct {
 
 
 	mut       sync.RWMutex
 	mut       sync.RWMutex
 	connected bool
 	connected bool
+	latency   time.Duration
 }
 }
 
 
 func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient {
 func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient {
@@ -168,6 +169,13 @@ func (c *ProtocolClient) StatusOK() bool {
 	return con
 	return con
 }
 }
 
 
+func (c *ProtocolClient) Latency() time.Duration {
+	c.mut.RLock()
+	lat := c.latency
+	c.mut.RUnlock()
+	return lat
+}
+
 func (c *ProtocolClient) String() string {
 func (c *ProtocolClient) String() string {
 	return fmt.Sprintf("ProtocolClient@%p", c)
 	return fmt.Sprintf("ProtocolClient@%p", c)
 }
 }
@@ -177,11 +185,21 @@ func (c *ProtocolClient) connect() error {
 		return fmt.Errorf("Unsupported relay schema:", c.URI.Scheme)
 		return fmt.Errorf("Unsupported relay schema:", c.URI.Scheme)
 	}
 	}
 
 
-	conn, err := tls.Dial("tcp", c.URI.Host, c.config)
+	t0 := time.Now()
+	tcpConn, err := net.Dial("tcp", c.URI.Host)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
+	c.mut.Lock()
+	c.latency = time.Since(t0)
+	c.mut.Unlock()
+
+	conn := tls.Client(tcpConn, c.config)
+	if err = conn.Handshake(); err != nil {
+		return err
+	}
+
 	if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
 	if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
 		conn.Close()
 		conn.Close()
 		return err
 		return err

+ 25 - 1
Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go

@@ -8,6 +8,7 @@ import (
 	"net"
 	"net"
 	"net/url"
 	"net/url"
 	"strconv"
 	"strconv"
+	"strings"
 	"time"
 	"time"
 
 
 	syncthingprotocol "github.com/syncthing/protocol"
 	syncthingprotocol "github.com/syncthing/protocol"
@@ -20,10 +21,10 @@ func GetInvitationFromRelay(uri *url.URL, id syncthingprotocol.DeviceID, certs [
 	}
 	}
 
 
 	conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs))
 	conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs))
-	conn.SetDeadline(time.Now().Add(10 * time.Second))
 	if err != nil {
 	if err != nil {
 		return protocol.SessionInvitation{}, err
 		return protocol.SessionInvitation{}, err
 	}
 	}
+	conn.SetDeadline(time.Now().Add(10 * time.Second))
 
 
 	if err := performHandshakeAndValidation(conn, uri); err != nil {
 	if err := performHandshakeAndValidation(conn, uri); err != nil {
 		return protocol.SessionInvitation{}, err
 		return protocol.SessionInvitation{}, err
@@ -97,6 +98,29 @@ func JoinSession(invitation protocol.SessionInvitation) (net.Conn, error) {
 	}
 	}
 }
 }
 
 
+func TestRelay(uri *url.URL, certs []tls.Certificate, sleep time.Duration, times int) bool {
+	id := syncthingprotocol.NewDeviceID(certs[0].Certificate[0])
+	invs := make(chan protocol.SessionInvitation, 1)
+	c := NewProtocolClient(uri, certs, invs)
+	go c.Serve()
+	defer func() {
+		close(invs)
+		c.Stop()
+	}()
+
+	for i := 0; i < times; i++ {
+		_, err := GetInvitationFromRelay(uri, id, certs)
+		if err == nil {
+			return true
+		}
+		if !strings.Contains(err.Error(), "Incorrect response code") {
+			return false
+		}
+		time.Sleep(sleep)
+	}
+	return false
+}
+
 func configForCerts(certs []tls.Certificate) *tls.Config {
 func configForCerts(certs []tls.Certificate) *tls.Config {
 	return &tls.Config{
 	return &tls.Config{
 		Certificates:           certs,
 		Certificates:           certs,

+ 2 - 1
Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go

@@ -7,8 +7,9 @@ package protocol
 
 
 import (
 import (
 	"fmt"
 	"fmt"
-	syncthingprotocol "github.com/syncthing/protocol"
 	"net"
 	"net"
+
+	syncthingprotocol "github.com/syncthing/protocol"
 )
 )
 
 
 const (
 const (

+ 2 - 8
Godeps/_workspace/src/github.com/thejerf/suture/README.md

@@ -6,10 +6,8 @@ Suture
 Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" ->
 Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" ->
 "sutree" -> "suture" -> holds your code together when it's trying to die.
 "sutree" -> "suture" -> holds your code together when it's trying to die.
 
 
-This is intended to be a production-quality library going into code that I
-will be very early on the phone tree to support when it goes down. However,
-it has not been deployed into something quite that serious yet. (I will
-update this statement when that changes.)
+This library has hit maturity, and isn't expected to be changed
+radically. This can also be imported via gopkg.in/thejerf/suture.v1 .
 
 
 It is intended to deal gracefully with the real failure cases that can
 It is intended to deal gracefully with the real failure cases that can
 occur with supervision trees (such as burning all your CPU time endlessly
 occur with supervision trees (such as burning all your CPU time endlessly
@@ -24,10 +22,6 @@ This module is fully covered with [godoc](http://godoc.org/github.com/thejerf/su
 including an example, usage, and everything else you might expect from a
 including an example, usage, and everything else you might expect from a
 README.md on GitHub. (DRY.)
 README.md on GitHub. (DRY.)
 
 
-This is not currently tagged with particular git tags for Go as this is
-currently considered to be alpha code. As I move this into production and
-feel more confident about it, I'll give it relevant tags.
-
 Code Signing
 Code Signing
 ------------
 ------------
 
 

+ 79 - 15
cmd/stfinddevice/main.go

@@ -7,41 +7,105 @@
 package main
 package main
 
 
 import (
 import (
+	"crypto/tls"
+	"errors"
 	"flag"
 	"flag"
-	"log"
+	"fmt"
+	"net/url"
 	"os"
 	"os"
+	"time"
 
 
 	"github.com/syncthing/protocol"
 	"github.com/syncthing/protocol"
+	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/discover"
 	"github.com/syncthing/syncthing/lib/discover"
 )
 )
 
 
-func main() {
-	log.SetFlags(0)
-	log.SetOutput(os.Stdout)
+var timeout = 5 * time.Second
 
 
+func main() {
 	var server string
 	var server string
 
 
-	flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22027", "Announce server")
+	flag.StringVar(&server, "server", "", "Announce server (blank for default set)")
+	flag.DurationVar(&timeout, "timeout", timeout, "Query timeout")
+	flag.Usage = usage
 	flag.Parse()
 	flag.Parse()
 
 
-	if len(flag.Args()) != 1 || server == "" {
-		log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22027\"] <device>", os.Args[0])
+	if flag.NArg() != 1 {
+		flag.Usage()
 		os.Exit(64)
 		os.Exit(64)
 	}
 	}
 
 
 	id, err := protocol.DeviceIDFromString(flag.Args()[0])
 	id, err := protocol.DeviceIDFromString(flag.Args()[0])
 	if err != nil {
 	if err != nil {
-		log.Println(err)
+		fmt.Println(err)
 		os.Exit(1)
 		os.Exit(1)
 	}
 	}
 
 
-	discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil, nil)
-	discoverer.StartGlobal([]string{server}, nil)
-	addresses, relays := discoverer.Lookup(id)
-	for _, addr := range addresses {
-		log.Println("address:", addr)
+	if server != "" {
+		checkServers(id, server)
+	} else {
+		checkServers(id, config.DefaultDiscoveryServers...)
+	}
+}
+
+type checkResult struct {
+	server string
+	direct []string
+	relays []discover.Relay
+	error
+}
+
+func checkServers(deviceID protocol.DeviceID, servers ...string) {
+	t0 := time.Now()
+	resc := make(chan checkResult)
+	for _, srv := range servers {
+		srv := srv
+		go func() {
+			res := checkServer(deviceID, srv)
+			res.server = srv
+			resc <- res
+		}()
+	}
+
+	for _ = range servers {
+		res := <-resc
+
+		u, _ := url.Parse(res.server)
+		fmt.Printf("%s (%v):\n", u.Host, time.Since(t0))
+
+		if res.error != nil {
+			fmt.Println("  " + res.error.Error())
+		}
+		for _, addr := range res.direct {
+			fmt.Println("  address:", addr)
+		}
+		for _, rel := range res.relays {
+			fmt.Printf("  relay: %s (%d ms)\n", rel.URL, rel.Latency)
+		}
 	}
 	}
-	for _, addr := range relays {
-		log.Println("relay:", addr)
+}
+
+func checkServer(deviceID protocol.DeviceID, server string) checkResult {
+	disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, nil)
+	if err != nil {
+		return checkResult{error: err}
 	}
 	}
+
+	res := make(chan checkResult, 1)
+
+	time.AfterFunc(timeout, func() {
+		res <- checkResult{error: errors.New("timeout")}
+	})
+
+	go func() {
+		direct, relays, err := disco.Lookup(deviceID)
+		res <- checkResult{direct: direct, relays: relays, error: err}
+	}()
+
+	return <-res
+}
+
+func usage() {
+	fmt.Printf("Usage:\n\t%s [options] <device ID>\n\nOptions:\n", os.Args[0])
+	flag.PrintDefaults()
 }
 }

+ 14 - 2
cmd/syncthing/externaladdr.go → cmd/syncthing/addresslister.go

@@ -32,6 +32,17 @@ func newAddressLister(upnpSvc *upnpSvc, cfg *config.Wrapper) *addressLister {
 // port number - this means that the outside address of a NAT gateway should
 // port number - this means that the outside address of a NAT gateway should
 // be substituted.
 // be substituted.
 func (e *addressLister) ExternalAddresses() []string {
 func (e *addressLister) ExternalAddresses() []string {
+	return e.addresses(false)
+}
+
+// AllAddresses returns a list of addresses that are our best guess for where
+// we are reachable from the local network. Same conditions as
+// ExternalAddresses, but private IPv4 addresses are included.
+func (e *addressLister) AllAddresses() []string {
+	return e.addresses(true)
+}
+
+func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
 	var addrs []string
 	var addrs []string
 
 
 	// Grab our listen addresses from the config. Unspecified ones are passed
 	// Grab our listen addresses from the config. Unspecified ones are passed
@@ -56,6 +67,9 @@ func (e *addressLister) ExternalAddresses() []string {
 		} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
 		} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
 			// A public address; include as is.
 			// A public address; include as is.
 			addrs = append(addrs, "tcp://"+addr.String())
 			addrs = append(addrs, "tcp://"+addr.String())
+		} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
+			// A private IPv4 address.
+			addrs = append(addrs, "tcp://"+addr.String())
 		}
 		}
 	}
 	}
 
 
@@ -67,8 +81,6 @@ func (e *addressLister) ExternalAddresses() []string {
 		}
 		}
 	}
 	}
 
 
-	l.Infoln("External addresses:", addrs)
-
 	return addrs
 	return addrs
 }
 }
 
 

+ 8 - 7
cmd/syncthing/connections.go

@@ -43,7 +43,7 @@ type connectionSvc struct {
 	myID       protocol.DeviceID
 	myID       protocol.DeviceID
 	model      *model.Model
 	model      *model.Model
 	tlsCfg     *tls.Config
 	tlsCfg     *tls.Config
-	discoverer *discover.Discoverer
+	discoverer discover.Finder
 	conns      chan model.IntermediateConnection
 	conns      chan model.IntermediateConnection
 	relaySvc   *relay.Svc
 	relaySvc   *relay.Svc
 
 
@@ -54,7 +54,7 @@ type connectionSvc struct {
 	relaysEnabled bool
 	relaysEnabled bool
 }
 }
 
 
-func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config, discoverer *discover.Discoverer, relaySvc *relay.Svc) *connectionSvc {
+func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config, discoverer discover.Finder, relaySvc *relay.Svc) *connectionSvc {
 	svc := &connectionSvc{
 	svc := &connectionSvc{
 		Supervisor: suture.NewSimple("connectionSvc"),
 		Supervisor: suture.NewSimple("connectionSvc"),
 		cfg:        cfg,
 		cfg:        cfg,
@@ -264,13 +264,14 @@ func (s *connectionSvc) connect() {
 			}
 			}
 
 
 			var addrs []string
 			var addrs []string
-			var relays []string
+			var relays []discover.Relay
 			for _, addr := range deviceCfg.Addresses {
 			for _, addr := range deviceCfg.Addresses {
 				if addr == "dynamic" {
 				if addr == "dynamic" {
 					if s.discoverer != nil {
 					if s.discoverer != nil {
-						t, r := s.discoverer.Lookup(deviceID)
-						addrs = append(addrs, t...)
-						relays = append(relays, r...)
+						if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
+							addrs = append(addrs, t...)
+							relays = append(relays, r...)
+						}
 					}
 					}
 				} else {
 				} else {
 					addrs = append(addrs, addr)
 					addrs = append(addrs, addr)
@@ -333,7 +334,7 @@ func (s *connectionSvc) connect() {
 			s.lastRelayCheck[deviceID] = time.Now()
 			s.lastRelayCheck[deviceID] = time.Now()
 
 
 			for _, addr := range relays {
 			for _, addr := range relays {
-				uri, err := url.Parse(addr)
+				uri, err := url.Parse(addr.URL)
 				if err != nil {
 				if err != nil {
 					l.Infoln("Failed to parse relay connection url:", addr, err)
 					l.Infoln("Failed to parse relay connection url:", addr, err)
 					continue
 					continue

+ 31 - 19
cmd/syncthing/gui.go

@@ -33,6 +33,7 @@ import (
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/osutil"
+	"github.com/syncthing/syncthing/lib/relay"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 	"github.com/syncthing/syncthing/lib/upgrade"
 	"github.com/syncthing/syncthing/lib/upgrade"
@@ -58,14 +59,15 @@ type apiSvc struct {
 	assetDir        string
 	assetDir        string
 	model           *model.Model
 	model           *model.Model
 	eventSub        *events.BufferedSubscription
 	eventSub        *events.BufferedSubscription
-	discoverer      *discover.Discoverer
+	discoverer      *discover.CachingMux
+	relaySvc        *relay.Svc
 	listener        net.Listener
 	listener        net.Listener
 	fss             *folderSummarySvc
 	fss             *folderSummarySvc
 	stop            chan struct{}
 	stop            chan struct{}
 	systemConfigMut sync.Mutex
 	systemConfigMut sync.Mutex
 }
 }
 
 
-func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.Discoverer) (*apiSvc, error) {
+func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc) (*apiSvc, error) {
 	svc := &apiSvc{
 	svc := &apiSvc{
 		id:              id,
 		id:              id,
 		cfg:             cfg,
 		cfg:             cfg,
@@ -73,6 +75,7 @@ func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir strin
 		model:           m,
 		model:           m,
 		eventSub:        eventSub,
 		eventSub:        eventSub,
 		discoverer:      discoverer,
 		discoverer:      discoverer,
+		relaySvc:        relaySvc,
 		systemConfigMut: sync.NewMutex(),
 		systemConfigMut: sync.NewMutex(),
 	}
 	}
 
 
@@ -164,7 +167,6 @@ func (s *apiSvc) Serve() {
 	postRestMux.HandleFunc("/rest/db/override", s.postDBOverride)              // folder
 	postRestMux.HandleFunc("/rest/db/override", s.postDBOverride)              // folder
 	postRestMux.HandleFunc("/rest/db/scan", s.postDBScan)                      // folder [sub...] [delay]
 	postRestMux.HandleFunc("/rest/db/scan", s.postDBScan)                      // folder [sub...] [delay]
 	postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig)          // <body>
 	postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig)          // <body>
-	postRestMux.HandleFunc("/rest/system/discovery", s.postSystemDiscovery)    // device addr
 	postRestMux.HandleFunc("/rest/system/error", s.postSystemError)            // <body>
 	postRestMux.HandleFunc("/rest/system/error", s.postSystemError)            // <body>
 	postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
 	postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
 	postRestMux.HandleFunc("/rest/system/ping", s.restPing)                    // -
 	postRestMux.HandleFunc("/rest/system/ping", s.restPing)                    // -
@@ -630,11 +632,30 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
 	res["alloc"] = m.Alloc
 	res["alloc"] = m.Alloc
 	res["sys"] = m.Sys - m.HeapReleased
 	res["sys"] = m.Sys - m.HeapReleased
 	res["tilde"] = tilde
 	res["tilde"] = tilde
-	if cfg.Options().GlobalAnnEnabled && s.discoverer != nil {
-		res["extAnnounceOK"] = s.discoverer.ExtAnnounceOK()
+	if cfg.Options().LocalAnnEnabled || cfg.Options().GlobalAnnEnabled {
+		res["discoveryEnabled"] = true
+		discoErrors := make(map[string]string)
+		discoMethods := 0
+		for disco, err := range s.discoverer.ChildErrors() {
+			discoMethods++
+			if err != nil {
+				discoErrors[disco] = err.Error()
+			}
+		}
+		res["discoveryMethods"] = discoMethods
+		res["discoveryErrors"] = discoErrors
 	}
 	}
-	if relaySvc != nil {
-		res["relayClientStatus"] = relaySvc.ClientStatus()
+	if s.relaySvc != nil {
+		res["relaysEnabled"] = true
+		relayClientStatus := make(map[string]bool)
+		relayClientLatency := make(map[string]int)
+		for _, relay := range s.relaySvc.Relays() {
+			latency, ok := s.relaySvc.RelayStatus(relay)
+			relayClientStatus[relay] = ok
+			relayClientLatency[relay] = int(latency / time.Millisecond)
+		}
+		res["relayClientStatus"] = relayClientStatus
+		res["relayClientLatency"] = relayClientLatency
 	}
 	}
 	cpuUsageLock.RLock()
 	cpuUsageLock.RLock()
 	var cpusum float64
 	var cpusum float64
@@ -679,25 +700,16 @@ func (s *apiSvc) showGuiError(l logger.LogLevel, err string) {
 	guiErrorsMut.Unlock()
 	guiErrorsMut.Unlock()
 }
 }
 
 
-func (s *apiSvc) postSystemDiscovery(w http.ResponseWriter, r *http.Request) {
-	var qs = r.URL.Query()
-	var device = qs.Get("device")
-	var addr = qs.Get("addr")
-	if len(device) != 0 && len(addr) != 0 && s.discoverer != nil {
-		s.discoverer.Hint(device, []string{addr})
-	}
-}
-
 func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
 func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
-	devices := map[string][]discover.CacheEntry{}
+	devices := make(map[string]discover.CacheEntry)
 
 
 	if s.discoverer != nil {
 	if s.discoverer != nil {
 		// Device ids can't be marshalled as keys so we need to manually
 		// Device ids can't be marshalled as keys so we need to manually
 		// rebuild this map using strings. Discoverer may be nil if discovery
 		// rebuild this map using strings. Discoverer may be nil if discovery
 		// has not started yet.
 		// has not started yet.
-		for device, entries := range s.discoverer.All() {
-			devices[device.String()] = entries
+		for device, entry := range s.discoverer.Cache() {
+			devices[device.String()] = entry
 		}
 		}
 	}
 	}
 
 

+ 46 - 36
cmd/syncthing/main.go

@@ -114,7 +114,6 @@ var (
 	writeRateLimit *ratelimit.Bucket
 	writeRateLimit *ratelimit.Bucket
 	readRateLimit  *ratelimit.Bucket
 	readRateLimit  *ratelimit.Bucket
 	stop           = make(chan int)
 	stop           = make(chan int)
-	relaySvc       *relay.Svc
 	cert           tls.Certificate
 	cert           tls.Certificate
 	lans           []*net.IPNet
 	lans           []*net.IPNet
 )
 )
@@ -689,8 +688,7 @@ func syncthingMain() {
 
 
 	var addrList *addressLister
 	var addrList *addressLister
 
 
-	// Start UPnP. The UPnP service will restart global discovery if the
-	// external port changes.
+	// Start UPnP
 
 
 	if opts.UPnPEnabled {
 	if opts.UPnPEnabled {
 		upnpSvc := newUPnPSvc(cfg, addr.Port)
 		upnpSvc := newUPnPSvc(cfg, addr.Port)
@@ -703,14 +701,6 @@ func syncthingMain() {
 		addrList = newAddressLister(nil, cfg)
 		addrList = newAddressLister(nil, cfg)
 	}
 	}
 
 
-	// Start discovery
-
-	discoverer := discovery(addrList, relaySvc)
-
-	// GUI
-
-	setupGUI(mainSvc, cfg, m, apiSub, discoverer)
-
 	// Start relay management
 	// Start relay management
 
 
 	var relaySvc *relay.Svc
 	var relaySvc *relay.Svc
@@ -719,9 +709,51 @@ func syncthingMain() {
 		mainSvc.Add(relaySvc)
 		mainSvc.Add(relaySvc)
 	}
 	}
 
 
+	// Start discovery
+
+	cachedDiscovery := discover.NewCachingMux()
+	mainSvc.Add(cachedDiscovery)
+
+	if cfg.Options().GlobalAnnEnabled {
+		for _, srv := range cfg.GlobalDiscoveryServers() {
+			l.Infoln("Using discovery server", srv)
+			gd, err := discover.NewGlobal(srv, cert, addrList, relaySvc)
+			if err != nil {
+				l.Warnln("Global discovery:", err)
+				continue
+			}
+
+			// Each global discovery server gets its results cached for five
+			// minutes, and is not asked again for a minute when it's returned
+			// unsuccessfully.
+			cachedDiscovery.Add(gd, 5*time.Minute, time.Minute)
+		}
+	}
+
+	if cfg.Options().LocalAnnEnabled {
+		// v4 broadcasts
+		bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relaySvc)
+		if err != nil {
+			l.Warnln("IPv4 local discovery:", err)
+		} else {
+			cachedDiscovery.Add(bcd, 0, 0)
+		}
+		// v6 multicasts
+		mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relaySvc)
+		if err != nil {
+			l.Warnln("IPv6 local discovery:", err)
+		} else {
+			cachedDiscovery.Add(mcd, 0, 0)
+		}
+	}
+
+	// GUI
+
+	setupGUI(mainSvc, cfg, m, apiSub, cachedDiscovery, relaySvc)
+
 	// Start connection management
 	// Start connection management
 
 
-	connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg, discoverer, relaySvc)
+	connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg, cachedDiscovery, relaySvc)
 	mainSvc.Add(connectionSvc)
 	mainSvc.Add(connectionSvc)
 
 
 	if cpuProfile {
 	if cpuProfile {
@@ -844,7 +876,7 @@ func startAuditing(mainSvc *suture.Supervisor) {
 	l.Infoln("Audit log in", auditFile)
 	l.Infoln("Audit log in", auditFile)
 }
 }
 
 
-func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.Discoverer) {
+func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc) {
 	opts := cfg.Options()
 	opts := cfg.Options()
 	guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
 	guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
 
 
@@ -873,7 +905,7 @@ func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, a
 
 
 			urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
 			urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
 			l.Infoln("Starting web GUI on", urlShow)
 			l.Infoln("Starting web GUI on", urlShow)
-			api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub, discoverer)
+			api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub, discoverer, relaySvc)
 			if err != nil {
 			if err != nil {
 				l.Fatalln("Cannot start GUI:", err)
 				l.Fatalln("Cannot start GUI:", err)
 			}
 			}
@@ -944,28 +976,6 @@ func shutdown() {
 	stop <- exitSuccess
 	stop <- exitSuccess
 }
 }
 
 
-func discovery(addrList *addressLister, relaySvc *relay.Svc) *discover.Discoverer {
-	opts := cfg.Options()
-	disc := discover.NewDiscoverer(myID, opts.ListenAddress, relaySvc)
-	if opts.LocalAnnEnabled {
-		l.Infoln("Starting local discovery announcements")
-		disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr)
-	}
-
-	if opts.GlobalAnnEnabled {
-		go func() {
-			// Defer starting global announce server, giving time to connect
-			// to relay servers.
-			time.Sleep(5 * time.Second)
-			l.Infoln("Starting global discovery announcements")
-			disc.StartGlobal(opts.GlobalAnnServers, addrList)
-		}()
-
-	}
-
-	return disc
-}
-
 func ensureDir(dir string, mode int) {
 func ensureDir(dir string, mode int) {
 	fi, err := os.Stat(dir)
 	fi, err := os.Stat(dir)
 	if os.IsNotExist(err) {
 	if os.IsNotExist(err) {

+ 11 - 0
cmd/syncthing/verbose.go

@@ -8,6 +8,7 @@ package main
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"strings"
 
 
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/events"
 )
 )
@@ -139,6 +140,16 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
 		data := ev.Data.(map[string]string)
 		data := ev.Data.(map[string]string)
 		device := data["device"]
 		device := data["device"]
 		return fmt.Sprintf("Device %v was resumed", device)
 		return fmt.Sprintf("Device %v was resumed", device)
+
+	case events.ExternalPortMappingChanged:
+		data := ev.Data.(map[string]int)
+		port := data["port"]
+		return fmt.Sprintf("External port mapping changed; new port is %d.", port)
+	case events.RelayStateChanged:
+		data := ev.Data.(map[string][]string)
+		newRelays := data["new"]
+		return fmt.Sprintf("Relay state changed; connected relay(s) are %s.", strings.Join(newRelays, ", "))
+
 	}
 	}
 
 
 	return fmt.Sprintf("%s %#v", ev.Type, ev)
 	return fmt.Sprintf("%s %#v", ev.Type, ev)

+ 4 - 0
gui/assets/css/overrides.css

@@ -249,3 +249,7 @@ ul.three-columns li, ul.two-columns li {
         position: static;
         position: static;
     }
     }
 }
 }
+
+.popover {
+    min-width: 250px;
+}

+ 13 - 13
gui/index.html

@@ -379,28 +379,28 @@
                     <th><span class="fa fa-fw fa-tachometer"></span>&nbsp;<span translate>CPU Utilization</span></th>
                     <th><span class="fa fa-fw fa-tachometer"></span>&nbsp;<span translate>CPU Utilization</span></th>
                     <td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
                     <td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
                   </tr>
                   </tr>
-                  <tr ng-if="system.extAnnounceOK != undefined && announceServersTotal > 0">
-                    <th><span class="fa fa-fw fa-bullhorn"></span>&nbsp;<span translate>Global Discovery</span></th>
+                  <tr ng-if="system.discoveryEnabled">
+                    <th><span class="fa fa-fw fa-map-signs"></span>&nbsp;<span translate>Discovery</span></th>
                     <td class="text-right">
                     <td class="text-right">
-                      <span ng-if="announceServersFailed.length == 0" class="data text-success">
-                        <span>OK</span>
+                      <span ng-if="discoveryFailed.length == 0" class="data text-success">
+                        <span>{{discoveryTotal}}/{{discoveryTotal}}</span>
                       </span>
                       </span>
-                      <span ng-if="announceServersFailed.length != 0" class="data" ng-class="{'text-danger': announceServersFailed.length == announceServersTotal}">
-                        <span popover data-trigger="hover" data-placement="bottom" data-content="{{announceServersFailed.join('\n')}}">
-                          {{announceServersTotal-announceServersFailed.length}}/{{announceServersTotal}}
+                      <span ng-if="discoveryFailed.length != 0" class="data" ng-class="{'text-danger': discoveryFailed.length == discoveryTotal}">
+                        <span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{discoveryFailed.join('<br>\n')}}">
+                          {{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
                         </span>
                         </span>
                       </span>
                       </span>
                     </td>
                     </td>
                   </tr>
                   </tr>
-                  <tr ng-if="system.relayClientStatus != undefined && relayClientsTotal > 0">
+                  <tr ng-if="system.relaysEnabled">
                     <th><span class="fa fa-fw fa-sitemap"></span>&nbsp;<span translate>Relays</span></th>
                     <th><span class="fa fa-fw fa-sitemap"></span>&nbsp;<span translate>Relays</span></th>
                     <td class="text-right">
                     <td class="text-right">
-                      <span ng-if="relayClientsFailed.length == 0" class="data text-success">
-                        <span>OK</span>
+                      <span ng-if="relaysFailed.length == 0" class="data text-success">
+                        <span>{{relaysTotal}}/{{relaysTotal}}</span>
                       </span>
                       </span>
-                      <span ng-if="relayClientsFailed.length != 0" class="data" ng-class="{'text-danger': relayClientsFailed.length == relayClientsTotal}">
-                        <span popover data-trigger="hover" data-placement="bottom" data-content="{{relayClientsFailed.join('\n')}}">
-                          {{relayClientsTotal-relayClientsFailed.length}}/{{relayClientsTotal}}
+                      <span ng-if="relaysFailed.length != 0" class="data" ng-class="{'text-danger': relaysFailed.length == relaysTotal}">
+                        <span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{relaysFailed.join('<br>\n')}}">
+                          {{relaysTotal-relaysFailed.length}}/{{relaysTotal}}
                         </span>
                         </span>
                       </span>
                       </span>
                     </td>
                     </td>

+ 12 - 11
gui/syncthing/core/syncthingController.js

@@ -378,24 +378,25 @@ angular.module('syncthing.core')
                 $scope.myID = data.myID;
                 $scope.myID = data.myID;
                 $scope.system = data;
                 $scope.system = data;
 
 
-                $scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
-                var failedAnnounce = [];
-                for (var server in data.extAnnounceOK) {
-                    if (!data.extAnnounceOK[server]) {
-                        failedAnnounce.push(server);
+                $scope.discoveryTotal = data.discoveryMethods;
+                var discoveryFailed = [];
+                for (var disco in data.discoveryErrors) {
+                    if (data.discoveryErrors[disco]) {
+                        discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
                     }
                     }
                 }
                 }
-                $scope.announceServersFailed = failedAnnounce;
+                $scope.discoveryFailed = discoveryFailed;
 
 
-                $scope.relayClientsTotal = data.relayClientStatus ? Object.keys(data.relayClientStatus).length : 0;
-                var failedRelays = [];
+                var relaysFailed = [];
+                var relaysTotal = 0;
                 for (var relay in data.relayClientStatus) {
                 for (var relay in data.relayClientStatus) {
                     if (!data.relayClientStatus[relay]) {
                     if (!data.relayClientStatus[relay]) {
-                        failedRelays.push(relay);
+                        relaysFailed.push(relay);
                     }
                     }
+                    relaysTotal++;
                 }
                 }
-                $scope.relayClientsFailed = failedRelays;
-
+                $scope.relaysFailed = relaysFailed;
+                $scope.relaysTotal = relaysTotal;
 
 
                 console.log("refreshSystem", data);
                 console.log("refreshSystem", data);
             }).error($scope.emitHTTPError);
             }).error($scope.emitHTTPError);

+ 1 - 1
gui/syncthing/device/editDeviceModalView.html

@@ -13,7 +13,7 @@
             <label translate for="deviceID">Device ID</label>
             <label translate for="deviceID">Device ID</label>
             <input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
             <input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
             <datalist id="discovery-list" ng-if="!editingExisting">
             <datalist id="discovery-list" ng-if="!editingExisting">
-              <option ng-repeat="(id,address) in discovery" value="{{ id }}" />
+              <option ng-repeat="(id, data) in discovery" value="{{id}}" />
             </datalist>
             </datalist>
             <div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.deviceID}}</div>
             <div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.deviceID}}</div>
             <p class="help-block">
             <p class="help-block">

File diff suppressed because it is too large
+ 1 - 1
lib/auto/gui.files.go


+ 24 - 23
lib/beacon/beacon.go

@@ -6,7 +6,12 @@
 
 
 package beacon
 package beacon
 
 
-import "net"
+import (
+	"net"
+	stdsync "sync"
+
+	"github.com/thejerf/suture"
+)
 
 
 type recv struct {
 type recv struct {
 	data []byte
 	data []byte
@@ -14,34 +19,30 @@ type recv struct {
 }
 }
 
 
 type Interface interface {
 type Interface interface {
+	suture.Service
 	Send(data []byte)
 	Send(data []byte)
 	Recv() ([]byte, net.Addr)
 	Recv() ([]byte, net.Addr)
+	Error() error
 }
 }
 
 
 type readerFrom interface {
 type readerFrom interface {
 	ReadFrom([]byte) (int, net.Addr, error)
 	ReadFrom([]byte) (int, net.Addr, error)
 }
 }
 
 
-func genericReader(conn readerFrom, outbox chan<- recv) {
-	bs := make([]byte, 65536)
-	for {
-		n, addr, err := conn.ReadFrom(bs)
-		if err != nil {
-			l.Warnln("multicast read:", err)
-			return
-		}
-		if debug {
-			l.Debugf("recv %d bytes from %s", n, addr)
-		}
-
-		c := make([]byte, n)
-		copy(c, bs)
-		select {
-		case outbox <- recv{c, addr}:
-		default:
-			if debug {
-				l.Debugln("dropping message")
-			}
-		}
-	}
+type errorHolder struct {
+	err error
+	mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
+}
+
+func (e *errorHolder) setError(err error) {
+	e.mut.Lock()
+	e.err = err
+	e.mut.Unlock()
+}
+
+func (e *errorHolder) Error() error {
+	e.mut.Lock()
+	err := e.err
+	e.mut.Unlock()
+	return err
 }
 }

+ 43 - 27
lib/beacon/broadcast.go

@@ -19,6 +19,8 @@ type Broadcast struct {
 	port   int
 	port   int
 	inbox  chan []byte
 	inbox  chan []byte
 	outbox chan recv
 	outbox chan recv
+	br     *broadcastReader
+	bw     *broadcastWriter
 }
 }
 
 
 func NewBroadcast(port int) *Broadcast {
 func NewBroadcast(port int) *Broadcast {
@@ -41,14 +43,16 @@ func NewBroadcast(port int) *Broadcast {
 		outbox: make(chan recv, 16),
 		outbox: make(chan recv, 16),
 	}
 	}
 
 
-	b.Add(&broadcastReader{
+	b.br = &broadcastReader{
 		port:   port,
 		port:   port,
 		outbox: b.outbox,
 		outbox: b.outbox,
-	})
-	b.Add(&broadcastWriter{
+	}
+	b.Add(b.br)
+	b.bw = &broadcastWriter{
 		port:  port,
 		port:  port,
 		inbox: b.inbox,
 		inbox: b.inbox,
-	})
+	}
+	b.Add(b.bw)
 
 
 	return b
 	return b
 }
 }
@@ -62,11 +66,18 @@ func (b *Broadcast) Recv() ([]byte, net.Addr) {
 	return recv.data, recv.src
 	return recv.data, recv.src
 }
 }
 
 
+func (b *Broadcast) Error() error {
+	if err := b.br.Error(); err != nil {
+		return err
+	}
+	return b.bw.Error()
+}
+
 type broadcastWriter struct {
 type broadcastWriter struct {
-	port   int
-	inbox  chan []byte
-	conn   *net.UDPConn
-	failed bool // Have we already logged a failure reason?
+	port  int
+	inbox chan []byte
+	conn  *net.UDPConn
+	errorHolder
 }
 }
 
 
 func (w *broadcastWriter) Serve() {
 func (w *broadcastWriter) Serve() {
@@ -78,22 +89,21 @@ func (w *broadcastWriter) Serve() {
 	var err error
 	var err error
 	w.conn, err = net.ListenUDP("udp4", nil)
 	w.conn, err = net.ListenUDP("udp4", nil)
 	if err != nil {
 	if err != nil {
-		if !w.failed {
-			l.Warnln("Local discovery over IPv4 unavailable:", err)
-			w.failed = true
+		if debug {
+			l.Debugln(err)
 		}
 		}
+		w.setError(err)
 		return
 		return
 	}
 	}
 	defer w.conn.Close()
 	defer w.conn.Close()
 
 
-	w.failed = false
-
 	for bs := range w.inbox {
 	for bs := range w.inbox {
 		addrs, err := net.InterfaceAddrs()
 		addrs, err := net.InterfaceAddrs()
 		if err != nil {
 		if err != nil {
 			if debug {
 			if debug {
-				l.Debugln("Local discovery (broadcast writer):", err)
+				l.Debugln(err)
 			}
 			}
+			w.setError(err)
 			continue
 			continue
 		}
 		}
 
 
@@ -117,13 +127,16 @@ func (w *broadcastWriter) Serve() {
 		for _, ip := range dsts {
 		for _, ip := range dsts {
 			dst := &net.UDPAddr{IP: ip, Port: w.port}
 			dst := &net.UDPAddr{IP: ip, Port: w.port}
 
 
-			w.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
+			w.conn.SetWriteDeadline(time.Now().Add(time.Second))
 			_, err := w.conn.WriteTo(bs, dst)
 			_, err := w.conn.WriteTo(bs, dst)
+			w.conn.SetWriteDeadline(time.Time{})
 			if err, ok := err.(net.Error); ok && err.Timeout() {
 			if err, ok := err.(net.Error); ok && err.Timeout() {
 				// Write timeouts should not happen. We treat it as a fatal
 				// Write timeouts should not happen. We treat it as a fatal
 				// error on the socket.
 				// error on the socket.
-				l.Infoln("Local discovery (broadcast writer):", err)
-				w.failed = true
+				if debug {
+					l.Debugln(err)
+				}
+				w.setError(err)
 				return
 				return
 			} else if err, ok := err.(net.Error); ok && err.Temporary() {
 			} else if err, ok := err.(net.Error); ok && err.Temporary() {
 				// A transient error. Lets hope for better luck in the future.
 				// A transient error. Lets hope for better luck in the future.
@@ -133,11 +146,14 @@ func (w *broadcastWriter) Serve() {
 				continue
 				continue
 			} else if err != nil {
 			} else if err != nil {
 				// Some other error that we don't expect. Bail and retry.
 				// Some other error that we don't expect. Bail and retry.
-				l.Infoln("Local discovery (broadcast writer):", err)
-				w.failed = true
+				if debug {
+					l.Debugln(err)
+				}
+				w.setError(err)
 				return
 				return
 			} else if debug {
 			} else if debug {
 				l.Debugf("sent %d bytes to %s", len(bs), dst)
 				l.Debugf("sent %d bytes to %s", len(bs), dst)
+				w.setError(nil)
 			}
 			}
 		}
 		}
 	}
 	}
@@ -155,7 +171,7 @@ type broadcastReader struct {
 	port   int
 	port   int
 	outbox chan recv
 	outbox chan recv
 	conn   *net.UDPConn
 	conn   *net.UDPConn
-	failed bool
+	errorHolder
 }
 }
 
 
 func (r *broadcastReader) Serve() {
 func (r *broadcastReader) Serve() {
@@ -167,10 +183,10 @@ func (r *broadcastReader) Serve() {
 	var err error
 	var err error
 	r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
 	r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
 	if err != nil {
 	if err != nil {
-		if !r.failed {
-			l.Warnln("Local discovery over IPv4 unavailable:", err)
-			r.failed = true
+		if debug {
+			l.Debugln(err)
 		}
 		}
+		r.setError(err)
 		return
 		return
 	}
 	}
 	defer r.conn.Close()
 	defer r.conn.Close()
@@ -179,14 +195,14 @@ func (r *broadcastReader) Serve() {
 	for {
 	for {
 		n, addr, err := r.conn.ReadFrom(bs)
 		n, addr, err := r.conn.ReadFrom(bs)
 		if err != nil {
 		if err != nil {
-			if !r.failed {
-				l.Infoln("Local discovery (broadcast reader):", err)
-				r.failed = true
+			if debug {
+				l.Debugln(err)
 			}
 			}
+			r.setError(err)
 			return
 			return
 		}
 		}
 
 
-		r.failed = false
+		r.setError(nil)
 
 
 		if debug {
 		if debug {
 			l.Debugf("recv %d bytes from %s", n, addr)
 			l.Debugf("recv %d bytes from %s", n, addr)

+ 200 - 53
lib/beacon/multicast.go

@@ -8,39 +8,200 @@ package beacon
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"fmt"
 	"net"
 	"net"
+	"time"
 
 
+	"github.com/thejerf/suture"
 	"golang.org/x/net/ipv6"
 	"golang.org/x/net/ipv6"
 )
 )
 
 
 type Multicast struct {
 type Multicast struct {
-	conn   *ipv6.PacketConn
+	*suture.Supervisor
 	addr   *net.UDPAddr
 	addr   *net.UDPAddr
 	inbox  chan []byte
 	inbox  chan []byte
 	outbox chan recv
 	outbox chan recv
-	intfs  []net.Interface
+	mr     *multicastReader
+	mw     *multicastWriter
 }
 }
 
 
-func NewMulticast(addr string) (*Multicast, error) {
-	gaddr, err := net.ResolveUDPAddr("udp6", addr)
+func NewMulticast(addr string) *Multicast {
+	m := &Multicast{
+		Supervisor: suture.New("multicastBeacon", suture.Spec{
+			// Don't retry too frenetically: an error to open a socket or
+			// whatever is usually something that is either permanent or takes
+			// a while to get solved...
+			FailureThreshold: 2,
+			FailureBackoff:   60 * time.Second,
+			// Only log restarts in debug mode.
+			Log: func(line string) {
+				if debug {
+					l.Debugln(line)
+				}
+			},
+		}),
+		inbox:  make(chan []byte),
+		outbox: make(chan recv, 16),
+	}
+
+	m.mr = &multicastReader{
+		addr:   addr,
+		outbox: m.outbox,
+		stop:   make(chan struct{}),
+	}
+	m.Add(m.mr)
+
+	m.mw = &multicastWriter{
+		addr:  addr,
+		inbox: m.inbox,
+		stop:  make(chan struct{}),
+	}
+	m.Add(m.mw)
+
+	return m
+}
+
+func (m *Multicast) Send(data []byte) {
+	m.inbox <- data
+}
+
+func (m *Multicast) Recv() ([]byte, net.Addr) {
+	recv := <-m.outbox
+	return recv.data, recv.src
+}
+
+func (m *Multicast) Error() error {
+	if err := m.mr.Error(); err != nil {
+		return err
+	}
+	return m.mw.Error()
+}
+
+type multicastWriter struct {
+	addr  string
+	inbox <-chan []byte
+	errorHolder
+	stop chan struct{}
+}
+
+func (w *multicastWriter) Serve() {
+	if debug {
+		l.Debugln(w, "starting")
+		defer l.Debugln(w, "stopping")
+	}
+
+	gaddr, err := net.ResolveUDPAddr("udp6", w.addr)
+	if err != nil {
+		if debug {
+			l.Debugln(err)
+		}
+		w.setError(err)
+		return
+	}
+
+	conn, err := net.ListenPacket("udp6", ":0")
+	if err != nil {
+		if debug {
+			l.Debugln(err)
+		}
+		w.setError(err)
+		return
+	}
+
+	pconn := ipv6.NewPacketConn(conn)
+
+	wcm := &ipv6.ControlMessage{
+		HopLimit: 1,
+	}
+
+	for bs := range w.inbox {
+		intfs, err := net.Interfaces()
+		if err != nil {
+			if debug {
+				l.Debugln(err)
+			}
+			w.setError(err)
+			return
+		}
+
+		var success int
+
+		for _, intf := range intfs {
+			wcm.IfIndex = intf.Index
+			pconn.SetWriteDeadline(time.Now().Add(time.Second))
+			_, err = pconn.WriteTo(bs, wcm, gaddr)
+			pconn.SetWriteDeadline(time.Time{})
+			if err != nil && debug {
+				l.Debugln(err, "on write to", gaddr, intf.Name)
+			} else if debug {
+				l.Debugf("sent %d bytes to %v on %s", len(bs), gaddr, intf.Name)
+				success++
+			}
+		}
+
+		if success > 0 {
+			w.setError(nil)
+		} else {
+			if debug {
+				l.Debugln(err)
+			}
+			w.setError(err)
+		}
+	}
+}
+
+func (w *multicastWriter) Stop() {
+	close(w.stop)
+}
+
+func (w *multicastWriter) String() string {
+	return fmt.Sprintf("multicastWriter@%p", w)
+}
+
+type multicastReader struct {
+	addr   string
+	outbox chan<- recv
+	errorHolder
+	stop chan struct{}
+}
+
+func (r *multicastReader) Serve() {
+	if debug {
+		l.Debugln(r, "starting")
+		defer l.Debugln(r, "stopping")
+	}
+
+	gaddr, err := net.ResolveUDPAddr("udp6", r.addr)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		if debug {
+			l.Debugln(err)
+		}
+		r.setError(err)
+		return
 	}
 	}
 
 
-	conn, err := net.ListenPacket("udp6", addr)
+	conn, err := net.ListenPacket("udp6", r.addr)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		if debug {
+			l.Debugln(err)
+		}
+		r.setError(err)
+		return
 	}
 	}
 
 
 	intfs, err := net.Interfaces()
 	intfs, err := net.Interfaces()
 	if err != nil {
 	if err != nil {
-		return nil, err
+		if debug {
+			l.Debugln(err)
+		}
+		r.setError(err)
+		return
 	}
 	}
 
 
-	p := ipv6.NewPacketConn(conn)
+	pconn := ipv6.NewPacketConn(conn)
 	joined := 0
 	joined := 0
 	for _, intf := range intfs {
 	for _, intf := range intfs {
-		err := p.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
+		err := pconn.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
 		if debug {
 		if debug {
 			if err != nil {
 			if err != nil {
 				l.Debugln("IPv6 join", intf.Name, "failed:", err)
 				l.Debugln("IPv6 join", intf.Name, "failed:", err)
@@ -52,57 +213,43 @@ func NewMulticast(addr string) (*Multicast, error) {
 	}
 	}
 
 
 	if joined == 0 {
 	if joined == 0 {
-		return nil, errors.New("no multicast interfaces available")
-	}
-
-	b := &Multicast{
-		conn:   p,
-		addr:   gaddr,
-		inbox:  make(chan []byte),
-		outbox: make(chan recv, 16),
-		intfs:  intfs,
+		if debug {
+			l.Debugln("no multicast interfaces available")
+		}
+		r.setError(errors.New("no multicast interfaces available"))
+		return
 	}
 	}
 
 
-	go genericReader(ipv6ReaderAdapter{b.conn}, b.outbox)
-	go b.writer()
-
-	return b, nil
-}
-
-func (b *Multicast) Send(data []byte) {
-	b.inbox <- data
-}
-
-func (b *Multicast) Recv() ([]byte, net.Addr) {
-	recv := <-b.outbox
-	return recv.data, recv.src
-}
-
-func (b *Multicast) writer() {
-	wcm := &ipv6.ControlMessage{
-		HopLimit: 1,
-	}
+	bs := make([]byte, 65536)
+	for {
+		n, _, addr, err := pconn.ReadFrom(bs)
+		if err != nil {
+			if debug {
+				l.Debugln(err)
+			}
+			r.setError(err)
+			continue
+		}
+		if debug {
+			l.Debugf("recv %d bytes from %s", n, addr)
+		}
 
 
-	for bs := range b.inbox {
-		for _, intf := range b.intfs {
-			wcm.IfIndex = intf.Index
-			_, err := b.conn.WriteTo(bs, wcm, b.addr)
-			if err != nil && debug {
-				l.Debugln(err, "on write to", b.addr)
-			} else if debug {
-				l.Debugf("sent %d bytes to %v on %s", len(bs), b.addr, intf.Name)
+		c := make([]byte, n)
+		copy(c, bs)
+		select {
+		case r.outbox <- recv{c, addr}:
+		default:
+			if debug {
+				l.Debugln("dropping message")
 			}
 			}
 		}
 		}
 	}
 	}
 }
 }
 
 
-// This makes ReadFrom on an *ipv6.PacketConn behave like ReadFrom on a
-// net.PacketConn.
-type ipv6ReaderAdapter struct {
-	c *ipv6.PacketConn
+func (r *multicastReader) Stop() {
+	close(r.stop)
 }
 }
 
 
-func (i ipv6ReaderAdapter) ReadFrom(bs []byte) (int, net.Addr, error) {
-	n, _, src, err := i.c.ReadFrom(bs)
-	return n, src, err
+func (r *multicastReader) String() string {
+	return fmt.Sprintf("multicastReader@%p", r)
 }
 }

+ 27 - 8
lib/config/config.go

@@ -31,6 +31,21 @@ const (
 	MaxRescanIntervalS   = 365 * 24 * 60 * 60
 	MaxRescanIntervalS   = 365 * 24 * 60 * 60
 )
 )
 
 
+var (
+	// DefaultDiscoveryServers should be substituted when the configuration
+	// contains <globalAnnounceServer>default</globalAnnounceServer>. This is
+	// done by the "consumer" of the configuration, as we don't want these
+	// saved to the config.
+	DefaultDiscoveryServers = []string{
+		"https://v4-1.discover.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
+		"https://v4-2.discover.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 45.55.230.38, USA
+		"https://v4-3.discover.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 128.199.95.124, Singapore
+		"https://v6-1.discover.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
+		"https://v6-2.discover.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 2604:a880:800:10::182:a001, USA
+		"https://v6-3.discover.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 2400:6180:0:d0::d9:d001, Singapore
+	}
+)
+
 type Configuration struct {
 type Configuration struct {
 	Version        int                   `xml:"version,attr" json:"version"`
 	Version        int                   `xml:"version,attr" json:"version"`
 	Folders        []FolderConfiguration `xml:"folder" json:"folders"`
 	Folders        []FolderConfiguration `xml:"folder" json:"folders"`
@@ -215,7 +230,7 @@ type FolderDeviceConfiguration struct {
 
 
 type OptionsConfiguration struct {
 type OptionsConfiguration struct {
 	ListenAddress           []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
 	ListenAddress           []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
-	GlobalAnnServers        []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22027, udp6://announce-v6.syncthing.net:22027"`
+	GlobalAnnServers        []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
 	GlobalAnnEnabled        bool     `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
 	GlobalAnnEnabled        bool     `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
 	LocalAnnEnabled         bool     `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
 	LocalAnnEnabled         bool     `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
 	LocalAnnPort            int      `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
 	LocalAnnPort            int      `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
@@ -498,17 +513,21 @@ func convertV11V12(cfg *Configuration) {
 	}
 	}
 
 
 	// Use new discovery server
 	// Use new discovery server
-	for i, addr := range cfg.Options.GlobalAnnServers {
+	var newDiscoServers []string
+	var useDefault bool
+	for _, addr := range cfg.Options.GlobalAnnServers {
 		if addr == "udp4://announce.syncthing.net:22026" {
 		if addr == "udp4://announce.syncthing.net:22026" {
-			cfg.Options.GlobalAnnServers[i] = "udp4://announce.syncthing.net:22027"
+			useDefault = true
 		} else if addr == "udp6://announce-v6.syncthing.net:22026" {
 		} else if addr == "udp6://announce-v6.syncthing.net:22026" {
-			cfg.Options.GlobalAnnServers[i] = "udp6://announce-v6.syncthing.net:22027"
-		} else if addr == "udp4://194.126.249.5:22026" {
-			cfg.Options.GlobalAnnServers[i] = "udp4://194.126.249.5:22027"
-		} else if addr == "udp6://[2001:470:28:4d6::5]:22026" {
-			cfg.Options.GlobalAnnServers[i] = "udp6://[2001:470:28:4d6::5]:22027"
+			useDefault = true
+		} else {
+			newDiscoServers = append(newDiscoServers, addr)
 		}
 		}
 	}
 	}
+	if useDefault {
+		newDiscoServers = append(newDiscoServers, "default")
+	}
+	cfg.Options.GlobalAnnServers = newDiscoServers
 
 
 	// Use new multicast group
 	// Use new multicast group
 	if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" {
 	if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" {

+ 1 - 1
lib/config/config_test.go

@@ -32,7 +32,7 @@ func init() {
 func TestDefaultValues(t *testing.T) {
 func TestDefaultValues(t *testing.T) {
 	expected := OptionsConfiguration{
 	expected := OptionsConfiguration{
 		ListenAddress:           []string{"tcp://0.0.0.0:22000"},
 		ListenAddress:           []string{"tcp://0.0.0.0:22000"},
-		GlobalAnnServers:        []string{"udp4://announce.syncthing.net:22027", "udp6://announce-v6.syncthing.net:22027"},
+		GlobalAnnServers:        []string{"default"},
 		GlobalAnnEnabled:        true,
 		GlobalAnnEnabled:        true,
 		LocalAnnEnabled:         true,
 		LocalAnnEnabled:         true,
 		LocalAnnPort:            21027,
 		LocalAnnPort:            21027,

+ 12 - 0
lib/config/wrapper.go

@@ -317,3 +317,15 @@ func (w *Wrapper) Save() error {
 	events.Default.Log(events.ConfigSaved, w.cfg)
 	events.Default.Log(events.ConfigSaved, w.cfg)
 	return nil
 	return nil
 }
 }
+
+func (w *Wrapper) GlobalDiscoveryServers() []string {
+	var servers []string
+	for _, srv := range w.cfg.Options.GlobalAnnServers {
+		if srv == "default" {
+			servers = append(servers, DefaultDiscoveryServers...)
+		} else {
+			servers = append(servers, srv)
+		}
+	}
+	return uniqueStrings(servers)
+}

+ 192 - 0
lib/discover/cache.go

@@ -0,0 +1,192 @@
+package discover
+
+import (
+	stdsync "sync"
+	"time"
+
+	"github.com/syncthing/protocol"
+	"github.com/syncthing/syncthing/lib/sync"
+	"github.com/thejerf/suture"
+)
+
+// The CachingMux aggregates results from multiple Finders. Each Finder has
+// an associated cache time and negative cache time. The cache time sets how
+// long we cache and return successfull lookup results, the negative cache
+// time sets how long we refrain from asking about the same device ID after
+// receiving a negative answer. The value of zero disables caching (positive
+// or negative).
+type CachingMux struct {
+	*suture.Supervisor
+	finders []cachedFinder
+	caches  []*cache
+	mut     sync.Mutex
+}
+
+// A cachedFinder is a Finder with associated cache timeouts.
+type cachedFinder struct {
+	Finder
+	cacheTime    time.Duration
+	negCacheTime time.Duration
+}
+
+func NewCachingMux() *CachingMux {
+	return &CachingMux{
+		Supervisor: suture.NewSimple("discover.cachingMux"),
+		mut:        sync.NewMutex(),
+	}
+}
+
+// Add registers a new Finder, with associated cache timeouts.
+func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration) {
+	m.mut.Lock()
+	m.finders = append(m.finders, cachedFinder{finder, cacheTime, negCacheTime})
+	m.caches = append(m.caches, newCache())
+	m.mut.Unlock()
+
+	if svc, ok := finder.(suture.Service); ok {
+		m.Supervisor.Add(svc)
+	}
+}
+
+// Lookup attempts to resolve the device ID using any of the added Finders,
+// while obeying the cache settings.
+func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
+	m.mut.Lock()
+	for i, finder := range m.finders {
+		if cacheEntry, ok := m.caches[i].Get(deviceID); ok {
+			// We have a cache entry. Lets see what it says.
+
+			if cacheEntry.found && time.Since(cacheEntry.when) < finder.cacheTime {
+				// It's a positive, valid entry. Use it.
+				if debug {
+					l.Debugln("cached discovery entry for", deviceID, "at", finder.String())
+					l.Debugln("   ", cacheEntry)
+				}
+				direct = append(direct, cacheEntry.Direct...)
+				relays = append(relays, cacheEntry.Relays...)
+				continue
+			}
+
+			if !cacheEntry.found && time.Since(cacheEntry.when) < finder.negCacheTime {
+				// It's a negative, valid entry. We should not make another
+				// attempt right now.
+				if debug {
+					l.Debugln("negative cache entry for", deviceID, "at", finder.String())
+				}
+				continue
+			}
+
+			// It's expired. Ignore and continue.
+		}
+
+		// Perform the actual lookup and cache the result.
+		if td, tr, err := finder.Lookup(deviceID); err == nil {
+			if debug {
+				l.Debugln("lookup for", deviceID, "at", finder.String())
+				l.Debugln("   ", td)
+				l.Debugln("   ", tr)
+			}
+			direct = append(direct, td...)
+			relays = append(relays, tr...)
+			m.caches[i].Set(deviceID, CacheEntry{
+				Direct: td,
+				Relays: tr,
+				when:   time.Now(),
+				found:  len(td)+len(tr) > 0,
+			})
+		}
+	}
+	m.mut.Unlock()
+
+	if debug {
+		l.Debugln("lookup results for", deviceID)
+		l.Debugln("   ", direct)
+		l.Debugln("   ", relays)
+	}
+
+	return direct, relays, nil
+}
+
+func (m *CachingMux) String() string {
+	return "discovery cache"
+}
+
+func (m *CachingMux) Error() error {
+	return nil
+}
+
+func (m *CachingMux) ChildErrors() map[string]error {
+	m.mut.Lock()
+	children := make(map[string]error, len(m.finders))
+	for _, f := range m.finders {
+		children[f.String()] = f.Error()
+	}
+	m.mut.Unlock()
+	return children
+}
+
+func (m *CachingMux) Cache() map[protocol.DeviceID]CacheEntry {
+	// Res will be the "total" cache, i.e. the union of our cache and all our
+	// children's caches.
+	res := make(map[protocol.DeviceID]CacheEntry)
+
+	m.mut.Lock()
+	for i := range m.finders {
+		// Each finder[i] has a corresponding cache at cache[i]. Go through it
+		// and populate the total, if it's newer than what's already in there.
+		// We skip any negative cache entries.
+		for k, v := range m.caches[i].Cache() {
+			if v.found && v.when.After(res[k].when) {
+				res[k] = v
+			}
+		}
+
+		// Then ask the finder itself for it's cache and do the same. If this
+		// finder is a global discovery client, it will have no cache. If it's
+		// a local discovery client, this will be it's current state.
+		for k, v := range m.finders[i].Cache() {
+			if v.found && v.when.After(res[k].when) {
+				res[k] = v
+			}
+		}
+	}
+	m.mut.Unlock()
+
+	return res
+}
+
+// A cache can be embedded wherever useful
+
+type cache struct {
+	entries map[protocol.DeviceID]CacheEntry
+	mut     stdsync.Mutex
+}
+
+func newCache() *cache {
+	return &cache{
+		entries: make(map[protocol.DeviceID]CacheEntry),
+	}
+}
+
+func (c *cache) Set(id protocol.DeviceID, ce CacheEntry) {
+	c.mut.Lock()
+	c.entries[id] = ce
+	c.mut.Unlock()
+}
+
+func (c *cache) Get(id protocol.DeviceID) (CacheEntry, bool) {
+	c.mut.Lock()
+	ce, ok := c.entries[id]
+	c.mut.Unlock()
+	return ce, ok
+}
+
+func (c *cache) Cache() map[protocol.DeviceID]CacheEntry {
+	c.mut.Lock()
+	m := make(map[protocol.DeviceID]CacheEntry, len(c.entries))
+	for k, v := range c.entries {
+		m[k] = v
+	}
+	c.mut.Unlock()
+	return m
+}

+ 0 - 54
lib/discover/client.go

@@ -1,54 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at http://mozilla.org/MPL/2.0/.
-
-package discover
-
-import (
-	"fmt"
-	"net/url"
-	"time"
-
-	"github.com/syncthing/protocol"
-)
-
-type Announcer interface {
-	Announcement() Announce
-}
-
-type Factory func(*url.URL, Announcer) (Client, error)
-
-var (
-	factories                      = make(map[string]Factory)
-	DefaultErrorRetryInternval     = 60 * time.Second
-	DefaultGlobalBroadcastInterval = 1800 * time.Second
-)
-
-func Register(proto string, factory Factory) {
-	factories[proto] = factory
-}
-
-func New(addr string, announcer Announcer) (Client, error) {
-	uri, err := url.Parse(addr)
-	if err != nil {
-		return nil, err
-	}
-	factory, ok := factories[uri.Scheme]
-	if !ok {
-		return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
-	}
-	client, err := factory(uri, announcer)
-	if err != nil {
-		return nil, err
-	}
-	return client, nil
-}
-
-type Client interface {
-	Lookup(device protocol.DeviceID) (Announce, error)
-	StatusOK() bool
-	Address() string
-	Stop()
-}

+ 0 - 239
lib/discover/client_test.go

@@ -1,239 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at http://mozilla.org/MPL/2.0/.
-
-package discover
-
-import (
-	"fmt"
-	"net"
-	"time"
-
-	"testing"
-
-	"github.com/syncthing/protocol"
-
-	"github.com/syncthing/syncthing/lib/sync"
-)
-
-var device protocol.DeviceID
-
-func init() {
-	device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
-}
-
-type FakeAnnouncer struct {
-	pkt Announce
-}
-
-func (f *FakeAnnouncer) Announcement() Announce {
-	return f.pkt
-}
-
-func TestUDP4Success(t *testing.T) {
-	conn, err := net.ListenUDP("udp4", nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	port := conn.LocalAddr().(*net.UDPAddr).Port
-
-	address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
-	pkt := Announce{
-		Magic: AnnouncementMagic,
-		This: Device{
-			device[:],
-			[]string{"tcp://123.123.123.123:1234"},
-			nil,
-		},
-	}
-	ann := &FakeAnnouncer{
-		pkt: pkt,
-	}
-
-	client, err := New(address, ann)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	udpclient := client.(*UDPClient)
-	if udpclient.errorRetryInterval != DefaultErrorRetryInternval {
-		t.Fatal("Incorrect retry interval")
-	}
-
-	if udpclient.listenAddress.IP != nil || udpclient.listenAddress.Port != 0 {
-		t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
-	}
-
-	if client.Address() != address {
-		t.Fatal("Incorrect address")
-	}
-
-	buf := make([]byte, 2048)
-
-	// First announcement
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
-	_, err = conn.Read(buf)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Announcement verification
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
-	_, addr, err := conn.ReadFromUDP(buf)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Reply to it.
-	_, err = conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// We should get nothing else
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
-	_, err = conn.Read(buf)
-	if err == nil {
-		t.Fatal("Expected error")
-	}
-
-	// Status should be ok
-	if !client.StatusOK() {
-		t.Fatal("Wrong status")
-	}
-
-	// Do a lookup in a separate routine
-	addrs := []string{}
-	wg := sync.NewWaitGroup()
-	wg.Add(1)
-	go func() {
-		pkt, err := client.Lookup(device)
-		if err == nil {
-			for _, addr := range pkt.This.Addresses {
-				addrs = append(addrs, addr)
-			}
-		}
-		wg.Done()
-	}()
-
-	// Receive the lookup and reply
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
-	_, addr, err = conn.ReadFromUDP(buf)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
-
-	// Wait for the lookup to arrive, verify that the number of answers is correct
-	wg.Wait()
-
-	if len(addrs) != 1 || addrs[0] != "tcp://123.123.123.123:1234" {
-		t.Fatal("Wrong number of answers")
-	}
-
-	client.Stop()
-}
-
-func TestUDP4Failure(t *testing.T) {
-	conn, err := net.ListenUDP("udp4", nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	port := conn.LocalAddr().(*net.UDPAddr).Port
-
-	address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
-
-	pkt := Announce{
-		Magic: AnnouncementMagic,
-		This: Device{
-			device[:],
-			[]string{"tcp://123.123.123.123:1234"},
-			nil,
-		},
-	}
-	ann := &FakeAnnouncer{
-		pkt: pkt,
-	}
-
-	client, err := New(address, ann)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	udpclient := client.(*UDPClient)
-	if udpclient.errorRetryInterval != time.Second*5 {
-		t.Fatal("Incorrect retry interval")
-	}
-
-	if !udpclient.listenAddress.IP.Equal(net.IPv4(127, 0, 0, 1)) || udpclient.listenAddress.Port != 0 {
-		t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
-	}
-
-	if client.Address() != address {
-		t.Fatal("Incorrect address")
-	}
-
-	buf := make([]byte, 2048)
-
-	// First announcement
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
-	_, err = conn.Read(buf)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Announcement verification
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
-	_, _, err = conn.ReadFromUDP(buf)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Don't reply
-	// We should get nothing else
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
-	_, err = conn.Read(buf)
-	if err == nil {
-		t.Fatal("Expected error")
-	}
-
-	// Status should be failure
-	if client.StatusOK() {
-		t.Fatal("Wrong status")
-	}
-
-	// Do a lookup in a separate routine
-	addrs := []string{}
-	wg := sync.NewWaitGroup()
-	wg.Add(1)
-	go func() {
-		pkt, err := client.Lookup(device)
-		if err == nil {
-			for _, addr := range pkt.This.Addresses {
-				addrs = append(addrs, addr)
-			}
-		}
-		wg.Done()
-	}()
-
-	// Receive the lookup and don't reply
-	conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
-	_, _, err = conn.ReadFromUDP(buf)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Wait for the lookup to timeout, verify that the number of answers is none
-	wg.Wait()
-
-	if len(addrs) != 0 {
-		t.Fatal("Wrong number of answers")
-	}
-
-	client.Stop()
-}

+ 0 - 261
lib/discover/client_udp.go

@@ -1,261 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at http://mozilla.org/MPL/2.0/.
-
-package discover
-
-import (
-	"encoding/hex"
-	"io"
-	"net"
-	"net/url"
-	"strconv"
-	"time"
-
-	"github.com/syncthing/protocol"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/sync"
-)
-
-func init() {
-	for _, proto := range []string{"udp", "udp4", "udp6"} {
-		Register(proto, func(uri *url.URL, announcer Announcer) (Client, error) {
-			c := &UDPClient{
-				announcer: announcer,
-				wg:        sync.NewWaitGroup(),
-				mut:       sync.NewRWMutex(),
-			}
-			err := c.Start(uri)
-			if err != nil {
-				return nil, err
-			}
-			return c, nil
-		})
-	}
-}
-
-type UDPClient struct {
-	url *url.URL
-
-	stop          chan struct{}
-	wg            sync.WaitGroup
-	listenAddress *net.UDPAddr
-
-	globalBroadcastInterval time.Duration
-	errorRetryInterval      time.Duration
-	announcer               Announcer
-
-	status bool
-	mut    sync.RWMutex
-}
-
-func (d *UDPClient) Start(uri *url.URL) error {
-	d.url = uri
-	d.stop = make(chan struct{})
-
-	params := uri.Query()
-	// The address must not have a port, as otherwise both announce and lookup
-	// sockets would try to bind to the same port.
-	addr, err := net.ResolveUDPAddr(d.url.Scheme, params.Get("listenaddress")+":0")
-	if err != nil {
-		return err
-	}
-	d.listenAddress = addr
-
-	broadcastSeconds, err := strconv.ParseUint(params.Get("broadcast"), 0, 0)
-	if err != nil {
-		d.globalBroadcastInterval = DefaultGlobalBroadcastInterval
-	} else {
-		d.globalBroadcastInterval = time.Duration(broadcastSeconds) * time.Second
-	}
-
-	retrySeconds, err := strconv.ParseUint(params.Get("retry"), 0, 0)
-	if err != nil {
-		d.errorRetryInterval = DefaultErrorRetryInternval
-	} else {
-		d.errorRetryInterval = time.Duration(retrySeconds) * time.Second
-	}
-
-	d.wg.Add(1)
-	go d.broadcast()
-	return nil
-}
-
-func (d *UDPClient) broadcast() {
-	defer d.wg.Done()
-
-	conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress)
-	for err != nil {
-		if debug {
-			l.Debugf("discover %s: broadcast listen: %v; trying again in %v", d.url, err, d.errorRetryInterval)
-		}
-		select {
-		case <-d.stop:
-			return
-		case <-time.After(d.errorRetryInterval):
-		}
-		conn, err = net.ListenUDP(d.url.Scheme, d.listenAddress)
-	}
-	defer conn.Close()
-
-	remote, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
-	for err != nil {
-		if debug {
-			l.Debugf("discover %s: broadcast resolve: %v; trying again in %v", d.url, err, d.errorRetryInterval)
-		}
-		select {
-		case <-d.stop:
-			return
-		case <-time.After(d.errorRetryInterval):
-		}
-		remote, err = net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
-	}
-
-	timer := time.NewTimer(0)
-
-	eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged)
-	defer events.Default.Unsubscribe(eventSub)
-
-	for {
-		select {
-		case <-d.stop:
-			return
-
-		case <-eventSub.C():
-			ok := d.sendAnnouncement(remote, conn)
-
-			d.mut.Lock()
-			d.status = ok
-			d.mut.Unlock()
-
-		case <-timer.C:
-			ok := d.sendAnnouncement(remote, conn)
-
-			d.mut.Lock()
-			d.status = ok
-			d.mut.Unlock()
-
-			if ok {
-				timer.Reset(d.globalBroadcastInterval)
-			} else {
-				timer.Reset(d.errorRetryInterval)
-			}
-		}
-	}
-}
-
-func (d *UDPClient) sendAnnouncement(remote net.Addr, conn *net.UDPConn) bool {
-	if debug {
-		l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote)
-	}
-
-	ann := d.announcer.Announcement()
-	pkt, err := ann.MarshalXDR()
-	if err != nil {
-		return false
-	}
-
-	myID := protocol.DeviceIDFromBytes(ann.This.ID)
-
-	_, err = conn.WriteTo(pkt, remote)
-	if err != nil {
-		if debug {
-			l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err)
-		}
-		return false
-	}
-
-	// Verify that the announce server responds positively for our device ID
-
-	time.Sleep(1 * time.Second)
-
-	ann, err = d.Lookup(myID)
-	if err != nil && debug {
-		l.Debugf("discover %s: broadcast: Self-lookup failed: %v", d.url, err)
-	} else if debug {
-		l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, ann.This.Addresses)
-	}
-	return len(ann.This.Addresses) > 0
-}
-
-func (d *UDPClient) Lookup(device protocol.DeviceID) (Announce, error) {
-	extIP, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
-	if err != nil {
-		if debug {
-			l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
-		}
-		return Announce{}, err
-	}
-
-	conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP)
-	if err != nil {
-		if debug {
-			l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
-		}
-		return Announce{}, err
-	}
-	defer conn.Close()
-
-	err = conn.SetDeadline(time.Now().Add(5 * time.Second))
-	if err != nil {
-		if debug {
-			l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
-		}
-		return Announce{}, err
-	}
-
-	buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
-	_, err = conn.Write(buf)
-	if err != nil {
-		if debug {
-			l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
-		}
-		return Announce{}, err
-	}
-
-	buf = make([]byte, 2048)
-	n, err := conn.Read(buf)
-	if err != nil {
-		if err, ok := err.(net.Error); ok && err.Timeout() {
-			// Expected if the server doesn't know about requested device ID
-			return Announce{}, err
-		}
-		if debug {
-			l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
-		}
-		return Announce{}, err
-	}
-
-	var pkt Announce
-	err = pkt.UnmarshalXDR(buf[:n])
-	if err != nil && err != io.EOF {
-		if debug {
-			l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n]))
-		}
-		return Announce{}, err
-	}
-
-	if debug {
-		l.Debugf("discover %s: Lookup(%s) result: %v relays: %v", d.url, device, pkt.This.Addresses, pkt.This.Relays)
-	}
-	return pkt, nil
-}
-
-func (d *UDPClient) Stop() {
-	if d.stop != nil {
-		close(d.stop)
-		d.wg.Wait()
-	}
-}
-
-func (d *UDPClient) StatusOK() bool {
-	d.mut.RLock()
-	defer d.mut.RUnlock()
-	return d.status
-}
-
-func (d *UDPClient) Address() string {
-	return d.url.String()
-}

+ 29 - 520
lib/discover/discover.go

@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Syncthing Authors.
+// Copyright (C) 2015 The Syncthing Authors.
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -7,539 +7,48 @@
 package discover
 package discover
 
 
 import (
 import (
-	"bytes"
-	"encoding/hex"
-	"errors"
-	"fmt"
-	"io"
-	"net"
-	"net/url"
-	"sort"
 	"time"
 	"time"
 
 
 	"github.com/syncthing/protocol"
 	"github.com/syncthing/protocol"
-	"github.com/syncthing/syncthing/lib/beacon"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/osutil"
-	"github.com/syncthing/syncthing/lib/sync"
+	"github.com/thejerf/suture"
 )
 )
 
 
-type Discoverer struct {
-	myID                protocol.DeviceID
-	listenAddrs         []string
-	relayStatusProvider relayStatusProvider
-	localBcastIntv      time.Duration
-	localBcastStart     time.Time
-	cacheLifetime       time.Duration
-	negCacheCutoff      time.Duration
-	beacons             []beacon.Interface
-	extAddr             externalAddr
-	localBcastTick      <-chan time.Time
-	forcedBcastTick     chan time.Time
-
-	registryLock    sync.RWMutex
-	addressRegistry map[protocol.DeviceID][]CacheEntry
-	relayRegistry   map[protocol.DeviceID][]CacheEntry
-	lastLookup      map[protocol.DeviceID]time.Time
-
-	clients []Client
-	mut     sync.RWMutex
-}
-
-type relayStatusProvider interface {
-	ClientStatus() map[string]bool
-}
-
-type externalAddr interface {
-	ExternalAddresses() []string
+// A Finder provides lookup services of some kind.
+type Finder interface {
+	Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error)
+	Error() error
+	String() string
+	Cache() map[protocol.DeviceID]CacheEntry
 }
 }
 
 
 type CacheEntry struct {
 type CacheEntry struct {
-	Address string
-	Seen    time.Time
-}
-
-var (
-	ErrIncorrectMagic = errors.New("incorrect magic number")
-)
-
-func NewDiscoverer(id protocol.DeviceID, addresses []string, relayStatusProvider relayStatusProvider) *Discoverer {
-	return &Discoverer{
-		myID:                id,
-		listenAddrs:         addresses,
-		relayStatusProvider: relayStatusProvider,
-		localBcastIntv:      30 * time.Second,
-		cacheLifetime:       5 * time.Minute,
-		negCacheCutoff:      3 * time.Minute,
-		addressRegistry:     make(map[protocol.DeviceID][]CacheEntry),
-		relayRegistry:       make(map[protocol.DeviceID][]CacheEntry),
-		lastLookup:          make(map[protocol.DeviceID]time.Time),
-		registryLock:        sync.NewRWMutex(),
-		mut:                 sync.NewRWMutex(),
-	}
-}
-
-func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
-	if localPort > 0 {
-		d.startLocalIPv4Broadcasts(localPort)
-	}
-
-	if len(localMCAddr) > 0 {
-		d.startLocalIPv6Multicasts(localMCAddr)
-	}
-
-	if len(d.beacons) == 0 {
-		l.Warnln("Local discovery unavailable")
-		return
-	}
-
-	d.localBcastTick = time.Tick(d.localBcastIntv)
-	d.forcedBcastTick = make(chan time.Time)
-	d.localBcastStart = time.Now()
-	go d.sendLocalAnnouncements()
-}
-
-func (d *Discoverer) startLocalIPv4Broadcasts(localPort int) {
-	bb := beacon.NewBroadcast(localPort)
-	d.beacons = append(d.beacons, bb)
-	go d.recvAnnouncements(bb)
-	bb.ServeBackground()
-}
-
-func (d *Discoverer) startLocalIPv6Multicasts(localMCAddr string) {
-	mb, err := beacon.NewMulticast(localMCAddr)
-	if err != nil {
-		if debug {
-			l.Debugln("beacon.NewMulticast:", err)
-		}
-		l.Infoln("Local discovery over IPv6 unavailable")
-		return
-	}
-	d.beacons = append(d.beacons, mb)
-	go d.recvAnnouncements(mb)
-}
-
-func (d *Discoverer) StartGlobal(servers []string, extAddr externalAddr) {
-	d.mut.Lock()
-	defer d.mut.Unlock()
-
-	if len(d.clients) > 0 {
-		d.stopGlobal()
-	}
-
-	d.extAddr = extAddr
-	wg := sync.NewWaitGroup()
-	clients := make(chan Client, len(servers))
-	for _, address := range servers {
-		wg.Add(1)
-		go func(addr string) {
-			defer wg.Done()
-			client, err := New(addr, d)
-			if err != nil {
-				l.Infoln("Error creating discovery client", addr, err)
-				return
-			}
-			clients <- client
-		}(address)
-	}
-
-	wg.Wait()
-	close(clients)
-
-	for client := range clients {
-		d.clients = append(d.clients, client)
-	}
-}
-
-func (d *Discoverer) StopGlobal() {
-	d.mut.Lock()
-	defer d.mut.Unlock()
-	d.stopGlobal()
-}
-
-func (d *Discoverer) stopGlobal() {
-	for _, client := range d.clients {
-		client.Stop()
-	}
-	d.clients = []Client{}
+	Direct []string  `json:"direct"`
+	Relays []Relay   `json:"relays"`
+	when   time.Time // When did we get the result
+	found  bool      // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
 }
 }
 
 
-func (d *Discoverer) ExtAnnounceOK() map[string]bool {
-	d.mut.RLock()
-	defer d.mut.RUnlock()
-
-	ret := make(map[string]bool)
-	for _, client := range d.clients {
-		ret[client.Address()] = client.StatusOK()
-	}
-	return ret
-}
-
-// Lookup returns a list of addresses the device is available at, as well as
-// a list of relays the device is supposed to be available on sorted by the
-// sum of latencies between this device, and the device in question.
-func (d *Discoverer) Lookup(device protocol.DeviceID) ([]string, []string) {
-	d.registryLock.RLock()
-	cachedAddresses := d.filterCached(d.addressRegistry[device])
-	cachedRelays := d.filterCached(d.relayRegistry[device])
-	lastLookup := d.lastLookup[device]
-	d.registryLock.RUnlock()
-
-	d.mut.RLock()
-	defer d.mut.RUnlock()
-
-	relays := make([]string, len(cachedRelays))
-	for i := range cachedRelays {
-		relays[i] = cachedRelays[i].Address
-	}
-
-	if len(cachedAddresses) > 0 {
-		// There are cached address entries.
-		addrs := make([]string, len(cachedAddresses))
-		for i := range cachedAddresses {
-			addrs[i] = cachedAddresses[i].Address
-		}
-		return addrs, relays
-	}
-
-	if time.Since(lastLookup) < d.negCacheCutoff {
-		// We have recently tried to lookup this address and failed. Lets
-		// chill for a while.
-		return nil, relays
-	}
-
-	if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
-		// Only perform external lookups if we have at least one external
-		// server client and one local announcement interval has passed. This is
-		// to avoid finding local peers on their remote address at startup.
-		results := make(chan Announce, len(d.clients))
-		wg := sync.NewWaitGroup()
-		for _, client := range d.clients {
-			wg.Add(1)
-			go func(c Client) {
-				defer wg.Done()
-				ann, err := c.Lookup(device)
-				if err == nil {
-					results <- ann
-				}
-
-			}(client)
-		}
-
-		wg.Wait()
-		close(results)
-
-		cachedAddresses := []CacheEntry{}
-		availableRelays := []Relay{}
-		seenAddresses := make(map[string]struct{})
-		seenRelays := make(map[string]struct{})
-		now := time.Now()
-
-		var addrs []string
-		for result := range results {
-			for _, addr := range result.This.Addresses {
-				_, ok := seenAddresses[addr]
-				if !ok {
-					cachedAddresses = append(cachedAddresses, CacheEntry{
-						Address: addr,
-						Seen:    now,
-					})
-					seenAddresses[addr] = struct{}{}
-					addrs = append(addrs, addr)
-				}
-			}
-
-			for _, relay := range result.This.Relays {
-				_, ok := seenRelays[relay.Address]
-				if !ok {
-					availableRelays = append(availableRelays, relay)
-					seenRelays[relay.Address] = struct{}{}
-				}
-			}
-		}
-
-		relays = RelayAddressesSortedByLatency(availableRelays)
-		cachedRelays := make([]CacheEntry, len(relays))
-		for i := range relays {
-			cachedRelays[i] = CacheEntry{
-				Address: relays[i],
-				Seen:    now,
-			}
-		}
-
-		d.registryLock.Lock()
-		d.addressRegistry[device] = cachedAddresses
-		d.relayRegistry[device] = cachedRelays
-		d.lastLookup[device] = time.Now()
-		d.registryLock.Unlock()
-
-		return addrs, relays
-	}
-
-	return nil, relays
+// A FinderService is a Finder that has background activity and must be run as
+// a suture.Service.
+type FinderService interface {
+	Finder
+	suture.Service
 }
 }
 
 
-func (d *Discoverer) Hint(device string, addrs []string) {
-	resAddrs := resolveAddrs(addrs)
-	var id protocol.DeviceID
-	id.UnmarshalText([]byte(device))
-	d.registerDevice(nil, Device{
-		Addresses: resAddrs,
-		ID:        id[:],
-	})
+type FinderMux interface {
+	Finder
+	ChildStatus() map[string]error
 }
 }
 
 
-func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
-	d.registryLock.RLock()
-	devices := make(map[protocol.DeviceID][]CacheEntry, len(d.addressRegistry))
-	for device, addrs := range d.addressRegistry {
-		addrsCopy := make([]CacheEntry, len(addrs))
-		copy(addrsCopy, addrs)
-		devices[device] = addrsCopy
-	}
-	d.registryLock.RUnlock()
-	return devices
+// The RelayStatusProvider answers questions about current relay status.
+type RelayStatusProvider interface {
+	Relays() []string
+	RelayStatus(uri string) (time.Duration, bool)
 }
 }
 
 
-func (d *Discoverer) Announcement() Announce {
-	return d.announcementPkt(true)
-}
-
-func (d *Discoverer) announcementPkt(allowExternal bool) Announce {
-	var addrs []string
-	if allowExternal && d.extAddr != nil {
-		addrs = d.extAddr.ExternalAddresses()
-	} else {
-		addrs = resolveAddrs(d.listenAddrs)
-	}
-
-	var relayAddrs []string
-	if d.relayStatusProvider != nil {
-		status := d.relayStatusProvider.ClientStatus()
-		for uri, ok := range status {
-			if ok {
-				relayAddrs = append(relayAddrs, uri)
-			}
-		}
-	}
-
-	return Announce{
-		Magic: AnnouncementMagic,
-		This:  Device{d.myID[:], addrs, measureLatency(relayAddrs)},
-	}
-}
-
-func (d *Discoverer) sendLocalAnnouncements() {
-	var pkt = d.announcementPkt(false)
-	msg := pkt.MustMarshalXDR()
-
-	for {
-		for _, b := range d.beacons {
-			b.Send(msg)
-		}
-
-		select {
-		case <-d.localBcastTick:
-		case <-d.forcedBcastTick:
-		}
-	}
-}
-
-func (d *Discoverer) recvAnnouncements(b beacon.Interface) {
-	for {
-		buf, addr := b.Recv()
-
-		var pkt Announce
-		err := pkt.UnmarshalXDR(buf)
-		if err != nil && err != io.EOF {
-			if debug {
-				l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
-			}
-			continue
-		}
-
-		if debug {
-			l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
-		}
-
-		var newDevice bool
-		if bytes.Compare(pkt.This.ID, d.myID[:]) != 0 {
-			newDevice = d.registerDevice(addr, pkt.This)
-		}
-
-		if newDevice {
-			select {
-			case d.forcedBcastTick <- time.Now():
-			}
-		}
-	}
-}
-
-func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
-	var id protocol.DeviceID
-	copy(id[:], device.ID)
-
-	d.registryLock.Lock()
-	defer d.registryLock.Unlock()
-
-	current := d.filterCached(d.addressRegistry[id])
-
-	orig := current
-
-	for _, deviceAddr := range device.Addresses {
-		uri, err := url.Parse(deviceAddr)
-		if err != nil {
-			if debug {
-				l.Debugf("discover: Failed to parse address %s: %s", deviceAddr, err)
-			}
-			continue
-		}
-
-		host, port, err := net.SplitHostPort(uri.Host)
-		if err != nil {
-			if debug {
-				l.Debugf("discover: Failed to split address host %s: %s", deviceAddr, err)
-			}
-			continue
-		}
-
-		if host == "" {
-			uri.Host = net.JoinHostPort(addr.(*net.UDPAddr).IP.String(), port)
-			deviceAddr = uri.String()
-		}
-
-		for i := range current {
-			if current[i].Address == deviceAddr {
-				current[i].Seen = time.Now()
-				goto done
-			}
-		}
-		current = append(current, CacheEntry{
-			Address: deviceAddr,
-			Seen:    time.Now(),
-		})
-	done:
-	}
-
-	if debug {
-		l.Debugf("discover: Caching %s addresses: %v", id, current)
-	}
-
-	d.addressRegistry[id] = current
-
-	if len(current) > len(orig) {
-		addrs := make([]string, len(current))
-		for i := range current {
-			addrs[i] = current[i].Address
-		}
-		events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
-			"device": id.String(),
-			"addrs":  addrs,
-		})
-	}
-
-	return len(current) > len(orig)
-}
-
-func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
-	for i := 0; i < len(c); {
-		if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
-			if debug {
-				l.Debugf("discover: Removing cached entry %s - seen %v ago", c[i].Address, ago)
-			}
-			c[i] = c[len(c)-1]
-			c = c[:len(c)-1]
-		} else {
-			i++
-		}
-	}
-	return c
-}
-
-func addrToAddr(addr *net.TCPAddr) string {
-	if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
-		return fmt.Sprintf(":%d", addr.Port)
-	} else if bs := addr.IP.To4(); bs != nil {
-		return fmt.Sprintf("%s:%d", bs.String(), addr.Port)
-	} else if bs := addr.IP.To16(); bs != nil {
-		return fmt.Sprintf("[%s]:%d", bs.String(), addr.Port)
-	}
-	return ""
-}
-
-func resolveAddrs(addrs []string) []string {
-	var raddrs []string
-	for _, addrStr := range addrs {
-		uri, err := url.Parse(addrStr)
-		if err != nil {
-			continue
-		}
-		addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
-		if err != nil {
-			continue
-		}
-		addr := addrToAddr(addrRes)
-		if len(addr) > 0 {
-			uri.Host = addr
-			raddrs = append(raddrs, uri.String())
-		}
-	}
-	return raddrs
-}
-
-func measureLatency(relayAdresses []string) []Relay {
-	relays := make([]Relay, 0, len(relayAdresses))
-	for i, addr := range relayAdresses {
-		relay := Relay{
-			Address: addr,
-			Latency: int32(time.Hour / time.Millisecond),
-		}
-		relays = append(relays, relay)
-
-		if latency, err := osutil.GetLatencyForURL(addr); err == nil {
-			if debug {
-				l.Debugf("Relay %s latency %s", addr, latency)
-			}
-			relays[i].Latency = int32(latency / time.Millisecond)
-		} else {
-			l.Debugf("Failed to get relay %s latency %s", addr, err)
-		}
-	}
-	return relays
-}
-
-// RelayAddressesSortedByLatency adds local latency to the relay, and sorts them
-// by sum latency, and returns the addresses.
-func RelayAddressesSortedByLatency(input []Relay) []string {
-	relays := make([]Relay, len(input))
-	copy(relays, input)
-	for i, relay := range relays {
-		if latency, err := osutil.GetLatencyForURL(relay.Address); err == nil {
-			relays[i].Latency += int32(latency / time.Millisecond)
-		} else {
-			relays[i].Latency += int32(time.Hour / time.Millisecond)
-		}
-	}
-
-	sort.Sort(relayList(relays))
-
-	addresses := make([]string, 0, len(relays))
-	for _, relay := range relays {
-		addresses = append(addresses, relay.Address)
-	}
-	return addresses
-}
-
-type relayList []Relay
-
-func (l relayList) Len() int {
-	return len(l)
-}
-
-func (l relayList) Less(a, b int) bool {
-	return l[a].Latency < l[b].Latency
-}
-
-func (l relayList) Swap(a, b int) {
-	l[a], l[b] = l[b], l[a]
+// The AddressLister answers questions about what addresses we are listening
+// on.
+type AddressLister interface {
+	ExternalAddresses() []string
+	AllAddresses() []string
 }
 }

+ 0 - 163
lib/discover/discover_test.go

@@ -1,163 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at http://mozilla.org/MPL/2.0/.
-
-package discover
-
-import (
-	"net/url"
-	"time"
-
-	"testing"
-
-	"github.com/syncthing/protocol"
-)
-
-type DummyClient struct {
-	url          *url.URL
-	lookups      []protocol.DeviceID
-	lookupRet    Announce
-	stops        int
-	statusRet    bool
-	statusChecks int
-}
-
-func (c *DummyClient) Lookup(device protocol.DeviceID) (Announce, error) {
-	c.lookups = append(c.lookups, device)
-	return c.lookupRet, nil
-}
-
-func (c *DummyClient) StatusOK() bool {
-	c.statusChecks++
-	return c.statusRet
-}
-
-func (c *DummyClient) Stop() {
-	c.stops++
-}
-
-func (c *DummyClient) Address() string {
-	return c.url.String()
-}
-
-func TestGlobalDiscovery(t *testing.T) {
-	c1 := &DummyClient{
-		statusRet: false,
-		lookupRet: Announce{
-			Magic: AnnouncementMagic,
-			This: Device{
-				ID:        protocol.LocalDeviceID[:],
-				Addresses: []string{"test.com:1234"},
-				Relays:    nil,
-			},
-			Extra: nil,
-		},
-	}
-
-	c2 := &DummyClient{
-		statusRet: true,
-		lookupRet: Announce{
-			Magic: AnnouncementMagic,
-			This: Device{
-				ID:        protocol.LocalDeviceID[:],
-				Addresses: nil,
-				Relays:    nil,
-			},
-			Extra: nil,
-		},
-	}
-
-	c3 := &DummyClient{
-		statusRet: true,
-		lookupRet: Announce{
-			Magic: AnnouncementMagic,
-			This: Device{
-				ID:        protocol.LocalDeviceID[:],
-				Addresses: []string{"best.com:2345"},
-				Relays:    nil,
-			},
-			Extra: nil,
-		},
-	}
-
-	clients := []*DummyClient{c1, c2}
-
-	Register("test1", func(uri *url.URL, ann Announcer) (Client, error) {
-		c := clients[0]
-		clients = clients[1:]
-		c.url = uri
-		return c, nil
-	})
-
-	Register("test2", func(uri *url.URL, ann Announcer) (Client, error) {
-		c3.url = uri
-		return c3, nil
-	})
-
-	d := NewDiscoverer(device, []string{}, nil)
-	d.localBcastStart = time.Time{}
-	servers := []string{
-		"test1://123.123.123.123:1234",
-		"test1://23.23.23.23:234",
-		"test2://234.234.234.234.2345",
-	}
-	d.StartGlobal(servers, nil)
-
-	if len(d.clients) != 3 {
-		t.Fatal("Wrong number of clients")
-	}
-
-	status := d.ExtAnnounceOK()
-
-	for _, c := range []*DummyClient{c1, c2, c3} {
-		if status[c.url.String()] != c.statusRet || c.statusChecks != 1 {
-			t.Fatal("Wrong status")
-		}
-	}
-
-	addrs, _ := d.Lookup(device)
-	if len(addrs) != 2 {
-		t.Fatal("Wrong number of addresses", addrs)
-	}
-
-	for _, addr := range []string{"test.com:1234", "best.com:2345"} {
-		found := false
-		for _, laddr := range addrs {
-			if laddr == addr {
-				found = true
-				break
-			}
-		}
-		if !found {
-			t.Fatal("Couldn't find", addr)
-		}
-	}
-
-	for _, c := range []*DummyClient{c1, c2, c3} {
-		if len(c.lookups) != 1 || c.lookups[0] != device {
-			t.Fatal("Wrong lookups")
-		}
-	}
-
-	addrs, _ = d.Lookup(device)
-	if len(addrs) != 2 {
-		t.Fatal("Wrong number of addresses", addrs)
-	}
-
-	// Answer should be cached, so number of lookups should have not increased
-	for _, c := range []*DummyClient{c1, c2, c3} {
-		if len(c.lookups) != 1 || c.lookups[0] != device {
-			t.Fatal("Wrong lookups")
-		}
-	}
-
-	d.StopGlobal()
-
-	for _, c := range []*DummyClient{c1, c2, c3} {
-		if c.stops != 1 {
-			t.Fatal("Wrong number of stops")
-		}
-	}
-}

+ 69 - 2
lib/discover/doc.go

@@ -1,8 +1,75 @@
-// Copyright (C) 2014 The Syncthing Authors.
+// Copyright (C) 2015 The Syncthing Authors.
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at http://mozilla.org/MPL/2.0/.
 // You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
-// Package discover implements the device discovery protocol.
+/*
+Package discover implements the local and global device discovery protocols.
+
+Global Discovery
+================
+
+Announcements
+-------------
+
+A device should announce itself at startup. It does this by an HTTPS POST to
+the announce server URL (with the path usually being "/", but this is of
+course up to the discovery server). The POST has a JSON payload listing direct
+connection addresses (if any) and relay addresses (if any).
+
+	{
+		direct: ["tcp://192.0.2.45:22000", "tcp://:22202"],
+		relays: [{"url": "relay://192.0.2.99:22028", "latency": 142}]
+	}
+
+It's OK for either of the "direct" or "relays" fields to be either the empty
+list ([]), null, or missing entirely. An announcment with both fields missing
+or empty is however not useful...
+
+Any empty or unspecified IP addresses (i.e. addresses like tcp://:22000,
+tcp://0.0.0.0:22000, tcp://[::]:22000) are interpreted as referring to the
+source IP address of the announcement.
+
+The device ID of the announcing device is not part of the announcement.
+Instead, the server requires that the client perform certificate
+authentication. The device ID is deduced from the presented certificate.
+
+The server response is empty, with code 200 (OK) on success. If no certificate
+was presented, status 403 (Forbidden) is returned. If the posted data doesn't
+conform to the expected format, 400 (Bad Request) is returned.
+
+In successfull responses, the server may return a "Reannounce-After" header
+containing the number of seconds after which the client should perform a new
+announcement.
+
+In error responses, the server may return a "Retry-After" header containing
+the number of seconds after which the client should retry.
+
+Performing announcements significantly more often than indicated by the
+Reannounce-After or Retry-After headers may result in the client being
+throttled. In such cases the server may respond with status code 429 (Too Many
+Requests).
+
+Queries
+=======
+
+Queries are performed as HTTPS GET requests to the announce server URL. The
+requested device ID is passed as the query parameter "device", in canonical
+string form, i.e. https://announce.syncthing.net/?device=ABC12345-....
+
+Successfull responses will have status code 200 (OK) and carry a JSON payload
+of the same format as the announcement above. The response will not contain
+empty or unspecified addresses.
+
+If the "device" query parameter is missing or malformed, the status code 400
+(Bad Request) is returned.
+
+If the device ID is of a valid format but not found in the registry, 404 (Not
+Found) is returned.
+
+If the client has exceeded a rate limit, the server may respond with 429 (Too
+Many Requests).
+
+*/
 package discover
 package discover

+ 385 - 0
lib/discover/global.go

@@ -0,0 +1,385 @@
+package discover
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	stdsync "sync"
+	"time"
+
+	"github.com/syncthing/protocol"
+	"github.com/syncthing/syncthing/lib/events"
+)
+
+type globalClient struct {
+	server         string
+	addrList       AddressLister
+	relayStat      RelayStatusProvider
+	announceClient httpClient
+	queryClient    httpClient
+	noAnnounce     bool
+	stop           chan struct{}
+	errorHolder
+}
+
+type httpClient interface {
+	Get(url string) (*http.Response, error)
+	Post(url, ctype string, data io.Reader) (*http.Response, error)
+}
+
+const (
+	defaultReannounceInterval  = 30 * time.Minute
+	announceErrorRetryInterval = 5 * time.Minute
+)
+
+type announcement struct {
+	Direct []string `json:"direct"`
+	Relays []Relay  `json:"relays"`
+}
+
+type serverOptions struct {
+	insecure   bool   // don't check certificate
+	noAnnounce bool   // don't announce
+	id         string // expected server device ID
+}
+
+func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
+	server, opts, err := parseOptions(server)
+	if err != nil {
+		return nil, err
+	}
+
+	var devID protocol.DeviceID
+	if opts.id != "" {
+		devID, err = protocol.DeviceIDFromString(opts.id)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// The http.Client used for announcements. It needs to have our
+	// certificate to prove our identity, and may or may not verify the server
+	// certificate depending on the insecure setting.
+	var announceClient httpClient = &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: opts.insecure,
+				Certificates:       []tls.Certificate{cert},
+			},
+		},
+	}
+	if opts.id != "" {
+		announceClient = newIDCheckingHTTPClient(announceClient, devID)
+	}
+
+	// The http.Client used for queries. We don't need to present our
+	// certificate here, so lets not include it. May be insecure if requested.
+	var queryClient httpClient = &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: opts.insecure,
+			},
+		},
+	}
+	if opts.id != "" {
+		queryClient = newIDCheckingHTTPClient(queryClient, devID)
+	}
+
+	cl := &globalClient{
+		server:         server,
+		addrList:       addrList,
+		relayStat:      relayStat,
+		announceClient: announceClient,
+		queryClient:    queryClient,
+		noAnnounce:     opts.noAnnounce,
+		stop:           make(chan struct{}),
+	}
+	cl.setError(errors.New("not announced"))
+
+	return cl, nil
+}
+
+// Lookup returns the list of addresses where the given device is available;
+// direct, and via relays.
+func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
+	qURL, err := url.Parse(c.server)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	q := qURL.Query()
+	q.Set("device", device.String())
+	qURL.RawQuery = q.Encode()
+
+	resp, err := c.queryClient.Get(qURL.String())
+	if err != nil {
+		if debug {
+			l.Debugln("globalClient.Lookup", qURL.String(), err)
+		}
+		return nil, nil, err
+	}
+	if resp.StatusCode != 200 {
+		resp.Body.Close()
+		if debug {
+			l.Debugln("globalClient.Lookup", qURL.String(), resp.Status)
+		}
+		return nil, nil, errors.New(resp.Status)
+	}
+
+	// TODO: Handle 429 and Retry-After?
+
+	var ann announcement
+	err = json.NewDecoder(resp.Body).Decode(&ann)
+	resp.Body.Close()
+	return ann.Direct, ann.Relays, err
+}
+
+func (c *globalClient) String() string {
+	return "global@" + c.server
+}
+
+func (c *globalClient) Serve() {
+	if c.noAnnounce {
+		// We're configured to not do announcements, only lookups. To maintain
+		// the same interface, we just pause here if Serve() is run.
+		<-c.stop
+		return
+	}
+
+	timer := time.NewTimer(0)
+	defer timer.Stop()
+
+	eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged | events.RelayStateChanged)
+	defer events.Default.Unsubscribe(eventSub)
+
+	for {
+		select {
+		case <-eventSub.C():
+			c.sendAnnouncement(timer)
+
+		case <-timer.C:
+			c.sendAnnouncement(timer)
+
+		case <-c.stop:
+			return
+		}
+	}
+}
+
+func (c *globalClient) sendAnnouncement(timer *time.Timer) {
+
+	var ann announcement
+	if c.addrList != nil {
+		ann.Direct = c.addrList.ExternalAddresses()
+	}
+
+	if c.relayStat != nil {
+		for _, relay := range c.relayStat.Relays() {
+			latency, ok := c.relayStat.RelayStatus(relay)
+			if ok {
+				ann.Relays = append(ann.Relays, Relay{
+					URL:     relay,
+					Latency: int32(latency / time.Millisecond),
+				})
+			}
+		}
+	}
+
+	if len(ann.Direct)+len(ann.Relays) == 0 {
+		c.setError(errors.New("nothing to announce"))
+		if debug {
+			l.Debugln("Nothing to announce")
+		}
+		timer.Reset(announceErrorRetryInterval)
+		return
+	}
+
+	// The marshal doesn't fail, I promise.
+	postData, _ := json.Marshal(ann)
+
+	if debug {
+		l.Debugf("Announcement: %s", postData)
+	}
+
+	resp, err := c.announceClient.Post(c.server, "application/json", bytes.NewReader(postData))
+	if err != nil {
+		if debug {
+			l.Debugln("announce POST:", err)
+		}
+		c.setError(err)
+		timer.Reset(announceErrorRetryInterval)
+		return
+	}
+	if debug {
+		l.Debugln("announce POST:", resp.Status)
+	}
+	resp.Body.Close()
+
+	if resp.StatusCode < 200 || resp.StatusCode > 299 {
+		if debug {
+			l.Debugln("announce POST:", resp.Status)
+		}
+		c.setError(errors.New(resp.Status))
+
+		if h := resp.Header.Get("Retry-After"); h != "" {
+			// The server has a recommendation on when we should
+			// retry. Follow it.
+			if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
+				if debug {
+					l.Debugln("announce Retry-After:", secs, err)
+				}
+				timer.Reset(time.Duration(secs) * time.Second)
+				return
+			}
+		}
+
+		timer.Reset(announceErrorRetryInterval)
+		return
+	}
+
+	c.setError(nil)
+
+	if h := resp.Header.Get("Reannounce-After"); h != "" {
+		// The server has a recommendation on when we should
+		// reannounce. Follow it.
+		if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
+			if debug {
+				l.Debugln("announce Reannounce-After:", secs, err)
+			}
+			timer.Reset(time.Duration(secs) * time.Second)
+			return
+		}
+	}
+
+	timer.Reset(defaultReannounceInterval)
+}
+
+func (c *globalClient) Stop() {
+	close(c.stop)
+}
+
+func (c *globalClient) Cache() map[protocol.DeviceID]CacheEntry {
+	// The globalClient doesn't do caching
+	return nil
+}
+
+// parseOptions parses and strips away any ?query=val options, setting the
+// corresponding field in the serverOptions struct. Unknown query options are
+// ignored and removed.
+func parseOptions(dsn string) (server string, opts serverOptions, err error) {
+	p, err := url.Parse(dsn)
+	if err != nil {
+		return "", serverOptions{}, err
+	}
+
+	// Grab known options from the query string
+	q := p.Query()
+	opts.id = q.Get("id")
+	opts.insecure = opts.id != "" || queryBool(q, "insecure")
+	opts.noAnnounce = queryBool(q, "noannounce")
+
+	// Check for disallowed combinations
+	if p.Scheme == "http" {
+		if !opts.insecure {
+			return "", serverOptions{}, errors.New("http without insecure not supported")
+		}
+		if !opts.noAnnounce {
+			return "", serverOptions{}, errors.New("http without noannounce not supported")
+		}
+	} else if p.Scheme != "https" {
+		return "", serverOptions{}, errors.New("unsupported scheme " + p.Scheme)
+	}
+
+	// Remove the query string
+	p.RawQuery = ""
+	server = p.String()
+
+	return
+}
+
+// queryBool returns the query parameter parsed as a boolean. An empty value
+// ("?foo") is considered true, as is any value string except false
+// ("?foo=false").
+func queryBool(q url.Values, key string) bool {
+	if _, ok := q[key]; !ok {
+		return false
+	}
+
+	return q.Get(key) != "false"
+}
+
+type idCheckingHTTPClient struct {
+	httpClient
+	id protocol.DeviceID
+}
+
+func newIDCheckingHTTPClient(client httpClient, id protocol.DeviceID) *idCheckingHTTPClient {
+	return &idCheckingHTTPClient{
+		httpClient: client,
+		id:         id,
+	}
+}
+
+func (c *idCheckingHTTPClient) check(resp *http.Response) error {
+	if resp.TLS == nil {
+		return errors.New("security: not TLS")
+	}
+
+	if len(resp.TLS.PeerCertificates) == 0 {
+		return errors.New("security: no certificates")
+	}
+
+	id := protocol.NewDeviceID(resp.TLS.PeerCertificates[0].Raw)
+	if !id.Equals(c.id) {
+		return errors.New("security: incorrect device id")
+	}
+
+	return nil
+}
+
+func (c *idCheckingHTTPClient) Get(url string) (*http.Response, error) {
+	resp, err := c.httpClient.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if err := c.check(resp); err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
+
+func (c *idCheckingHTTPClient) Post(url, ctype string, data io.Reader) (*http.Response, error) {
+	resp, err := c.httpClient.Post(url, ctype, data)
+	if err != nil {
+		return nil, err
+	}
+	if err := c.check(resp); err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
+
+type errorHolder struct {
+	err error
+	mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
+}
+
+func (e *errorHolder) setError(err error) {
+	e.mut.Lock()
+	e.err = err
+	e.mut.Unlock()
+}
+
+func (e *errorHolder) Error() error {
+	e.mut.Lock()
+	err := e.err
+	e.mut.Unlock()
+	return err
+}

+ 253 - 0
lib/discover/global_test.go

@@ -0,0 +1,253 @@
+package discover
+
+import (
+	"crypto/tls"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/syncthing/protocol"
+	"github.com/syncthing/syncthing/lib/tlsutil"
+)
+
+func TestParseOptions(t *testing.T) {
+	testcases := []struct {
+		in   string
+		out  string
+		opts serverOptions
+	}{
+		{"https://example.com/", "https://example.com/", serverOptions{}},
+		{"https://example.com/?insecure", "https://example.com/", serverOptions{insecure: true}},
+		{"https://example.com/?insecure=true", "https://example.com/", serverOptions{insecure: true}},
+		{"https://example.com/?insecure=yes", "https://example.com/", serverOptions{insecure: true}},
+		{"https://example.com/?insecure=false&noannounce", "https://example.com/", serverOptions{noAnnounce: true}},
+		{"https://example.com/?id=abc", "https://example.com/", serverOptions{id: "abc", insecure: true}},
+	}
+
+	for _, tc := range testcases {
+		res, opts, err := parseOptions(tc.in)
+		if err != nil {
+			t.Errorf("Unexpected err %v for %v", err, tc.in)
+			continue
+		}
+		if res != tc.out {
+			t.Errorf("Incorrect server, %v!= %v for %v", res, tc.out, tc.in)
+		}
+		if opts != tc.opts {
+			t.Errorf("Incorrect options, %v!= %v for %v", opts, tc.opts, tc.in)
+		}
+	}
+}
+
+func TestGlobalOverHTTP(t *testing.T) {
+	// HTTP works for queries, but is obviously insecure and we can't do
+	// announces over it (as we don't present a certificate). As such, http://
+	// is only allowed in combination with the "insecure" and "noannounce"
+	// parameters.
+
+	if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, nil); err == nil {
+		t.Fatal("http is not allowed without insecure and noannounce")
+	}
+
+	if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil, nil); err == nil {
+		t.Fatal("http is not allowed without noannounce")
+	}
+
+	if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, nil); err == nil {
+		t.Fatal("http is not allowed without insecure")
+	}
+
+	// Now lets check that lookups work over HTTP, given the correct options.
+
+	list, err := net.Listen("tcp4", "127.0.0.1:0")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer list.Close()
+
+	s := new(fakeDiscoveryServer)
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", s.handler)
+	go http.Serve(list, mux)
+
+	direct, relays, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
+		t.Errorf("incorrect direct list: %+v", direct)
+	}
+	if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
+		t.Errorf("incorrect relays list: %+v", direct)
+	}
+}
+
+func TestGlobalOverHTTPS(t *testing.T) {
+	dir, err := ioutil.TempDir("", "syncthing")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Generate a server certificate, using fewer bits than usual to hurry the
+	// process along a bit.
+	cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer list.Close()
+
+	s := new(fakeDiscoveryServer)
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", s.handler)
+	go http.Serve(list, mux)
+
+	// With default options the lookup code expects the server certificate to
+	// check out according to the usual CA chains etc. That won't be the case
+	// here so we expect the lookup to fail.
+
+	url := "https://" + list.Addr().String()
+	if _, _, err := testLookup(url); err == nil {
+		t.Fatalf("unexpected nil error when we should have got a certificate error")
+	}
+
+	// With "insecure" set, whatever certificate is on the other side should
+	// be accepted.
+
+	url = "https://" + list.Addr().String() + "?insecure"
+	if direct, relays, err := testLookup(url); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	} else {
+		if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
+			t.Errorf("incorrect direct list: %+v", direct)
+		}
+		if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
+			t.Errorf("incorrect relays list: %+v", direct)
+		}
+	}
+
+	// With "id" set to something incorrect, the checks should fail again.
+
+	url = "https://" + list.Addr().String() + "?id=" + protocol.LocalDeviceID.String()
+	if _, _, err := testLookup(url); err == nil {
+		t.Fatalf("unexpected nil error for incorrect discovery server ID")
+	}
+
+	// With the correct device ID, the check should pass and we should get a
+	// lookup response.
+
+	id := protocol.NewDeviceID(cert.Certificate[0])
+	url = "https://" + list.Addr().String() + "?id=" + id.String()
+	if direct, relays, err := testLookup(url); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	} else {
+		if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
+			t.Errorf("incorrect direct list: %+v", direct)
+		}
+		if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
+			t.Errorf("incorrect relays list: %+v", direct)
+		}
+	}
+}
+
+func TestGlobalAnnounce(t *testing.T) {
+	dir, err := ioutil.TempDir("", "syncthing")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Generate a server certificate, using fewer bits than usual to hurry the
+	// process along a bit.
+	cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer list.Close()
+
+	s := new(fakeDiscoveryServer)
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", s.handler)
+	go http.Serve(list, mux)
+
+	url := "https://" + list.Addr().String() + "?insecure"
+	disco, err := NewGlobal(url, cert, new(fakeAddressLister), new(fakeRelayStatus))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	go disco.Serve()
+	defer disco.Stop()
+
+	// The discovery thing should attempt an announcement immediately. We wait
+	// for it to succeed, a while.
+	t0 := time.Now()
+	for err := disco.Error(); err != nil; err = disco.Error() {
+		if time.Since(t0) > 10*time.Second {
+			t.Fatal("announce failed:", err)
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+
+	if !strings.Contains(string(s.announce), "tcp://0.0.0.0:22000") {
+		t.Errorf("announce missing direct address: %s", s.announce)
+	}
+	if !strings.Contains(string(s.announce), "relay://192.0.2.42:443") {
+		t.Errorf("announce missing relay address: %s", s.announce)
+	}
+}
+
+func testLookup(url string) ([]string, []Relay, error) {
+	disco, err := NewGlobal(url, tls.Certificate{}, nil, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+	go disco.Serve()
+	defer disco.Stop()
+
+	return disco.Lookup(protocol.LocalDeviceID)
+}
+
+type fakeDiscoveryServer struct {
+	announce []byte
+}
+
+func (s *fakeDiscoveryServer) handler(w http.ResponseWriter, r *http.Request) {
+	if r.Method == "POST" {
+		s.announce, _ = ioutil.ReadAll(r.Body)
+		w.WriteHeader(204)
+	} else {
+		w.Header().Set("Content-Type", "application/json")
+		w.Write([]byte(`{"direct":["tcp://192.0.2.42::22000"], "relays":[{"url": "relay://192.0.2.43:443", "latency": 42}]}`))
+	}
+}
+
+type fakeAddressLister struct{}
+
+func (f *fakeAddressLister) ExternalAddresses() []string {
+	return []string{"tcp://0.0.0.0:22000"}
+}
+func (f *fakeAddressLister) AllAddresses() []string {
+	return []string{"tcp://0.0.0.0:22000", "tcp://192.168.0.1:22000"}
+}
+
+type fakeRelayStatus struct{}
+
+func (f *fakeRelayStatus) Relays() []string {
+	return []string{"relay://192.0.2.42:443"}
+}
+func (f *fakeRelayStatus) RelayStatus(uri string) (time.Duration, bool) {
+	return 42 * time.Millisecond, true
+}

+ 270 - 0
lib/discover/local.go

@@ -0,0 +1,270 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package discover
+
+import (
+	"bytes"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/syncthing/protocol"
+	"github.com/syncthing/syncthing/lib/beacon"
+	"github.com/syncthing/syncthing/lib/events"
+	"github.com/thejerf/suture"
+)
+
+type localClient struct {
+	*suture.Supervisor
+	myID      protocol.DeviceID
+	addrList  AddressLister
+	relayStat RelayStatusProvider
+	name      string
+
+	beacon          beacon.Interface
+	localBcastStart time.Time
+	localBcastTick  <-chan time.Time
+	forcedBcastTick chan time.Time
+
+	*cache
+}
+
+const (
+	BroadcastInterval = 30 * time.Second
+	CacheLifeTime     = 3 * BroadcastInterval
+)
+
+var (
+	ErrIncorrectMagic = errors.New("incorrect magic number")
+)
+
+func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
+	c := &localClient{
+		Supervisor:      suture.NewSimple("local"),
+		myID:            id,
+		addrList:        addrList,
+		relayStat:       relayStat,
+		localBcastTick:  time.Tick(BroadcastInterval),
+		forcedBcastTick: make(chan time.Time),
+		localBcastStart: time.Now(),
+		cache:           newCache(),
+	}
+
+	host, port, err := net.SplitHostPort(addr)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(host) == 0 {
+		// A broadcast client
+		c.name = "IPv4 local"
+		bcPort, err := strconv.Atoi(port)
+		if err != nil {
+			return nil, err
+		}
+		c.startLocalIPv4Broadcasts(bcPort)
+	} else {
+		// A multicast client
+		c.name = "IPv6 local"
+		c.startLocalIPv6Multicasts(addr)
+	}
+
+	go c.sendLocalAnnouncements()
+
+	return c, nil
+}
+
+func (c *localClient) startLocalIPv4Broadcasts(localPort int) {
+	c.beacon = beacon.NewBroadcast(localPort)
+	c.Add(c.beacon)
+	go c.recvAnnouncements(c.beacon)
+}
+
+func (c *localClient) startLocalIPv6Multicasts(localMCAddr string) {
+	c.beacon = beacon.NewMulticast(localMCAddr)
+	c.Add(c.beacon)
+	go c.recvAnnouncements(c.beacon)
+}
+
+// Lookup returns a list of addresses the device is available at. Local
+// discovery never returns relays.
+func (c *localClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
+	if cache, ok := c.Get(device); ok {
+		if time.Since(cache.when) < CacheLifeTime {
+			direct = cache.Direct
+			relays = cache.Relays
+		}
+	}
+
+	return
+}
+
+func (c *localClient) String() string {
+	return c.name
+}
+
+func (c *localClient) Error() error {
+	return c.beacon.Error()
+}
+
+func (c *localClient) announcementPkt() Announce {
+	addrs := c.addrList.AllAddresses()
+
+	var relays []Relay
+	for _, relay := range c.relayStat.Relays() {
+		latency, ok := c.relayStat.RelayStatus(relay)
+		if ok {
+			relays = append(relays, Relay{
+				URL:     relay,
+				Latency: int32(latency / time.Millisecond),
+			})
+		}
+	}
+
+	return Announce{
+		Magic: AnnouncementMagic,
+		This: Device{
+			ID:        c.myID[:],
+			Addresses: addrs,
+			Relays:    relays,
+		},
+	}
+}
+
+func (c *localClient) sendLocalAnnouncements() {
+	var pkt = c.announcementPkt()
+	msg := pkt.MustMarshalXDR()
+
+	for {
+		c.beacon.Send(msg)
+
+		select {
+		case <-c.localBcastTick:
+		case <-c.forcedBcastTick:
+		}
+	}
+}
+
+func (c *localClient) recvAnnouncements(b beacon.Interface) {
+	for {
+		buf, addr := b.Recv()
+
+		var pkt Announce
+		err := pkt.UnmarshalXDR(buf)
+		if err != nil && err != io.EOF {
+			if debug {
+				l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
+			}
+			continue
+		}
+
+		if debug {
+			l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
+		}
+
+		var newDevice bool
+		if bytes.Compare(pkt.This.ID, c.myID[:]) != 0 {
+			newDevice = c.registerDevice(addr, pkt.This)
+		}
+
+		if newDevice {
+			select {
+			case c.forcedBcastTick <- time.Now():
+			}
+		}
+	}
+}
+
+func (c *localClient) registerDevice(src net.Addr, device Device) bool {
+	var id protocol.DeviceID
+	copy(id[:], device.ID)
+
+	// Remember whether we already had a valid cache entry for this device.
+
+	ce, existsAlready := c.Get(id)
+	isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime
+
+	// Any empty or unspecified addresses should be set to the source address
+	// of the announcement. We also skip any addresses we can't parse.
+
+	var validAddresses []string
+	for _, addr := range device.Addresses {
+		u, err := url.Parse(addr)
+		if err != nil {
+			continue
+		}
+
+		tcpAddr, err := net.ResolveTCPAddr("tcp", u.Host)
+		if err != nil {
+			continue
+		}
+
+		if len(tcpAddr.IP) == 0 || tcpAddr.IP.IsUnspecified() {
+			host, _, err := net.SplitHostPort(src.String())
+			if err != nil {
+				continue
+			}
+			u.Host = fmt.Sprintf("%s:%d", host, tcpAddr.Port)
+			validAddresses = append(validAddresses, u.String())
+		} else {
+			validAddresses = append(validAddresses, addr)
+		}
+	}
+
+	c.Set(id, CacheEntry{
+		Direct: validAddresses,
+		Relays: device.Relays,
+		when:   time.Now(),
+		found:  true,
+	})
+
+	if isNewDevice {
+		events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
+			"device": id.String(),
+			"addrs":  device.Addresses,
+			"relays": device.Relays,
+		})
+	}
+
+	return isNewDevice
+}
+
+func addrToAddr(addr *net.TCPAddr) string {
+	if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
+		return fmt.Sprintf(":%c", addr.Port)
+	} else if bs := addr.IP.To4(); bs != nil {
+		return fmt.Sprintf("%s:%c", bs.String(), addr.Port)
+	} else if bs := addr.IP.To16(); bs != nil {
+		return fmt.Sprintf("[%s]:%c", bs.String(), addr.Port)
+	}
+	return ""
+}
+
+func resolveAddrs(addrs []string) []string {
+	var raddrs []string
+	for _, addrStr := range addrs {
+		uri, err := url.Parse(addrStr)
+		if err != nil {
+			continue
+		}
+		addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
+		if err != nil {
+			continue
+		}
+		addr := addrToAddr(addrRes)
+		if len(addr) > 0 {
+			uri.Host = addr
+			raddrs = append(raddrs, uri.String())
+		}
+	}
+	return raddrs
+}

+ 3 - 3
lib/discover/packets.go → lib/discover/localpackets.go

@@ -5,7 +5,7 @@
 // You can obtain one at http://mozilla.org/MPL/2.0/.
 // You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 //go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
 //go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
-//go:generate genxdr -o packets_xdr.go packets.go
+//go:generate genxdr -o localpackets_xdr.go localpackets.go
 
 
 package discover
 package discover
 
 
@@ -26,8 +26,8 @@ type Announce struct {
 }
 }
 
 
 type Relay struct {
 type Relay struct {
-	Address string // max:256
-	Latency int32
+	URL     string `json:"url"` // max:2083
+	Latency int32  `json:"latency"`
 }
 }
 
 
 type Device struct {
 type Device struct {

+ 7 - 7
lib/discover/packets_xdr.go → lib/discover/localpackets_xdr.go

@@ -192,10 +192,10 @@ Relay Structure:
  0                   1                   2                   3
  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-|                       Length of Address                       |
+|                         Length of URL                         |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 /                                                               /
 /                                                               /
-\                   Address (variable length)                   \
+\                     URL (variable length)                     \
 /                                                               /
 /                                                               /
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                            Latency                            |
 |                            Latency                            |
@@ -203,7 +203,7 @@ Relay Structure:
 
 
 
 
 struct Relay {
 struct Relay {
-	string Address<256>;
+	string URL<256>;
 	int Latency;
 	int Latency;
 }
 }
 
 
@@ -234,10 +234,10 @@ func (o Relay) AppendXDR(bs []byte) ([]byte, error) {
 }
 }
 
 
 func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) {
 func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) {
-	if l := len(o.Address); l > 256 {
-		return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 256)
+	if l := len(o.URL); l > 256 {
+		return xw.Tot(), xdr.ElementSizeExceeded("URL", l, 256)
 	}
 	}
-	xw.WriteString(o.Address)
+	xw.WriteString(o.URL)
 	xw.WriteUint32(uint32(o.Latency))
 	xw.WriteUint32(uint32(o.Latency))
 	return xw.Tot(), xw.Error()
 	return xw.Tot(), xw.Error()
 }
 }
@@ -254,7 +254,7 @@ func (o *Relay) UnmarshalXDR(bs []byte) error {
 }
 }
 
 
 func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error {
 func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error {
-	o.Address = xr.ReadStringMax(256)
+	o.URL = xr.ReadStringMax(256)
 	o.Latency = int32(xr.ReadUint32())
 	o.Latency = int32(xr.ReadUint32())
 	return xr.Error()
 	return xr.Error()
 }
 }

+ 3 - 0
lib/events/events.go

@@ -40,6 +40,7 @@ const (
 	FolderErrors
 	FolderErrors
 	FolderScanProgress
 	FolderScanProgress
 	ExternalPortMappingChanged
 	ExternalPortMappingChanged
+	RelayStateChanged
 
 
 	AllEvents = (1 << iota) - 1
 	AllEvents = (1 << iota) - 1
 )
 )
@@ -90,6 +91,8 @@ func (t EventType) String() string {
 		return "FolderScanProgress"
 		return "FolderScanProgress"
 	case ExternalPortMappingChanged:
 	case ExternalPortMappingChanged:
 		return "ExternalPortMappingChanged"
 		return "ExternalPortMappingChanged"
+	case RelayStateChanged:
+		return "RelayStateChanged"
 	default:
 	default:
 		return "Unknown"
 		return "Unknown"
 	}
 	}

+ 2 - 0
lib/ignore/ignore.go

@@ -12,6 +12,7 @@ import (
 	"crypto/md5"
 	"crypto/md5"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
+	"log"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"regexp"
 	"regexp"
@@ -236,6 +237,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
 			includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
 			includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
 			includes, err := loadIgnoreFile(includeFile, seen)
 			includes, err := loadIgnoreFile(includeFile, seen)
 			if err != nil {
 			if err != nil {
+				log.Println(err)
 				return err
 				return err
 			}
 			}
 			patterns = append(patterns, includes...)
 			patterns = append(patterns, includes...)

+ 137 - 11
lib/relay/relay.go

@@ -12,18 +12,23 @@ import (
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"sort"
 	"time"
 	"time"
 
 
 	"github.com/syncthing/relaysrv/client"
 	"github.com/syncthing/relaysrv/client"
 	"github.com/syncthing/relaysrv/protocol"
 	"github.com/syncthing/relaysrv/protocol"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/discover"
+	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/sync"
 
 
 	"github.com/thejerf/suture"
 	"github.com/thejerf/suture"
 )
 )
 
 
+const (
+	eventBroadcasterCheckInterval = 10 * time.Second
+)
+
 type Svc struct {
 type Svc struct {
 	*suture.Supervisor
 	*suture.Supervisor
 	cfg    *config.Wrapper
 	cfg    *config.Wrapper
@@ -71,7 +76,12 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
 		stop:        make(chan struct{}),
 		stop:        make(chan struct{}),
 	}
 	}
 
 
+	eventBc := &eventBroadcaster{
+		svc: svc,
+	}
+
 	svc.Add(receiver)
 	svc.Add(receiver)
+	svc.Add(eventBc)
 
 
 	return svc
 	return svc
 }
 }
@@ -132,7 +142,7 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
 			continue
 			continue
 		}
 		}
 
 
-		dynRelays := make([]discover.Relay, 0, len(ann.Relays))
+		var dynRelayAddrs []string
 		for _, relayAnn := range ann.Relays {
 		for _, relayAnn := range ann.Relays {
 			ruri, err := url.Parse(relayAnn.URL)
 			ruri, err := url.Parse(relayAnn.URL)
 			if err != nil {
 			if err != nil {
@@ -144,13 +154,11 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
 			if debug {
 			if debug {
 				l.Debugln("Found", ruri, "via", uri)
 				l.Debugln("Found", ruri, "via", uri)
 			}
 			}
-			dynRelays = append(dynRelays, discover.Relay{
-				Address: ruri.String(),
-			})
+			dynRelayAddrs = append(dynRelayAddrs, ruri.String())
 		}
 		}
 
 
-		dynRelayAddrs := discover.RelayAddressesSortedByLatency(dynRelays)
 		if len(dynRelayAddrs) > 0 {
 		if len(dynRelayAddrs) > 0 {
+			dynRelayAddrs = relayAddressesSortedByLatency(dynRelayAddrs)
 			closestRelay := dynRelayAddrs[0]
 			closestRelay := dynRelayAddrs[0]
 			if debug {
 			if debug {
 				l.Debugln("Picking", closestRelay, "as closest dynamic relay from", uri)
 				l.Debugln("Picking", closestRelay, "as closest dynamic relay from", uri)
@@ -193,7 +201,14 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
 	return true
 	return true
 }
 }
 
 
-func (s *Svc) ClientStatus() map[string]bool {
+type Status struct {
+	URL     string
+	OK      bool
+	Latency int
+}
+
+// Relays return the list of relays that currently have an OK status.
+func (s *Svc) Relays() []string {
 	if s == nil {
 	if s == nil {
 		// A nil client does not have a status, really. Yet we may be called
 		// A nil client does not have a status, really. Yet we may be called
 		// this way, for raisins...
 		// this way, for raisins...
@@ -201,12 +216,34 @@ func (s *Svc) ClientStatus() map[string]bool {
 	}
 	}
 
 
 	s.mut.RLock()
 	s.mut.RLock()
-	status := make(map[string]bool, len(s.clients))
-	for uri, client := range s.clients {
-		status[uri] = client.StatusOK()
+	relays := make([]string, 0, len(s.clients))
+	for uri := range s.clients {
+		relays = append(relays, uri)
+	}
+	s.mut.RUnlock()
+
+	sort.Strings(relays)
+
+	return relays
+}
+
+// RelayStatus returns the latency and OK status for a given relay.
+func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
+	if s == nil {
+		// A nil client does not have a status, really. Yet we may be called
+		// this way, for raisins...
+		return time.Hour, false
 	}
 	}
+
+	s.mut.RLock()
+	client, ok := s.clients[uri]
 	s.mut.RUnlock()
 	s.mut.RUnlock()
-	return status
+
+	if !ok || !client.StatusOK() {
+		return time.Hour, false
+	}
+
+	return client.Latency(), true
 }
 }
 
 
 // Accept returns a new *tls.Conn. The connection is already handshaken.
 // Accept returns a new *tls.Conn. The connection is already handshaken.
@@ -266,6 +303,55 @@ func (r *invitationReceiver) Stop() {
 	close(r.stop)
 	close(r.stop)
 }
 }
 
 
+// The eventBroadcaster sends a RelayStateChanged event when the relay status
+// changes. We need this somewhat ugly polling mechanism as there's currently
+// no way to get the event feed directly from the relay lib. This may be
+// somethign to revisit later, possibly.
+type eventBroadcaster struct {
+	svc  *Svc
+	stop chan struct{}
+}
+
+func (e *eventBroadcaster) Serve() {
+	timer := time.NewTicker(eventBroadcasterCheckInterval)
+	defer timer.Stop()
+
+	var prevOKRelays []string
+
+	for {
+		select {
+		case <-timer.C:
+			curOKRelays := e.svc.Relays()
+
+			changed := len(curOKRelays) != len(prevOKRelays)
+			if !changed {
+				for i := range curOKRelays {
+					if curOKRelays[i] != prevOKRelays[i] {
+						changed = true
+						break
+					}
+				}
+			}
+
+			if changed {
+				events.Default.Log(events.RelayStateChanged, map[string][]string{
+					"old": prevOKRelays,
+					"new": curOKRelays,
+				})
+			}
+
+			prevOKRelays = curOKRelays
+
+		case <-e.stop:
+			return
+		}
+	}
+}
+
+func (e *eventBroadcaster) Stop() {
+	close(e.stop)
+}
+
 // This is the announcement recieved from the relay server;
 // This is the announcement recieved from the relay server;
 // {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]}
 // {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]}
 type dynamicAnnouncement struct {
 type dynamicAnnouncement struct {
@@ -273,3 +359,43 @@ type dynamicAnnouncement struct {
 		URL string
 		URL string
 	}
 	}
 }
 }
+
+// relayAddressesSortedByLatency adds local latency to the relay, and sorts them
+// by sum latency, and returns the addresses.
+func relayAddressesSortedByLatency(input []string) []string {
+	relays := make(relayList, len(input))
+	for i, relay := range input {
+		if latency, err := osutil.GetLatencyForURL(relay); err == nil {
+			relays[i] = relayWithLatency{relay, int(latency / time.Millisecond)}
+		} else {
+			relays[i] = relayWithLatency{relay, int(time.Hour / time.Millisecond)}
+		}
+	}
+
+	sort.Sort(relays)
+
+	addresses := make([]string, len(relays))
+	for i, relay := range relays {
+		addresses[i] = relay.relay
+	}
+	return addresses
+}
+
+type relayWithLatency struct {
+	relay   string
+	latency int
+}
+
+type relayList []relayWithLatency
+
+func (l relayList) Len() int {
+	return len(l)
+}
+
+func (l relayList) Less(a, b int) bool {
+	return l[a].latency < l[b].latency
+}
+
+func (l relayList) Swap(a, b int) {
+	l[a], l[b] = l[b], l[a]
+}

+ 1 - 2
test/h1/config.xml

@@ -46,8 +46,7 @@
     </gui>
     </gui>
     <options>
     <options>
         <listenAddress>tcp://127.0.0.1:22001</listenAddress>
         <listenAddress>tcp://127.0.0.1:22001</listenAddress>
-        <globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
-        <globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
+        <globalAnnounceServer>default</globalAnnounceServer>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <localAnnounceEnabled>true</localAnnounceEnabled>
         <localAnnounceEnabled>true</localAnnounceEnabled>
         <localAnnouncePort>21025</localAnnouncePort>
         <localAnnouncePort>21025</localAnnouncePort>

+ 1 - 2
test/h2/config.xml

@@ -53,8 +53,7 @@
     </gui>
     </gui>
     <options>
     <options>
         <listenAddress>tcp://127.0.0.1:22002</listenAddress>
         <listenAddress>tcp://127.0.0.1:22002</listenAddress>
-        <globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
-        <globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
+        <globalAnnounceServer>default</globalAnnounceServer>
         <globalAnnounceEnabled>true</globalAnnounceEnabled>
         <globalAnnounceEnabled>true</globalAnnounceEnabled>
         <localAnnounceEnabled>true</localAnnounceEnabled>
         <localAnnounceEnabled>true</localAnnounceEnabled>
         <localAnnouncePort>21025</localAnnouncePort>
         <localAnnouncePort>21025</localAnnouncePort>

+ 1 - 2
test/h3/config.xml

@@ -39,8 +39,7 @@
     </gui>
     </gui>
     <options>
     <options>
         <listenAddress>tcp://127.0.0.1:22003</listenAddress>
         <listenAddress>tcp://127.0.0.1:22003</listenAddress>
-        <globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
-        <globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
+        <globalAnnounceServer>default</globalAnnounceServer>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <localAnnounceEnabled>false</localAnnounceEnabled>
         <localAnnounceEnabled>false</localAnnounceEnabled>
         <localAnnouncePort>21025</localAnnouncePort>
         <localAnnouncePort>21025</localAnnouncePort>

+ 1 - 2
test/h4/config.xml

@@ -18,8 +18,7 @@
     </gui>
     </gui>
     <options>
     <options>
         <listenAddress>tcp://127.0.0.1:22004</listenAddress>
         <listenAddress>tcp://127.0.0.1:22004</listenAddress>
-        <globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
-        <globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
+        <globalAnnounceServer>default</globalAnnounceServer>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <localAnnounceEnabled>false</localAnnounceEnabled>
         <localAnnounceEnabled>false</localAnnounceEnabled>
         <localAnnouncePort>21025</localAnnouncePort>
         <localAnnouncePort>21025</localAnnouncePort>

Some files were not shown because too many files changed in this diff