From b0cd7be39b914a3766a092d1af9d2185a7c59bce Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Sun, 20 Sep 2015 15:30:25 +0200 Subject: [PATCH] New global discovery protocol over HTTPS (fixes #628, fixes #1907) --- Godeps/Godeps.json | 6 +- .../syncthing/relaysrv/client/client.go | 20 +- .../syncthing/relaysrv/client/methods.go | 26 +- .../syncthing/relaysrv/protocol/packets.go | 3 +- .../src/github.com/thejerf/suture/README.md | 10 +- cmd/stfinddevice/main.go | 96 ++- .../{externaladdr.go => addresslister.go} | 16 +- cmd/syncthing/connections.go | 15 +- cmd/syncthing/gui.go | 50 +- cmd/syncthing/main.go | 82 +-- cmd/syncthing/verbose.go | 11 + gui/assets/css/overrides.css | 4 + gui/index.html | 26 +- gui/syncthing/core/syncthingController.js | 23 +- gui/syncthing/device/editDeviceModalView.html | 2 +- lib/auto/gui.files.go | 8 +- lib/beacon/beacon.go | 47 +- lib/beacon/broadcast.go | 70 ++- lib/beacon/multicast.go | 255 ++++++-- lib/config/config.go | 35 +- lib/config/config_test.go | 2 +- lib/config/wrapper.go | 12 + lib/discover/cache.go | 192 ++++++ lib/discover/client.go | 54 -- lib/discover/client_test.go | 239 -------- lib/discover/client_udp.go | 261 --------- lib/discover/discover.go | 549 +----------------- lib/discover/discover_test.go | 163 ------ lib/discover/doc.go | 71 ++- lib/discover/global.go | 385 ++++++++++++ lib/discover/global_test.go | 253 ++++++++ lib/discover/local.go | 270 +++++++++ lib/discover/{packets.go => localpackets.go} | 6 +- .../{packets_xdr.go => localpackets_xdr.go} | 14 +- lib/events/events.go | 3 + lib/ignore/ignore.go | 2 + lib/relay/relay.go | 148 ++++- test/h1/config.xml | 3 +- test/h2/config.xml | 3 +- test/h3/config.xml | 3 +- test/h4/config.xml | 3 +- 41 files changed, 1937 insertions(+), 1504 deletions(-) rename cmd/syncthing/{externaladdr.go => addresslister.go} (84%) create mode 100644 lib/discover/cache.go delete mode 100644 lib/discover/client.go delete mode 100644 lib/discover/client_test.go delete mode 100644 lib/discover/client_udp.go delete mode 100644 lib/discover/discover_test.go create mode 100644 lib/discover/global.go create mode 100644 lib/discover/global_test.go create mode 100644 lib/discover/local.go rename lib/discover/{packets.go => localpackets.go} (84%) rename lib/discover/{packets_xdr.go => localpackets_xdr.go} (97%) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d82671f84..da62100a9 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -43,11 +43,11 @@ }, { "ImportPath": "github.com/syncthing/relaysrv/client", - "Rev": "7fe1fdd8c751df165ea825bc8d3e895f118bb236" + "Rev": "6e126fb97e2ff566d35f8d8824e86793d22b2147" }, { "ImportPath": "github.com/syncthing/relaysrv/protocol", - "Rev": "7fe1fdd8c751df165ea825bc8d3e895f118bb236" + "Rev": "6e126fb97e2ff566d35f8d8824e86793d22b2147" }, { "ImportPath": "github.com/syndtr/goleveldb/leveldb", @@ -55,7 +55,7 @@ }, { "ImportPath": "github.com/thejerf/suture", - "Rev": "fc7aaeabdc43fe41c5328efa1479ffea0b820978" + "Rev": "860b44045335c64a6d54ac7eed22a3aedfc687c9" }, { "ImportPath": "github.com/vitrun/qart/coding", diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go index 94e4eedd2..89b16e000 100644 --- a/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go @@ -32,6 +32,7 @@ type ProtocolClient struct { mut sync.RWMutex connected bool + latency time.Duration } func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient { @@ -168,6 +169,13 @@ func (c *ProtocolClient) StatusOK() bool { return con } +func (c *ProtocolClient) Latency() time.Duration { + c.mut.RLock() + lat := c.latency + c.mut.RUnlock() + return lat +} + func (c *ProtocolClient) String() string { return fmt.Sprintf("ProtocolClient@%p", c) } @@ -177,11 +185,21 @@ func (c *ProtocolClient) connect() error { 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 { 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 { conn.Close() return err diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go index ef6145e9c..67a9a71c1 100644 --- a/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go @@ -8,6 +8,7 @@ import ( "net" "net/url" "strconv" + "strings" "time" 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.SetDeadline(time.Now().Add(10 * time.Second)) if err != nil { return protocol.SessionInvitation{}, err } + conn.SetDeadline(time.Now().Add(10 * time.Second)) if err := performHandshakeAndValidation(conn, uri); err != nil { 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 { return &tls.Config{ Certificates: certs, diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go index 7ff020115..1b21eba24 100644 --- a/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go @@ -7,8 +7,9 @@ package protocol import ( "fmt" - syncthingprotocol "github.com/syncthing/protocol" "net" + + syncthingprotocol "github.com/syncthing/protocol" ) const ( diff --git a/Godeps/_workspace/src/github.com/thejerf/suture/README.md b/Godeps/_workspace/src/github.com/thejerf/suture/README.md index 3bbd9875a..83888737a 100644 --- a/Godeps/_workspace/src/github.com/thejerf/suture/README.md +++ b/Godeps/_workspace/src/github.com/thejerf/suture/README.md @@ -6,10 +6,8 @@ Suture Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" -> "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 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 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 ------------ diff --git a/cmd/stfinddevice/main.go b/cmd/stfinddevice/main.go index 0cf27ec13..f01fe5fbd 100644 --- a/cmd/stfinddevice/main.go +++ b/cmd/stfinddevice/main.go @@ -7,41 +7,105 @@ package main import ( + "crypto/tls" + "errors" "flag" - "log" + "fmt" + "net/url" "os" + "time" "github.com/syncthing/protocol" + "github.com/syncthing/syncthing/lib/config" "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 - 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() - if len(flag.Args()) != 1 || server == "" { - log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22027\"] ", os.Args[0]) + if flag.NArg() != 1 { + flag.Usage() os.Exit(64) } id, err := protocol.DeviceIDFromString(flag.Args()[0]) if err != nil { - log.Println(err) + fmt.Println(err) 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) - } - for _, addr := range relays { - log.Println("relay:", 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) + } + } +} + +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] \n\nOptions:\n", os.Args[0]) + flag.PrintDefaults() +} diff --git a/cmd/syncthing/externaladdr.go b/cmd/syncthing/addresslister.go similarity index 84% rename from cmd/syncthing/externaladdr.go rename to cmd/syncthing/addresslister.go index 5868276a8..5f85c6e93 100644 --- a/cmd/syncthing/externaladdr.go +++ b/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 // be substituted. 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 // 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) { // A public address; include as is. 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 } diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index 960e0e32a..1be9db1bb 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -43,7 +43,7 @@ type connectionSvc struct { myID protocol.DeviceID model *model.Model tlsCfg *tls.Config - discoverer *discover.Discoverer + discoverer discover.Finder conns chan model.IntermediateConnection relaySvc *relay.Svc @@ -54,7 +54,7 @@ type connectionSvc struct { 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{ Supervisor: suture.NewSimple("connectionSvc"), cfg: cfg, @@ -264,13 +264,14 @@ func (s *connectionSvc) connect() { } var addrs []string - var relays []string + var relays []discover.Relay for _, addr := range deviceCfg.Addresses { if addr == "dynamic" { 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 { addrs = append(addrs, addr) @@ -333,7 +334,7 @@ func (s *connectionSvc) connect() { s.lastRelayCheck[deviceID] = time.Now() for _, addr := range relays { - uri, err := url.Parse(addr) + uri, err := url.Parse(addr.URL) if err != nil { l.Infoln("Failed to parse relay connection url:", addr, err) continue diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 75d0cad66..ed3697fe5 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -33,6 +33,7 @@ import ( "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/osutil" + "github.com/syncthing/syncthing/lib/relay" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/upgrade" @@ -58,14 +59,15 @@ type apiSvc struct { assetDir string model *model.Model eventSub *events.BufferedSubscription - discoverer *discover.Discoverer + discoverer *discover.CachingMux + relaySvc *relay.Svc listener net.Listener fss *folderSummarySvc stop chan struct{} 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{ id: id, cfg: cfg, @@ -73,6 +75,7 @@ func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir strin model: m, eventSub: eventSub, discoverer: discoverer, + relaySvc: relaySvc, systemConfigMut: sync.NewMutex(), } @@ -164,7 +167,6 @@ func (s *apiSvc) Serve() { postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay] postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // - postRestMux.HandleFunc("/rest/system/discovery", s.postSystemDiscovery) // device addr postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // - 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["sys"] = m.Sys - m.HeapReleased 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() var cpusum float64 @@ -679,25 +700,16 @@ func (s *apiSvc) showGuiError(l logger.LogLevel, err string) { 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) { 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 { // Device ids can't be marshalled as keys so we need to manually // rebuild this map using strings. Discoverer may be nil if discovery // 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 } } diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 6210136ed..a5d85b47c 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -114,7 +114,6 @@ var ( writeRateLimit *ratelimit.Bucket readRateLimit *ratelimit.Bucket stop = make(chan int) - relaySvc *relay.Svc cert tls.Certificate lans []*net.IPNet ) @@ -689,8 +688,7 @@ func syncthingMain() { var addrList *addressLister - // Start UPnP. The UPnP service will restart global discovery if the - // external port changes. + // Start UPnP if opts.UPnPEnabled { upnpSvc := newUPnPSvc(cfg, addr.Port) @@ -703,14 +701,6 @@ func syncthingMain() { addrList = newAddressLister(nil, cfg) } - // Start discovery - - discoverer := discovery(addrList, relaySvc) - - // GUI - - setupGUI(mainSvc, cfg, m, apiSub, discoverer) - // Start relay management var relaySvc *relay.Svc @@ -719,9 +709,51 @@ func syncthingMain() { 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 - connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg, discoverer, relaySvc) + connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg, cachedDiscovery, relaySvc) mainSvc.Add(connectionSvc) if cpuProfile { @@ -844,7 +876,7 @@ func startAuditing(mainSvc *suture.Supervisor) { 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() 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))) 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 { l.Fatalln("Cannot start GUI:", err) } @@ -944,28 +976,6 @@ func shutdown() { 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) { fi, err := os.Stat(dir) if os.IsNotExist(err) { diff --git a/cmd/syncthing/verbose.go b/cmd/syncthing/verbose.go index 9e9f90a0d..7fd215f00 100644 --- a/cmd/syncthing/verbose.go +++ b/cmd/syncthing/verbose.go @@ -8,6 +8,7 @@ package main import ( "fmt" + "strings" "github.com/syncthing/syncthing/lib/events" ) @@ -139,6 +140,16 @@ func (s *verboseSvc) formatEvent(ev events.Event) string { data := ev.Data.(map[string]string) device := data["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) diff --git a/gui/assets/css/overrides.css b/gui/assets/css/overrides.css index 94abd1588..abfbbb7ea 100644 --- a/gui/assets/css/overrides.css +++ b/gui/assets/css/overrides.css @@ -249,3 +249,7 @@ ul.three-columns li, ul.two-columns li { position: static; } } + +.popover { + min-width: 250px; +} diff --git a/gui/index.html b/gui/index.html index e8b702828..d3bbf2d58 100755 --- a/gui/index.html +++ b/gui/index.html @@ -379,28 +379,28 @@  CPU Utilization {{system.cpuPercent | alwaysNumber | natural:1}}% - -  Global Discovery + +  Discovery - - OK + + {{discoveryTotal}}/{{discoveryTotal}} - - - {{announceServersTotal-announceServersFailed.length}}/{{announceServersTotal}} + + + {{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}} - +  Relays - - OK + + {{relaysTotal}}/{{relaysTotal}} - - - {{relayClientsTotal-relayClientsFailed.length}}/{{relayClientsTotal}} + + + {{relaysTotal-relaysFailed.length}}/{{relaysTotal}} diff --git a/gui/syncthing/core/syncthingController.js b/gui/syncthing/core/syncthingController.js index 00234d0c8..5da13e88f 100755 --- a/gui/syncthing/core/syncthingController.js +++ b/gui/syncthing/core/syncthingController.js @@ -378,24 +378,25 @@ angular.module('syncthing.core') $scope.myID = data.myID; $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) { if (!data.relayClientStatus[relay]) { - failedRelays.push(relay); + relaysFailed.push(relay); } + relaysTotal++; } - $scope.relayClientsFailed = failedRelays; - + $scope.relaysFailed = relaysFailed; + $scope.relaysTotal = relaysTotal; console.log("refreshSystem", data); }).error($scope.emitHTTPError); diff --git a/gui/syncthing/device/editDeviceModalView.html b/gui/syncthing/device/editDeviceModalView.html index 0606d1337..9e5b3ac23 100644 --- a/gui/syncthing/device/editDeviceModalView.html +++ b/gui/syncthing/device/editDeviceModalView.html @@ -13,7 +13,7 @@ -
{{currentDevice.deviceID}}

diff --git a/lib/auto/gui.files.go b/lib/auto/gui.files.go index 873872910..a14a8a45d 100644 --- a/lib/auto/gui.files.go +++ b/lib/auto/gui.files.go @@ -5,13 +5,13 @@ import ( ) const ( - AssetsBuildDate = "Thu, 10 Sep 2015 13:04:55 GMT" + AssetsBuildDate = "Fri, 18 Sep 2015 06:05:31 GMT" ) func Assets() map[string][]byte { var assets = make(map[string][]byte, 116) - assets["assets/css/overrides.css"], _ = base64.StdEncoding.DecodeString("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") + assets["assets/css/overrides.css"], _ = base64.StdEncoding.DecodeString("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") assets["assets/font/raleway-500.woff"], _ = base64.StdEncoding.DecodeString("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") assets["assets/font/raleway.css"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3JIy88r0U1LTE5VqOZSUIDycjNzKq0U1IMSc1LLEyvVrWEyxSWVOalWCnn5RbmJOXDR8tTM9IwSKwVTAwOQWHFRspVCTn5yYo4G3ARNHYXSohyNIghXF6hSrzw/LU0TaADQqBINdRBPXdOaq5YLAAAA//8BAAD//xy2WhaSAAAA") assets["assets/img/favicon.png"], _ = base64.StdEncoding.DecodeString("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") @@ -49,7 +49,7 @@ func Assets() map[string][]byte { assets["assets/lang/lang-zh-TW.json"], _ = base64.StdEncoding.DecodeString("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") assets["assets/lang/prettyprint.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1xSbW7aQBD931Os/MtI9QUqVWrtUtoQEWQg+T3gxd54PWvtBwiiHCcnycUys7aA5Bfz3puZffPwAazQgPXSSu9PvVXoxU/xkmzr5EeSB12DVYDJ92QHRBTgQY/w1wG0xJ264UX6OHI44R7H0lnuGgKVJDCTtovjUjOyUrYMkMAUa61cE2E2y6+MSDeovKzEXGFdmY43S9686gHHCZdNV1dGpFSoaGGviP6rcGzcW4aWLA4oK35fCJEWgFABjzWB6H8BL9crT8R/OnKAz3z0HdBz0knCrcnmJVFzYyXHEH95kea5e+WbQM7iKG6JWRh7lDURIjdt9/6mWeBI/gQfrSF+6lqc0FjHWfXctTRjVL3P8jIS1oc6kBmR5hbOSk8Gdbn+og41RN2arHwgvTRddCfSsYoiR1AG5wbb7sD5HmU1PBxagpvWUsyDfm6yYsF/d6MwPsRFXETK+ulWWYM68gfy+u0DAAD//wEAAP//j0iqQH0CAAA=") assets["assets/lang/valid-langs.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/yTMvQ6CMBTF8d2naDp7H8FEIcZBo6aSOBiHAhUqTSEtdPDpvacsv38/kpN0EEk7216076LYiZesO7mVjc7s+c/4xuZbZFrDGAd8hk4FGjN0fHA/FoQMlQduvzB2Zr5YGkY6K67Di68BFj0WJ5ymmQq19l5xw0jqhmInJmYZmF9P5XVt9ZTvzR8AAP//AQAA//8eEnv9zQAAAA==") - assets["index.html"], _ = base64.StdEncoding.DecodeString("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") + assets["index.html"], _ = base64.StdEncoding.DecodeString("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") assets["modal.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3RST0/cPhC9/z7F/HLogkQ2gDjRXaoWqVIlKiHBpceJPUlcHDuyJ9BtlO/eibNNYbUcEv97783Mm9lo8wzKYozbrPUaLVSoKQPG0jhNv7ZZfpGBq3NkDrlGxrxE9aSD77bZMAjVR4JPwKEnuIZVZGSjVjCOB6wn2pUeg06sL95bQneS2KcT+OY/gM3/eS5LUcCt73bB1A3Dye0pXJ5fXMFjQ/Cwc4ob42r43HPjQ1wn+Mx5bEyEB98HRcLXBF99aEHuYl/+JMXAHlhEmEIbwVfp8N3/NtYi3PelNWoWujOKXKQzeF7D5fp8Dd8qQFCS0sK6v4MXjOA8gzaRgyl7Jg0vhhsBSMzKWDqb5X74HhQ68CWjkcURIEPD3F0XRTvHX/tQF6JaSLxiKirPkyGHzcm1QevrZO3+fljNL7ZeXYPFUNNs5jG28o7J8f79GKIh6X4AtBR4/ufDMPW0j+O48ITZXL0lsmFLrwACiZ1ULYmaapsZCZ39ZVQoQya60+Wkuikm6GvuMCS9cfwXsGiulrQLyfv9Gkqvd8khDuiisr2m95n7/NIgZm91Ku9lVl4XLV1m74B3HW2z+bBwSnYgX66pwt5y2sc2gzT8MiStWZSP2PTGGTYtxcWWD66M3ccZliqyyHRzOyV8aNymmJM6Uu6y3W/2yx8AAAD//wEAAP//qvxG6f8DAAA=") assets["syncthing/app.js"], _ = base64.StdEncoding.DecodeString("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") assets["syncthing/core/aboutModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2ENqWktOglwHhFDvTgpSgT/+/yw/F8fMTmxIQ2asYX5ErxPF0fokCE1lSuxAgl7pVgB3TlkPaXAMrbnkQlOKpm7M842uEdQs8af4lLMKed0Z2EP7NymGKzvFo3BZN4NuNei+/06EdztpYNiul75amr56AQAA//8BAAD//xLf4CHFAAAA") @@ -78,7 +78,7 @@ func Assets() map[string][]byte { assets["syncthing/core/selectOnClickDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3SQQWvEIBCF7/srPBRiIEjPXXooe2+hPZYeRKeprJkpo2YpJf+9atLdJWzeSZ/z+Z5q7JPXrAayyYNswg+a+OWwV4YYmnYnspR1DCa6sQyAz8sXPHhnjk0nPlMGHKGQdyeHlk6t+K1QEUNMjFdGkXd4fLjigqFv6ES+dwCMndAxcmhXUNEyoQhlY1bx8hZQNGoWc+cS9iiWlqqH+PZvy3a/yXL+IcicJZNqumHQEV6LvcVVRs2pz2ThQBgzGuTygvf7jw3y3FQxDDTCk/c1KWxFXQBt7Vyqpt8Yn1bedN5N+105/AMAAP//AQAA//+SF+4JDAIAAA==") assets["syncthing/core/shutdownDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOwQqDQAxE737F3qIg672eCv2F9r6sqQZitsRspRT/vdqC1DnOTDIvSJ85qB9TlxlLmF4SbSDpfUyKUBVule9IMRo9t8KQrUuzXChw6qF297xeUBJXVu79rW9StKzyZ/zMyZSinRycoT5EhuODg+FVeU13imajaI6bN8LZDzYy7B+WtliqtvgAAAD//wEAAP//3qFOo80AAAA=") assets["syncthing/core/shutdownDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0TOTaqDMBAH8PXzFMNsXL16gZhNj+AJ0jjWQJwJzgQp1ru3lkK3P/h/uEXGkCGNPepcbZSNEdSCVX1LjZFUEVIU7rHIRuu/TBOCJcvU4763wzcFV1lKJqMWnmBrYM3B6DjQN3+u/MQPD442J77DHBRuRAznMpwlF9cV37juc8o3LwAAAP//AQAA//99X8KxnQAAAA==") - assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("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") + assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("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") assets["syncthing/core/uniqueFolderDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/7SSwW4yMQyE7zyFD7+0IK3CHc5/b1V74h5tvKzVkIDtQFHFuzdhUaGwtJWqzgVp4vkYW2vDMnnLZhVd8jiuZB8a7SgsTRMZq8kIsowjxkZpmwdSoE3Ch+gdclVDm/I8xQDjCbwdh4sYNXG4MHpzkzJnBlVYPkaHvqo/vXsKL7MLoDRxjTWgX9VgVVlqaJT95ApbVHzzb21ZkMWkIB21Oj6jtoS7hfUJh8JF1J7+z6Ajzev/fyUpv/cCRdMp7BCki8m7UClsrSdnFe8G+pqCuiiTpPubcyrnjvNBwCFfQvCiaXsMiemsPO3CM2ePM/K86zfdtUPoGYBlWwHrGa3b/6J/a3PHrxf4aScS6OF/dM5B9/ThftzwNnu44p05h/moPL4DAAD//wEAAP//r65WJ1IDAAA=") assets["syncthing/core/upgradingDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1yOwQrCMBBE7/2K3LaFkt7tSfAX9B7SNV3YJmW7UUT676YKRZ3jzNudcTFkdmKnNGTGGpZH9DpSDNYnQWgqU2QHEvRKtwLkOYgbCnAixylAa665nFCKpm7M881vEtQs8cv4mIsKeT0YOEL7EylOMzvFs3BJ9xndNqP7K70Q3u2oE8P+Yu2rtemrFwAAAP//AQAA//99zQ2GzwAAAA==") assets["syncthing/core/upgradingDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1zOTarDMAwE4PXLKYQ2WSW5gO0zPCg9gHDcVODaxlIIJc3dm5b+QLfDN8yYSx4pAo8W5zJVGjlNCKKks1jkdMoI7HOySLXmpfNcfQzdXBCUNQaL69oe380WbqCVkkTSsG3omj9TnJFC6Zu7wzV5Pe8cWOCz2pvh4Rz88v8YSAIsxPoifb/j4hozPN+75g4AAP//AQAA///kaeW6xgAAAA==") diff --git a/lib/beacon/beacon.go b/lib/beacon/beacon.go index 9c13e3c84..bafc6aa9b 100644 --- a/lib/beacon/beacon.go +++ b/lib/beacon/beacon.go @@ -6,7 +6,12 @@ package beacon -import "net" +import ( + "net" + stdsync "sync" + + "github.com/thejerf/suture" +) type recv struct { data []byte @@ -14,34 +19,30 @@ type recv struct { } type Interface interface { + suture.Service Send(data []byte) Recv() ([]byte, net.Addr) + Error() error } type readerFrom interface { 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 } diff --git a/lib/beacon/broadcast.go b/lib/beacon/broadcast.go index 5ab2b7f58..c4ad41099 100644 --- a/lib/beacon/broadcast.go +++ b/lib/beacon/broadcast.go @@ -19,6 +19,8 @@ type Broadcast struct { port int inbox chan []byte outbox chan recv + br *broadcastReader + bw *broadcastWriter } func NewBroadcast(port int) *Broadcast { @@ -41,14 +43,16 @@ func NewBroadcast(port int) *Broadcast { outbox: make(chan recv, 16), } - b.Add(&broadcastReader{ + b.br = &broadcastReader{ port: port, outbox: b.outbox, - }) - b.Add(&broadcastWriter{ + } + b.Add(b.br) + b.bw = &broadcastWriter{ port: port, inbox: b.inbox, - }) + } + b.Add(b.bw) return b } @@ -62,11 +66,18 @@ func (b *Broadcast) Recv() ([]byte, net.Addr) { 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 { - 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() { @@ -78,22 +89,21 @@ func (w *broadcastWriter) Serve() { var err error w.conn, err = net.ListenUDP("udp4", 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 } defer w.conn.Close() - w.failed = false - for bs := range w.inbox { addrs, err := net.InterfaceAddrs() if err != nil { if debug { - l.Debugln("Local discovery (broadcast writer):", err) + l.Debugln(err) } + w.setError(err) continue } @@ -117,13 +127,16 @@ func (w *broadcastWriter) Serve() { for _, ip := range dsts { 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) + w.conn.SetWriteDeadline(time.Time{}) if err, ok := err.(net.Error); ok && err.Timeout() { // Write timeouts should not happen. We treat it as a fatal // error on the socket. - l.Infoln("Local discovery (broadcast writer):", err) - w.failed = true + if debug { + l.Debugln(err) + } + w.setError(err) return } else if err, ok := err.(net.Error); ok && err.Temporary() { // A transient error. Lets hope for better luck in the future. @@ -133,11 +146,14 @@ func (w *broadcastWriter) Serve() { continue } else if err != nil { // 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 } else if debug { l.Debugf("sent %d bytes to %s", len(bs), dst) + w.setError(nil) } } } @@ -155,7 +171,7 @@ type broadcastReader struct { port int outbox chan recv conn *net.UDPConn - failed bool + errorHolder } func (r *broadcastReader) Serve() { @@ -167,10 +183,10 @@ func (r *broadcastReader) Serve() { var err error r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port}) 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 } defer r.conn.Close() @@ -179,14 +195,14 @@ func (r *broadcastReader) Serve() { for { n, addr, err := r.conn.ReadFrom(bs) 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 } - r.failed = false + r.setError(nil) if debug { l.Debugf("recv %d bytes from %s", n, addr) diff --git a/lib/beacon/multicast.go b/lib/beacon/multicast.go index 8fb6a785e..030944c96 100644 --- a/lib/beacon/multicast.go +++ b/lib/beacon/multicast.go @@ -8,39 +8,200 @@ package beacon import ( "errors" + "fmt" "net" + "time" + "github.com/thejerf/suture" "golang.org/x/net/ipv6" ) type Multicast struct { - conn *ipv6.PacketConn + *suture.Supervisor addr *net.UDPAddr inbox chan []byte outbox chan recv - intfs []net.Interface + mr *multicastReader + mw *multicastWriter } -func NewMulticast(addr string) (*Multicast, error) { - gaddr, err := net.ResolveUDPAddr("udp6", addr) - if err != nil { - return nil, err +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), } - conn, err := net.ListenPacket("udp6", addr) + 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 { - return nil, err + 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 debug { + l.Debugln(err) + } + r.setError(err) + return + } + + conn, err := net.ListenPacket("udp6", r.addr) + if err != nil { + if debug { + l.Debugln(err) + } + r.setError(err) + return } intfs, err := net.Interfaces() 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 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 err != nil { l.Debugln("IPv6 join", intf.Name, "failed:", err) @@ -52,57 +213,43 @@ func NewMulticast(addr string) (*Multicast, error) { } if joined == 0 { - return nil, errors.New("no multicast interfaces available") + if debug { + l.Debugln("no multicast interfaces available") + } + r.setError(errors.New("no multicast interfaces available")) + return } - b := &Multicast{ - conn: p, - addr: gaddr, - inbox: make(chan []byte), - outbox: make(chan recv, 16), - intfs: intfs, - } + 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) + } - 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, - } - - 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) } diff --git a/lib/config/config.go b/lib/config/config.go index a1f227622..1fe7dcc7c 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -31,6 +31,21 @@ const ( MaxRescanIntervalS = 365 * 24 * 60 * 60 ) +var ( + // DefaultDiscoveryServers should be substituted when the configuration + // contains default. 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 { Version int `xml:"version,attr" json:"version"` Folders []FolderConfiguration `xml:"folder" json:"folders"` @@ -215,7 +230,7 @@ type FolderDeviceConfiguration struct { type OptionsConfiguration struct { 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"` LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"` LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"` @@ -498,17 +513,21 @@ func convertV11V12(cfg *Configuration) { } // 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" { - cfg.Options.GlobalAnnServers[i] = "udp4://announce.syncthing.net:22027" + useDefault = true } 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 if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" { diff --git a/lib/config/config_test.go b/lib/config/config_test.go index 2bffd6f69..ef0215376 100644 --- a/lib/config/config_test.go +++ b/lib/config/config_test.go @@ -32,7 +32,7 @@ func init() { func TestDefaultValues(t *testing.T) { expected := OptionsConfiguration{ 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, LocalAnnEnabled: true, LocalAnnPort: 21027, diff --git a/lib/config/wrapper.go b/lib/config/wrapper.go index 8b054c29a..c862db465 100644 --- a/lib/config/wrapper.go +++ b/lib/config/wrapper.go @@ -317,3 +317,15 @@ func (w *Wrapper) Save() error { events.Default.Log(events.ConfigSaved, w.cfg) 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) +} diff --git a/lib/discover/cache.go b/lib/discover/cache.go new file mode 100644 index 000000000..ade5f2d9d --- /dev/null +++ b/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 +} diff --git a/lib/discover/client.go b/lib/discover/client.go deleted file mode 100644 index a9bee2d56..000000000 --- a/lib/discover/client.go +++ /dev/null @@ -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() -} diff --git a/lib/discover/client_test.go b/lib/discover/client_test.go deleted file mode 100644 index 15897584a..000000000 --- a/lib/discover/client_test.go +++ /dev/null @@ -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() -} diff --git a/lib/discover/client_udp.go b/lib/discover/client_udp.go deleted file mode 100644 index 5cda19bd2..000000000 --- a/lib/discover/client_udp.go +++ /dev/null @@ -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() -} diff --git a/lib/discover/discover.go b/lib/discover/discover.go index fb1ba764a..8f59ec157 100644 --- a/lib/discover/discover.go +++ b/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 // License, v. 2.0. If a copy of the MPL was not distributed with this file, @@ -7,539 +7,48 @@ package discover import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "io" - "net" - "net/url" - "sort" "time" "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 { - Address string - Seen time.Time + 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)? } -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(), - } +// 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) 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() +type FinderMux interface { + Finder + ChildStatus() map[string]error } -func (d *Discoverer) startLocalIPv4Broadcasts(localPort int) { - bb := beacon.NewBroadcast(localPort) - d.beacons = append(d.beacons, bb) - go d.recvAnnouncements(bb) - bb.ServeBackground() +// The RelayStatusProvider answers questions about current relay status. +type RelayStatusProvider interface { + Relays() []string + RelayStatus(uri string) (time.Duration, bool) } -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{} -} - -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 -} - -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[:], - }) -} - -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 -} - -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 } diff --git a/lib/discover/discover_test.go b/lib/discover/discover_test.go deleted file mode 100644 index 75d4429e3..000000000 --- a/lib/discover/discover_test.go +++ /dev/null @@ -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") - } - } -} diff --git a/lib/discover/doc.go b/lib/discover/doc.go index 61686c257..c537ddc51 100644 --- a/lib/discover/doc.go +++ b/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 // 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 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 diff --git a/lib/discover/global.go b/lib/discover/global.go new file mode 100644 index 000000000..964cc4eda --- /dev/null +++ b/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 +} diff --git a/lib/discover/global_test.go b/lib/discover/global_test.go new file mode 100644 index 000000000..b4212c54c --- /dev/null +++ b/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 +} diff --git a/lib/discover/local.go b/lib/discover/local.go new file mode 100644 index 000000000..71b164743 --- /dev/null +++ b/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 +} diff --git a/lib/discover/packets.go b/lib/discover/localpackets.go similarity index 84% rename from lib/discover/packets.go rename to lib/discover/localpackets.go index 482b53f01..f97b91696 100644 --- a/lib/discover/packets.go +++ b/lib/discover/localpackets.go @@ -5,7 +5,7 @@ // 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 genxdr -o packets_xdr.go packets.go +//go:generate genxdr -o localpackets_xdr.go localpackets.go package discover @@ -26,8 +26,8 @@ type Announce struct { } type Relay struct { - Address string // max:256 - Latency int32 + URL string `json:"url"` // max:2083 + Latency int32 `json:"latency"` } type Device struct { diff --git a/lib/discover/packets_xdr.go b/lib/discover/localpackets_xdr.go similarity index 97% rename from lib/discover/packets_xdr.go rename to lib/discover/localpackets_xdr.go index 643e54ab1..b5a032d42 100644 --- a/lib/discover/packets_xdr.go +++ b/lib/discover/localpackets_xdr.go @@ -192,10 +192,10 @@ Relay Structure: 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -| Length of Address | +| Length of URL | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / / -\ Address (variable length) \ +\ URL (variable length) \ / / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Latency | @@ -203,7 +203,7 @@ Relay Structure: struct Relay { - string Address<256>; + string URL<256>; int Latency; } @@ -234,10 +234,10 @@ func (o Relay) AppendXDR(bs []byte) ([]byte, 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)) return xw.Tot(), xw.Error() } @@ -254,7 +254,7 @@ func (o *Relay) UnmarshalXDR(bs []byte) error { } func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error { - o.Address = xr.ReadStringMax(256) + o.URL = xr.ReadStringMax(256) o.Latency = int32(xr.ReadUint32()) return xr.Error() } diff --git a/lib/events/events.go b/lib/events/events.go index ec19b6c5f..21dbcc0ae 100644 --- a/lib/events/events.go +++ b/lib/events/events.go @@ -40,6 +40,7 @@ const ( FolderErrors FolderScanProgress ExternalPortMappingChanged + RelayStateChanged AllEvents = (1 << iota) - 1 ) @@ -90,6 +91,8 @@ func (t EventType) String() string { return "FolderScanProgress" case ExternalPortMappingChanged: return "ExternalPortMappingChanged" + case RelayStateChanged: + return "RelayStateChanged" default: return "Unknown" } diff --git a/lib/ignore/ignore.go b/lib/ignore/ignore.go index 8c8cabf2c..6b518ef62 100644 --- a/lib/ignore/ignore.go +++ b/lib/ignore/ignore.go @@ -12,6 +12,7 @@ import ( "crypto/md5" "fmt" "io" + "log" "os" "path/filepath" "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 "):]) includes, err := loadIgnoreFile(includeFile, seen) if err != nil { + log.Println(err) return err } patterns = append(patterns, includes...) diff --git a/lib/relay/relay.go b/lib/relay/relay.go index c763f9229..184190a03 100644 --- a/lib/relay/relay.go +++ b/lib/relay/relay.go @@ -12,18 +12,23 @@ import ( "net" "net/http" "net/url" + "sort" "time" "github.com/syncthing/relaysrv/client" "github.com/syncthing/relaysrv/protocol" "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/sync" "github.com/thejerf/suture" ) +const ( + eventBroadcasterCheckInterval = 10 * time.Second +) + type Svc struct { *suture.Supervisor cfg *config.Wrapper @@ -71,7 +76,12 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc { stop: make(chan struct{}), } + eventBc := &eventBroadcaster{ + svc: svc, + } + svc.Add(receiver) + svc.Add(eventBc) return svc } @@ -132,7 +142,7 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool { continue } - dynRelays := make([]discover.Relay, 0, len(ann.Relays)) + var dynRelayAddrs []string for _, relayAnn := range ann.Relays { ruri, err := url.Parse(relayAnn.URL) if err != nil { @@ -144,13 +154,11 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool { if debug { 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 { + dynRelayAddrs = relayAddressesSortedByLatency(dynRelayAddrs) closestRelay := dynRelayAddrs[0] if debug { 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 } -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 { // A nil client does not have a status, really. Yet we may be called // this way, for raisins... @@ -201,12 +216,34 @@ func (s *Svc) ClientStatus() map[string]bool { } 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() - return status + + 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() + + if !ok || !client.StatusOK() { + return time.Hour, false + } + + return client.Latency(), true } // Accept returns a new *tls.Conn. The connection is already handshaken. @@ -266,6 +303,55 @@ func (r *invitationReceiver) 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; // {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]} type dynamicAnnouncement struct { @@ -273,3 +359,43 @@ type dynamicAnnouncement struct { 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] +} diff --git a/test/h1/config.xml b/test/h1/config.xml index 833a0324d..47ad52e1d 100644 --- a/test/h1/config.xml +++ b/test/h1/config.xml @@ -46,8 +46,7 @@ tcp://127.0.0.1:22001 - udp4://announce.syncthing.net:22027 - udp6://announce-v6.syncthing.net:22027 + default false true 21025 diff --git a/test/h2/config.xml b/test/h2/config.xml index b2feb2f04..025daceee 100644 --- a/test/h2/config.xml +++ b/test/h2/config.xml @@ -53,8 +53,7 @@ tcp://127.0.0.1:22002 - udp4://announce.syncthing.net:22027 - udp6://announce-v6.syncthing.net:22027 + default true true 21025 diff --git a/test/h3/config.xml b/test/h3/config.xml index ee34a7821..b6a11bca3 100644 --- a/test/h3/config.xml +++ b/test/h3/config.xml @@ -39,8 +39,7 @@ tcp://127.0.0.1:22003 - udp4://announce.syncthing.net:22027 - udp6://announce-v6.syncthing.net:22027 + default false false 21025 diff --git a/test/h4/config.xml b/test/h4/config.xml index 551a42be6..dbcf253ce 100644 --- a/test/h4/config.xml +++ b/test/h4/config.xml @@ -18,8 +18,7 @@ tcp://127.0.0.1:22004 - udp4://announce.syncthing.net:22027 - udp6://announce-v6.syncthing.net:22027 + default false false 21025