diff --git a/cmd/stfinddevice/main.go b/cmd/stfinddevice/main.go index 165c85e7..c0cee884 100644 --- a/cmd/stfinddevice/main.go +++ b/cmd/stfinddevice/main.go @@ -21,11 +21,11 @@ func main() { var server string - flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22026", "Announce server") + flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22027", "Announce server") flag.Parse() if len(flag.Args()) != 1 || server == "" { - log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22026\"] ", os.Args[0]) + log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22027\"] ", os.Args[0]) os.Exit(64) } @@ -35,9 +35,13 @@ func main() { os.Exit(1) } - discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil) + discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil, nil) discoverer.StartGlobal([]string{server}, 1) - for _, addr := range discoverer.Lookup(id) { - log.Println(addr) + addresses, relays := discoverer.Lookup(id) + for _, addr := range addresses { + log.Println("address:", addr) + } + for _, addr := range relays { + log.Println("relay:", addr) } } diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index b0774681..35a383e0 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -11,7 +11,7 @@ import ( "fmt" "io" "net" - "strings" + "net/url" "time" "github.com/syncthing/protocol" @@ -21,6 +21,14 @@ import ( "github.com/thejerf/suture" ) +type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error) +type ListenerFactory func(*url.URL, *tls.Config, chan<- *tls.Conn) + +var ( + dialers = make(map[string]DialerFactory, 0) + listeners = make(map[string]ListenerFactory, 0) +) + // The connection service listens on TLS and dials configured unconnected // devices. Successful connections are handed to the model. type connectionSvc struct { @@ -51,9 +59,9 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model. // // +-----------------+ // Incoming | +---------------+-+ +-----------------+ - // Connections | | | | | Outgoing - // -------------->| | svc.listen | | | Connections - // | | (1 per listen | | svc.connect |--------------> + // Connections | | | | | + // -------------->| | listener | | | Outgoing connections via dialers + // | | (1 per listen | | svc.connect |-----------------------------------> // | | address) | | | // +-+ | | | // +-----------------+ +-----------------+ @@ -79,11 +87,25 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model. svc.Add(serviceFunc(svc.connect)) for _, addr := range svc.cfg.Options().ListenAddress { - addr := addr - listener := serviceFunc(func() { - svc.listen(addr) - }) - svc.Add(listener) + uri, err := url.Parse(addr) + if err != nil { + l.Infoln("Failed to parse listen address:", addr, err) + continue + } + + listener, ok := listeners[uri.Scheme] + if !ok { + l.Infoln("Unknown listen address scheme:", uri.String()) + continue + } + + if debugNet { + l.Debugln("listening on", uri.String()) + } + + svc.Add(serviceFunc(func() { + listener(uri, svc.tlsCfg, svc.conns) + })) } svc.Add(serviceFunc(svc.handle)) @@ -197,46 +219,6 @@ next: } } -func (s *connectionSvc) listen(addr string) { - if debugNet { - l.Debugln("listening on", addr) - } - - tcaddr, err := net.ResolveTCPAddr("tcp", addr) - if err != nil { - l.Fatalln("listen (BEP):", err) - } - listener, err := net.ListenTCP("tcp", tcaddr) - if err != nil { - l.Fatalln("listen (BEP):", err) - } - - for { - conn, err := listener.Accept() - if err != nil { - l.Warnln("Accepting connection:", err) - continue - } - - if debugNet { - l.Debugln("connect from", conn.RemoteAddr()) - } - - tcpConn := conn.(*net.TCPConn) - s.setTCPOptions(tcpConn) - - tc := tls.Server(conn, s.tlsCfg) - err = tc.Handshake() - if err != nil { - l.Infoln("TLS handshake:", err) - tc.Close() - continue - } - - s.conns <- tc - } -} - func (s *connectionSvc) connect() { delay := time.Second for { @@ -254,7 +236,7 @@ func (s *connectionSvc) connect() { for _, addr := range deviceCfg.Addresses { if addr == "dynamic" { if discoverer != nil { - t := discoverer.Lookup(deviceID) + t, _ := discoverer.Lookup(deviceID) if len(t) == 0 { continue } @@ -266,45 +248,30 @@ func (s *connectionSvc) connect() { } for _, addr := range addrs { - host, port, err := net.SplitHostPort(addr) - if err != nil && strings.HasPrefix(err.Error(), "missing port") { - // addr is on the form "1.2.3.4" - addr = net.JoinHostPort(addr, "22000") - } else if err == nil && port == "" { - // addr is on the form "1.2.3.4:" - addr = net.JoinHostPort(host, "22000") + uri, err := url.Parse(addr) + if err != nil { + l.Infoln("Failed to parse connection url:", addr, err) + continue } + + dialer, ok := dialers[uri.Scheme] + if !ok { + l.Infoln("Unknown address schema", uri.String()) + continue + } + if debugNet { - l.Debugln("dial", deviceCfg.DeviceID, addr) + l.Debugln("dial", deviceCfg.DeviceID, uri.String()) } - - raddr, err := net.ResolveTCPAddr("tcp", addr) + conn, err := dialer(uri, s.tlsCfg) if err != nil { if debugNet { - l.Debugln(err) + l.Debugln("dial failed", deviceCfg.DeviceID, uri.String(), err) } continue } - conn, err := net.DialTCP("tcp", nil, raddr) - if err != nil { - if debugNet { - l.Debugln(err) - } - continue - } - - s.setTCPOptions(conn) - - tc := tls.Client(conn, s.tlsCfg) - err = tc.Handshake() - if err != nil { - l.Infoln("TLS handshake:", err) - tc.Close() - continue - } - - s.conns <- tc + s.conns <- conn continue nextDevice } } @@ -317,22 +284,6 @@ func (s *connectionSvc) connect() { } } -func (*connectionSvc) setTCPOptions(conn *net.TCPConn) { - var err error - if err = conn.SetLinger(0); err != nil { - l.Infoln(err) - } - if err = conn.SetNoDelay(false); err != nil { - l.Infoln(err) - } - if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil { - l.Infoln(err) - } - if err = conn.SetKeepAlive(true); err != nil { - l.Infoln(err) - } -} - func (s *connectionSvc) shouldLimit(addr net.Addr) bool { if s.cfg.Options().LimitBandwidthInLan { return true @@ -370,3 +321,19 @@ func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool return true } + +func setTCPOptions(conn *net.TCPConn) { + var err error + if err = conn.SetLinger(0); err != nil { + l.Infoln(err) + } + if err = conn.SetNoDelay(false); err != nil { + l.Infoln(err) + } + if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil { + l.Infoln(err) + } + if err = conn.SetKeepAlive(true); err != nil { + l.Infoln(err) + } +} diff --git a/cmd/syncthing/connections_tcp.go b/cmd/syncthing/connections_tcp.go new file mode 100644 index 00000000..10f2ebed --- /dev/null +++ b/cmd/syncthing/connections_tcp.go @@ -0,0 +1,95 @@ +// 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 main + +import ( + "crypto/tls" + "net" + "net/url" + "strings" +) + +func init() { + dialers["tcp"] = tcpDialer + listeners["tcp"] = tcpListener +} + +func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) { + host, port, err := net.SplitHostPort(uri.Host) + if err != nil && strings.HasPrefix(err.Error(), "missing port") { + // addr is on the form "1.2.3.4" + uri.Host = net.JoinHostPort(uri.Host, "22000") + } else if err == nil && port == "" { + // addr is on the form "1.2.3.4:" + uri.Host = net.JoinHostPort(host, "22000") + } + + raddr, err := net.ResolveTCPAddr("tcp", uri.Host) + if err != nil { + if debugNet { + l.Debugln(err) + } + return nil, err + } + + conn, err := net.DialTCP("tcp", nil, raddr) + if err != nil { + if debugNet { + l.Debugln(err) + } + return nil, err + } + + setTCPOptions(conn) + + tc := tls.Client(conn, tlsCfg) + err = tc.Handshake() + if err != nil { + tc.Close() + return nil, err + } + + return tc, nil +} + +func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- *tls.Conn) { + tcaddr, err := net.ResolveTCPAddr("tcp", uri.Host) + if err != nil { + l.Fatalln("listen (BEP/tcp):", err) + return + } + listener, err := net.ListenTCP("tcp", tcaddr) + if err != nil { + l.Fatalln("listen (BEP/tcp):", err) + return + } + + for { + conn, err := listener.Accept() + if err != nil { + l.Warnln("Accepting connection (BEP/tcp):", err) + continue + } + + if debugNet { + l.Debugln("connect from", conn.RemoteAddr()) + } + + tcpConn := conn.(*net.TCPConn) + setTCPOptions(tcpConn) + + tc := tls.Server(conn, tlsCfg) + err = tc.Handshake() + if err != nil { + l.Infoln("TLS handshake (BEP/tcp):", err) + tc.Close() + continue + } + + conns <- tc + } +} diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 9c2c1ce6..ffa0dc8d 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -658,7 +658,12 @@ func syncthingMain() { // The default port we announce, possibly modified by setupUPnP next. - addr, err := net.ResolveTCPAddr("tcp", opts.ListenAddress[0]) + uri, err := url.Parse(opts.ListenAddress[0]) + if err != nil { + l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], err) + } + + addr, err := net.ResolveTCPAddr("tcp", uri.Host) if err != nil { l.Fatalln("Bad listen address:", err) } @@ -902,7 +907,7 @@ func shutdown() { func discovery(extPort int) *discover.Discoverer { opts := cfg.Options() - disc := discover.NewDiscoverer(myID, opts.ListenAddress) + disc := discover.NewDiscoverer(myID, opts.ListenAddress, opts.RelayServers) if opts.LocalAnnEnabled { l.Infoln("Starting local discovery announcements") diff --git a/gui/syncthing/device/editDeviceModalView.html b/gui/syncthing/device/editDeviceModalView.html index 85cab08c..0606d133 100644 --- a/gui/syncthing/device/editDeviceModalView.html +++ b/gui/syncthing/device/editDeviceModalView.html @@ -32,7 +32,7 @@
-

Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.

+

Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.

diff --git a/lib/auto/gui.files.go b/lib/auto/gui.files.go index 2d0d6202..db2a0fb1 100644 --- a/lib/auto/gui.files.go +++ b/lib/auto/gui.files.go @@ -5,7 +5,7 @@ import ( ) const ( - AssetsBuildDate = "Tue, 18 Aug 2015 11:29:36 GMT" + AssetsBuildDate = "Wed, 19 Aug 2015 19:52:35 GMT" ) func Assets() map[string][]byte { @@ -50,7 +50,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/1xSUY7TMBD95xRWvrISuQASEiQsCxSVKpuF79nGm5g442hst9oijsNJuBgzdtSW/eq892bGb15zAFIWcNiRDuF5IYNBvVW/iseheFPU0Q5ABrB4XeyBiQYC2BW+O4DVuDdXvCq/rxzeSI8X6aT3I4NeM7jTNKdxbQWR1pMAZHCLgzV+TLC6qy+MKh/QBN2rjcGhd7Ns1rL5fgFcJ3x1e39hVMmFSRaeDNMfDa6NTySQ2GJGVfP+TKiyAYQeZGyMTH+KeL7eBCY+85EZ/pSjvwA/p71mPLlq0zK1caQlhvQri6zMfTVhjOwsjeIjM1tHRz0woWo3zX//WBEkkg8xJGuI/3Vtn9GRl6wW6dq5NaolVHWbCApxiGxGlTXBydibrO66F2quIenkqvYb662bkztVrlUSJYI2ep9t+4Pke9R9fjhIkF2kKcM4MXyYiFPP7aexarby748G07tSpL2sdD+ulQ7MUb6X36/+AQAA//8BAAD//5YJ/N+MAgAA") assets["assets/lang/valid-langs.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/yTMvQ6CMBTF8d2nIJ29j2CiEOOgUVNJHIxDgQqVppC2dPDpvacsv38/kpOUL5Kyprso14diV7xE04utaFVmz3/atSbfAtNpRlvgMnQq0ZCh44P7McBnqDpwh4UxkfliaZzoLLkWL64BWHRYnHGaI5Vy7b3m+onkDcVOSEzE/DIyv4Gq69r6Kd6bPwAAAP//AQAA//8bXi5E0gAAAA==") - 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") @@ -85,7 +85,7 @@ func Assets() map[string][]byte { assets["syncthing/core/upgradingDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1zOTarDMAwE4PXLKYQ2WSW5gO0zPCg9gHDcVODaxlIIJc3dm5b+QLfDN8yYSx4pAo8W5zJVGjlNCKKks1jkdMoI7HOySLXmpfNcfQzdXBCUNQaL69oe380WbqCVkkTSsG3omj9TnJFC6Zu7wzV5Pe8cWOCz2pvh4Rz88v8YSAIsxPoifb/j4hozPN+75g4AAP//AQAA///kaeW6xgAAAA==") assets["syncthing/core/validDeviceidDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/5STQW/qMAzH73yKHJBS9Kr0Dnp6l7fjrtyzxG2jhbSznTI08d2XAAMGlDFfKtn+/23/ourQRK9RrTobPRSSNsFw60KjTIcgZxORQlmHYNgNqWHQ3tn/MDgDzspS1DEJXBdEMW2Z+5n42ElyIHDEcJbYJ99icpsLGZrnzoKX5be6d+F1fuZKpuuhFOBXpdDMSKUwjH52YZsj59W010iApGKg1tVcnKwGB+ul9hFuiXO4+jBPgXWcIDy9O8rfMUGOqhJrENR20dsgWewAaYZRwX5NAl7mTseba6iMacnFTYdtQkFwZ5/dM6gGuIjoX3Tq/SNkRYOp7GHAP2f/ypQ98VAUjQGiM1gI1N87+4tX7lOA2OFP3Q/eXut038jxD0P4xbw7rI/jRqvbsVe6mT38EEfu19pLv5PPdjHJxU8AAAD//wEAAP//7ceZ97ADAAA=") assets["syncthing/device/editDeviceModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1yOMQ+CMBCFd35Ft4OElF0mE1dH3Zv2hEuOYo4rxhj+uxUSor7xfe/uPRe7xE7sMIbEWML0jF57ip0NOJNHqAqTZQMJeqU5RzCQnlZ4HoNjqM0t5SMaoykr81rzHwlqkvhlbOakQl4PBo5Q/yDF4c5O8SKc6T6k2YY0f7VXwoftdWDYnyxtsVRt8QYAAP//AQAA//8AoJJL0wAAAA==") - assets["syncthing/device/editDeviceModalView.html"], _ = base64.StdEncoding.DecodeString("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") + assets["syncthing/device/editDeviceModalView.html"], _ = base64.StdEncoding.DecodeString("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") assets["syncthing/device/idqrModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2KPEtJbcFFynCKHenUClCP74/7P8XBwSO7HTHBJjDcszeh0pDjbgSh6hqUyODSToldaMULjLaQ6OoTXXlHGao6kb8/qSnwhqkvhT7OWiQl4PBo7Q/k2K042d4lk4r0Wh2xW68vBC+LCjTgzlfOurremrNwAAAP//AQAA///xwJawxwAAAA==") assets["syncthing/device/idqrModalView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/zSQPXLCMBCFe59iZxug0PgCttPQ0OQOQlqbnegnSAsJo+jukQ00Gj3pvW/e7uCj1Q7Yjsj2mhCyaLnlpsIcEdjEMOI1mWgJQVgcjVjK7kh3NgQnS0F4ZqOFY9jBH0jSITstVCsoKMVuxk/taS8Xzs/c/nCoFcHptDTcgzKCcTG/7lMHMFi+tzedW5Mfcg7WQ2UPQr+ifAwxf+tWYJOmlaCEUyn+cTrWOvQtvVHYLxAWxfOI6xe+kc+EOrtovqCZlFxu/hw0O1z9OZl16P5jxY9vLPZTN/TbvqbuHwAA//8BAAD//2Nevvg4AQAA") assets["syncthing/device/module.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0rMSy/NSSzSy81PKc1J1VAvrsxLLsnIzEvXS0kty0xOVddRiI7VtOYCAAAA//8BAAD//23b1z0oAAAA") diff --git a/lib/config/config.go b/lib/config/config.go index 7abca84e..737fca5a 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -9,6 +9,7 @@ package config import ( "encoding/xml" + "fmt" "io" "math/rand" "os" @@ -26,7 +27,7 @@ import ( const ( OldestHandledVersion = 5 - CurrentVersion = 11 + CurrentVersion = 12 MaxRescanIntervalS = 365 * 24 * 60 * 60 ) @@ -212,12 +213,13 @@ type FolderDeviceConfiguration struct { } type OptionsConfiguration struct { - ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"0.0.0.0:22000"` - GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22026, udp6://announce-v6.syncthing.net:22026"` + 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"` GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"` LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"` LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21025"` LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff32::5222]:21026"` + RelayServers []string `xml:"relayServer" json:"relayServers" default:""` MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"` MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"` ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"` @@ -346,6 +348,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { } } + cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress) + cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers) + if cfg.Version < OldestHandledVersion { l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version) } @@ -369,6 +374,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { if cfg.Version == 10 { convertV10V11(cfg) } + if cfg.Version == 11 { + convertV11V12(cfg) + } // Hash old cleartext passwords if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' { @@ -420,9 +428,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { cfg.Options.ReconnectIntervalS = 5 } - cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress) - cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers) - if cfg.GUI.APIKey == "" { cfg.GUI.APIKey = randomString(32) } @@ -467,6 +472,38 @@ func convertV10V11(cfg *Configuration) { cfg.Version = 11 } +func convertV11V12(cfg *Configuration) { + // Change listen address schema + for i, addr := range cfg.Options.ListenAddress { + if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") { + cfg.Options.ListenAddress[i] = fmt.Sprintf("tcp://%s", addr) + } + } + + for i, device := range cfg.Devices { + for j, addr := range device.Addresses { + if addr != "dynamic" && addr != "" { + cfg.Devices[i].Addresses[j] = fmt.Sprintf("tcp://%s", addr) + } + } + } + + // Use new discovery server + for i, addr := range cfg.Options.GlobalAnnServers { + if addr == "udp4://announce.syncthing.net:22026" { + cfg.Options.GlobalAnnServers[i] = "udp4://announce.syncthing.net:22027" + } 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" + } + } + + cfg.Version = 12 +} + func convertV9V10(cfg *Configuration) { // Enable auto normalization on existing folders. for i := range cfg.Folders { diff --git a/lib/config/config_test.go b/lib/config/config_test.go index 0a8b3942..aae70f43 100644 --- a/lib/config/config_test.go +++ b/lib/config/config_test.go @@ -31,8 +31,8 @@ func init() { func TestDefaultValues(t *testing.T) { expected := OptionsConfiguration{ - ListenAddress: []string{"0.0.0.0:22000"}, - GlobalAnnServers: []string{"udp4://announce.syncthing.net:22026", "udp6://announce-v6.syncthing.net:22026"}, + ListenAddress: []string{"tcp://0.0.0.0:22000"}, + GlobalAnnServers: []string{"udp4://announce.syncthing.net:22027", "udp6://announce-v6.syncthing.net:22027"}, GlobalAnnEnabled: true, LocalAnnEnabled: true, LocalAnnPort: 21025, @@ -100,13 +100,13 @@ func TestDeviceConfig(t *testing.T) { { DeviceID: device1, Name: "node one", - Addresses: []string{"a"}, + Addresses: []string{"tcp://a"}, Compression: protocol.CompressMetadata, }, { DeviceID: device4, Name: "node two", - Addresses: []string{"b"}, + Addresses: []string{"tcp://b"}, Compression: protocol.CompressMetadata, }, } @@ -142,12 +142,13 @@ func TestNoListenAddress(t *testing.T) { func TestOverriddenValues(t *testing.T) { expected := OptionsConfiguration{ - ListenAddress: []string{":23000"}, + ListenAddress: []string{"tcp://:23000"}, GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"}, GlobalAnnEnabled: false, LocalAnnEnabled: false, LocalAnnPort: 42123, LocalAnnMCAddr: "quux:3232", + RelayServers: []string{"relay://123.123.123.123:1234", "relay://125.125.125.125:1255"}, MaxSendKbps: 1234, MaxRecvKbps: 2341, ReconnectIntervalS: 6000, @@ -255,15 +256,15 @@ func TestDeviceAddressesStatic(t *testing.T) { expected := map[protocol.DeviceID]DeviceConfiguration{ device1: { DeviceID: device1, - Addresses: []string{"192.0.2.1", "192.0.2.2"}, + Addresses: []string{"tcp://192.0.2.1", "tcp://192.0.2.2"}, }, device2: { DeviceID: device2, - Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"}, + Addresses: []string{"tcp://192.0.2.3:6070", "tcp://[2001:db8::42]:4242"}, }, device3: { DeviceID: device3, - Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"}, + Addresses: []string{"tcp://[2001:db8::44]:4444", "tcp://192.0.2.4:6090"}, }, device4: { DeviceID: device4, @@ -330,12 +331,12 @@ func TestIssue1750(t *testing.T) { t.Fatal(err) } - if cfg.Options().ListenAddress[0] != ":23000" { - t.Errorf("%q != %q", cfg.Options().ListenAddress[0], ":23000") + if cfg.Options().ListenAddress[0] != "tcp://:23000" { + t.Errorf("%q != %q", cfg.Options().ListenAddress[0], "tcp://:23000") } - if cfg.Options().ListenAddress[1] != ":23001" { - t.Errorf("%q != %q", cfg.Options().ListenAddress[1], ":23001") + if cfg.Options().ListenAddress[1] != "tcp://:23001" { + t.Errorf("%q != %q", cfg.Options().ListenAddress[1], "tcp://:23001") } if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" { diff --git a/lib/config/testdata/overridenvalues.xml b/lib/config/testdata/overridenvalues.xml index a67ec9a2..7ab01d06 100755 --- a/lib/config/testdata/overridenvalues.xml +++ b/lib/config/testdata/overridenvalues.xml @@ -7,6 +7,8 @@ false 42123 quux:3232 + relay://123.123.123.123:1234 + relay://125.125.125.125:1255 32 1234 2341 diff --git a/lib/config/testdata/v12.xml b/lib/config/testdata/v12.xml new file mode 100644 index 00000000..0e8ebdc1 --- /dev/null +++ b/lib/config/testdata/v12.xml @@ -0,0 +1,13 @@ + + + + + 1 + + +
tcp://a
+
+ +
tcp://b
+
+
diff --git a/lib/discover/client.go b/lib/discover/client.go index 9b47d8a7..64f43d8c 100644 --- a/lib/discover/client.go +++ b/lib/discover/client.go @@ -43,7 +43,7 @@ func New(addr string, pkt *Announce) (Client, error) { } type Client interface { - Lookup(device protocol.DeviceID) []string + 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 index a8271b46..9c359276 100644 --- a/lib/discover/client_test.go +++ b/lib/discover/client_test.go @@ -37,10 +37,8 @@ func TestUDP4Success(t *testing.T) { Magic: AnnouncementMagic, This: Device{ device[:], - []Address{{ - IP: net.IPv4(123, 123, 123, 123), - Port: 1234, - }}, + []string{"tcp://123.123.123.123:1234"}, + nil, }, } @@ -101,7 +99,12 @@ func TestUDP4Success(t *testing.T) { wg := sync.NewWaitGroup() wg.Add(1) go func() { - addrs = client.Lookup(device) + pkt, err := client.Lookup(device) + if err == nil { + for _, addr := range pkt.This.Addresses { + addrs = append(addrs, addr) + } + } wg.Done() }() @@ -117,7 +120,7 @@ func TestUDP4Success(t *testing.T) { // Wait for the lookup to arrive, verify that the number of answers is correct wg.Wait() - if len(addrs) != 1 || addrs[0] != "123.123.123.123:1234" { + if len(addrs) != 1 || addrs[0] != "tcp://123.123.123.123:1234" { t.Fatal("Wrong number of answers") } @@ -138,10 +141,8 @@ func TestUDP4Failure(t *testing.T) { Magic: AnnouncementMagic, This: Device{ device[:], - []Address{{ - IP: net.IPv4(123, 123, 123, 123), - Port: 1234, - }}, + []string{"tcp://123.123.123.123:1234"}, + nil, }, } @@ -197,7 +198,12 @@ func TestUDP4Failure(t *testing.T) { wg := sync.NewWaitGroup() wg.Add(1) go func() { - addrs = client.Lookup(device) + pkt, err := client.Lookup(device) + if err == nil { + for _, addr := range pkt.This.Addresses { + addrs = append(addrs, addr) + } + } wg.Done() }() diff --git a/lib/discover/client_udp.go b/lib/discover/client_udp.go index 2da76781..8283e595 100644 --- a/lib/discover/client_udp.go +++ b/lib/discover/client_udp.go @@ -137,11 +137,13 @@ func (d *UDPClient) broadcast(pkt []byte) { time.Sleep(1 * time.Second) - res := d.Lookup(d.id) - if debug { - l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, res) + pkt, err := d.Lookup(d.id) + 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, pkt.This.Addresses) } - ok = len(res) > 0 + ok = len(pkt.This.Addresses) > 0 } d.mut.Lock() @@ -157,13 +159,13 @@ func (d *UDPClient) broadcast(pkt []byte) { } } -func (d *UDPClient) Lookup(device protocol.DeviceID) []string { +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 nil + return Announce{}, err } conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP) @@ -171,7 +173,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string { if debug { l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err) } - return nil + return Announce{}, err } defer conn.Close() @@ -180,7 +182,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string { if debug { l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err) } - return nil + return Announce{}, err } buf := Query{QueryMagic, device[:]}.MustMarshalXDR() @@ -189,7 +191,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string { if debug { l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err) } - return nil + return Announce{}, err } buf = make([]byte, 2048) @@ -197,12 +199,12 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string { if err != nil { if err, ok := err.(net.Error); ok && err.Timeout() { // Expected if the server doesn't know about requested device ID - return nil + return Announce{}, err } if debug { l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err) } - return nil + return Announce{}, err } var pkt Announce @@ -211,18 +213,13 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string { if debug { l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n])) } - return nil + return Announce{}, err } - var addrs []string - for _, a := range pkt.This.Addresses { - deviceAddr := net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port))) - addrs = append(addrs, deviceAddr) - } if debug { - l.Debugf("discover %s: Lookup(%s) result: %v", d.url, device, addrs) + l.Debugf("discover %s: Lookup(%s) result: %v relays: %v", d.url, device, pkt.This.Addresses, pkt.This.Relays) } - return addrs + return pkt, nil } func (d *UDPClient) Stop() { diff --git a/lib/discover/discover.go b/lib/discover/discover.go index 87502a4d..df6359e4 100644 --- a/lib/discover/discover.go +++ b/lib/discover/discover.go @@ -10,21 +10,25 @@ import ( "bytes" "encoding/hex" "errors" + "fmt" "io" "net" + "net/url" "runtime" - "strconv" + "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" ) type Discoverer struct { myID protocol.DeviceID listenAddrs []string + relays []Relay localBcastIntv time.Duration localBcastStart time.Time cacheLifetime time.Duration @@ -34,9 +38,10 @@ type Discoverer struct { localBcastTick <-chan time.Time forcedBcastTick chan time.Time - registryLock sync.RWMutex - registry map[protocol.DeviceID][]CacheEntry - lastLookup map[protocol.DeviceID]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 @@ -51,17 +56,19 @@ var ( ErrIncorrectMagic = errors.New("incorrect magic number") ) -func NewDiscoverer(id protocol.DeviceID, addresses []string) *Discoverer { +func NewDiscoverer(id protocol.DeviceID, addresses []string, relayAdresses []string) *Discoverer { return &Discoverer{ - myID: id, - listenAddrs: addresses, - localBcastIntv: 30 * time.Second, - cacheLifetime: 5 * time.Minute, - negCacheCutoff: 3 * time.Minute, - registry: make(map[protocol.DeviceID][]CacheEntry), - lastLookup: make(map[protocol.DeviceID]time.Time), - registryLock: sync.NewRWMutex(), - mut: sync.NewRWMutex(), + myID: id, + listenAddrs: addresses, + relays: measureLatency(relayAdresses), + 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(), } } @@ -184,75 +191,108 @@ func (d *Discoverer) ExtAnnounceOK() map[string]bool { return ret } -func (d *Discoverer) Lookup(device protocol.DeviceID) []string { +// 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() - cached := d.filterCached(d.registry[device]) + 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() - if len(cached) > 0 { + 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(cached)) - for i := range cached { - addrs[i] = cached[i].Address + addrs := make([]string, len(cachedAddresses)) + for i := range cachedAddresses { + addrs[i] = cachedAddresses[i].Address } - return addrs + 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 + 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 []string, len(d.clients)) + 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() - results <- c.Lookup(device) + ann, err := c.Lookup(device) + if err == nil { + results <- ann + } + }(client) } wg.Wait() close(results) - cached := []CacheEntry{} - seen := make(map[string]struct{}) + 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 { - _, ok := seen[addr] + for _, addr := range result.This.Addresses { + _, ok := seenAddresses[addr] if !ok { - cached = append(cached, CacheEntry{ + cachedAddresses = append(cachedAddresses, CacheEntry{ Address: addr, Seen: now, }) - seen[addr] = struct{}{} + 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 = addressesSortedByLatency(availableRelays) + cachedRelays := make([]CacheEntry, len(relays)) + for i := range relays { + cachedRelays[i] = CacheEntry{ + Address: relays[i], + Seen: now, + } } d.registryLock.Lock() - d.registry[device] = cached + d.addressRegistry[device] = cachedAddresses + d.relayRegistry[device] = cachedRelays d.lastLookup[device] = time.Now() d.registryLock.Unlock() - return addrs + return addrs, relays } - return nil + return nil, relays } func (d *Discoverer) Hint(device string, addrs []string) { @@ -267,8 +307,8 @@ func (d *Discoverer) Hint(device string, addrs []string) { func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry { d.registryLock.RLock() - devices := make(map[protocol.DeviceID][]CacheEntry, len(d.registry)) - for device, addrs := range d.registry { + 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 @@ -278,30 +318,38 @@ func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry { } func (d *Discoverer) announcementPkt() *Announce { - var addrs []Address + var addrs []string if d.extPort != 0 { - addrs = []Address{{Port: d.extPort}} + addrs = []string{fmt.Sprintf("tcp://:%d", d.extPort)} } else { - for _, astr := range d.listenAddrs { - addr, err := net.ResolveTCPAddr("tcp", astr) + for _, aurl := range d.listenAddrs { + uri, err := url.Parse(aurl) if err != nil { - l.Warnln("discover: %v: not announcing %s", err, astr) + if debug { + l.Debugf("discovery: failed to parse listen address %s: %s", aurl, err) + } + continue + } + addr, err := net.ResolveTCPAddr("tcp", uri.Host) + if err != nil { + l.Warnln("discover: %v: not announcing %s", err, aurl) continue } else if debug { - l.Debugf("discover: resolved %s as %#v", astr, addr) + l.Debugf("discover: resolved %s as %#v", aurl, uri.Host) } if len(addr.IP) == 0 || addr.IP.IsUnspecified() { - addrs = append(addrs, Address{Port: uint16(addr.Port)}) + uri.Host = fmt.Sprintf(":%d", addr.Port) } else if bs := addr.IP.To4(); bs != nil { - addrs = append(addrs, Address{IP: bs, Port: uint16(addr.Port)}) + uri.Host = fmt.Sprintf("%s:%d", bs.String(), addr.Port) } else if bs := addr.IP.To16(); bs != nil { - addrs = append(addrs, Address{IP: bs, Port: uint16(addr.Port)}) + uri.Host = fmt.Sprintf("[%s]:%d", bs.String(), addr.Port) } + addrs = append(addrs, uri.String()) } } return &Announce{ Magic: AnnouncementMagic, - This: Device{d.myID[:], addrs}, + This: Device{d.myID[:], addrs, d.relays}, } } @@ -310,7 +358,7 @@ func (d *Discoverer) sendLocalAnnouncements() { var pkt = Announce{ Magic: AnnouncementMagic, - This: Device{d.myID[:], addrs}, + This: Device{d.myID[:], addrs, d.relays}, } msg := pkt.MustMarshalXDR() @@ -363,19 +411,32 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool { d.registryLock.Lock() defer d.registryLock.Unlock() - current := d.filterCached(d.registry[id]) + current := d.filterCached(d.addressRegistry[id]) orig := current - for _, a := range device.Addresses { - var deviceAddr string - if len(a.IP) > 0 { - deviceAddr = net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port))) - } else if addr != nil { - ua := addr.(*net.UDPAddr) - ua.Port = int(a.Port) - deviceAddr = ua.String() + 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() @@ -393,7 +454,7 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool { l.Debugf("discover: Caching %s addresses: %v", id, current) } - d.registry[id] = current + d.addressRegistry[id] = current if len(current) > len(orig) { addrs := make([]string, len(current)) @@ -413,7 +474,7 @@ 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 address %s - seen %v ago", c[i].Address, ago) + 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] @@ -424,30 +485,99 @@ func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry { return c } -func addrToAddr(addr *net.TCPAddr) Address { +func addrToAddr(addr *net.TCPAddr) string { if len(addr.IP) == 0 || addr.IP.IsUnspecified() { - return Address{Port: uint16(addr.Port)} + return fmt.Sprintf(":%d", addr.Port) } else if bs := addr.IP.To4(); bs != nil { - return Address{IP: bs, Port: uint16(addr.Port)} + return fmt.Sprintf("%s:%d", bs.String(), addr.Port) } else if bs := addr.IP.To16(); bs != nil { - return Address{IP: bs, Port: uint16(addr.Port)} + return fmt.Sprintf("[%s]:%d", bs.String(), addr.Port) } - return Address{} + return "" } -func resolveAddrs(addrs []string) []Address { - var raddrs []Address +func resolveAddrs(addrs []string) []string { + var raddrs []string for _, addrStr := range addrs { - addrRes, err := net.ResolveTCPAddr("tcp", addrStr) + 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.IP) > 0 { - raddrs = append(raddrs, addr) - } else { - raddrs = append(raddrs, Address{Port: addr.Port}) + 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 := 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 +} + +// addressesSortedByLatency adds local latency to the relay, and sorts them +// by sum latency, and returns the addresses. +func addressesSortedByLatency(input []Relay) []string { + relays := make([]Relay, len(input)) + copy(relays, input) + for i, relay := range relays { + if latency, err := 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 +} + +func getLatencyForURL(addr string) (time.Duration, error) { + uri, err := url.Parse(addr) + if err != nil { + return 0, err + } + + return osutil.TCPPing(uri.Host) +} + +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] +} diff --git a/lib/discover/discover_test.go b/lib/discover/discover_test.go index c4975684..a6469ea1 100644 --- a/lib/discover/discover_test.go +++ b/lib/discover/discover_test.go @@ -18,15 +18,15 @@ import ( type DummyClient struct { url *url.URL lookups []protocol.DeviceID - lookupRet []string + lookupRet Announce stops int statusRet bool statusChecks int } -func (c *DummyClient) Lookup(device protocol.DeviceID) []string { +func (c *DummyClient) Lookup(device protocol.DeviceID) (Announce, error) { c.lookups = append(c.lookups, device) - return c.lookupRet + return c.lookupRet, nil } func (c *DummyClient) StatusOK() bool { @@ -45,17 +45,41 @@ func (c *DummyClient) Address() string { func TestGlobalDiscovery(t *testing.T) { c1 := &DummyClient{ statusRet: false, - lookupRet: []string{"test.com:1234"}, + lookupRet: Announce{ + Magic: AnnouncementMagic, + This: Device{ + ID: protocol.LocalDeviceID[:], + Addresses: []string{"test.com:1234"}, + Relays: nil, + }, + Extra: nil, + }, } c2 := &DummyClient{ statusRet: true, - lookupRet: []string{}, + lookupRet: Announce{ + Magic: AnnouncementMagic, + This: Device{ + ID: protocol.LocalDeviceID[:], + Addresses: nil, + Relays: nil, + }, + Extra: nil, + }, } c3 := &DummyClient{ statusRet: true, - lookupRet: []string{"best.com:2345"}, + lookupRet: Announce{ + Magic: AnnouncementMagic, + This: Device{ + ID: protocol.LocalDeviceID[:], + Addresses: []string{"best.com:2345"}, + Relays: nil, + }, + Extra: nil, + }, } clients := []*DummyClient{c1, c2} @@ -72,7 +96,7 @@ func TestGlobalDiscovery(t *testing.T) { return c3, nil }) - d := NewDiscoverer(device, []string{}) + d := NewDiscoverer(device, []string{}, nil) d.localBcastStart = time.Time{} servers := []string{ "test1://123.123.123.123:1234", @@ -93,7 +117,7 @@ func TestGlobalDiscovery(t *testing.T) { } } - addrs := d.Lookup(device) + addrs, _ := d.Lookup(device) if len(addrs) != 2 { t.Fatal("Wrong number of addresses", addrs) } @@ -117,7 +141,7 @@ func TestGlobalDiscovery(t *testing.T) { } } - addrs = d.Lookup(device) + addrs, _ = d.Lookup(device) if len(addrs) != 2 { t.Fatal("Wrong number of addresses", addrs) } diff --git a/lib/discover/packets.go b/lib/discover/packets.go index eed22c77..482b53f0 100644 --- a/lib/discover/packets.go +++ b/lib/discover/packets.go @@ -10,8 +10,8 @@ package discover const ( - AnnouncementMagic = 0x9D79BC39 - QueryMagic = 0x2CA856F5 + AnnouncementMagic = 0x9D79BC40 + QueryMagic = 0x2CA856F6 ) type Query struct { @@ -25,12 +25,13 @@ type Announce struct { Extra []Device // max:16 } -type Device struct { - ID []byte // max:32 - Addresses []Address // max:16 +type Relay struct { + Address string // max:256 + Latency int32 } -type Address struct { - IP []byte // max:16 - Port uint16 +type Device struct { + ID []byte // max:32 + Addresses []string // max:16 + Relays []Relay // max:16 } diff --git a/lib/discover/packets_xdr.go b/lib/discover/packets_xdr.go index 04863cbf..643e54ab 100644 --- a/lib/discover/packets_xdr.go +++ b/lib/discover/packets_xdr.go @@ -187,6 +187,80 @@ func (o *Announce) DecodeXDRFrom(xr *xdr.Reader) error { /* +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 | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ Address (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Latency | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct Relay { + string Address<256>; + int Latency; +} + +*/ + +func (o Relay) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o Relay) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o Relay) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o Relay) AppendXDR(bs []byte) ([]byte, error) { + var aw = xdr.AppendWriter(bs) + var xw = xdr.NewWriter(&aw) + _, err := o.EncodeXDRInto(xw) + return []byte(aw), err +} + +func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) { + if l := len(o.Address); l > 256 { + return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 256) + } + xw.WriteString(o.Address) + xw.WriteUint32(uint32(o.Latency)) + return xw.Tot(), xw.Error() +} + +func (o *Relay) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *Relay) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error { + o.Address = xr.ReadStringMax(256) + o.Latency = int32(xr.ReadUint32()) + return xr.Error() +} + +/* + Device Structure: 0 1 2 3 @@ -200,15 +274,24 @@ Device Structure: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Number of Addresses | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Length of Addresses | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / / -\ Zero or more Address Structures \ +\ Addresses (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Number of Relays | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ Zero or more Relay Structures \ / / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ struct Device { opaque ID<32>; - Address Addresses<16>; + string Addresses<16>; + Relay Relays<16>; } */ @@ -247,7 +330,14 @@ func (o Device) EncodeXDRInto(xw *xdr.Writer) (int, error) { } xw.WriteUint32(uint32(len(o.Addresses))) for i := range o.Addresses { - _, err := o.Addresses[i].EncodeXDRInto(xw) + xw.WriteString(o.Addresses[i]) + } + if l := len(o.Relays); l > 16 { + return xw.Tot(), xdr.ElementSizeExceeded("Relays", l, 16) + } + xw.WriteUint32(uint32(len(o.Relays))) + for i := range o.Relays { + _, err := o.Relays[i].EncodeXDRInto(xw) if err != nil { return xw.Tot(), err } @@ -275,83 +365,20 @@ func (o *Device) DecodeXDRFrom(xr *xdr.Reader) error { if _AddressesSize > 16 { return xdr.ElementSizeExceeded("Addresses", _AddressesSize, 16) } - o.Addresses = make([]Address, _AddressesSize) + o.Addresses = make([]string, _AddressesSize) for i := range o.Addresses { - (&o.Addresses[i]).DecodeXDRFrom(xr) + o.Addresses[i] = xr.ReadString() + } + _RelaysSize := int(xr.ReadUint32()) + if _RelaysSize < 0 { + return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16) + } + if _RelaysSize > 16 { + return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16) + } + o.Relays = make([]Relay, _RelaysSize) + for i := range o.Relays { + (&o.Relays[i]).DecodeXDRFrom(xr) } return xr.Error() } - -/* - -Address 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 IP | -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/ / -\ IP (variable length) \ -/ / -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -| 0x0000 | Port | -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -struct Address { - opaque IP<16>; - unsigned int Port; -} - -*/ - -func (o Address) EncodeXDR(w io.Writer) (int, error) { - var xw = xdr.NewWriter(w) - return o.EncodeXDRInto(xw) -} - -func (o Address) MarshalXDR() ([]byte, error) { - return o.AppendXDR(make([]byte, 0, 128)) -} - -func (o Address) MustMarshalXDR() []byte { - bs, err := o.MarshalXDR() - if err != nil { - panic(err) - } - return bs -} - -func (o Address) AppendXDR(bs []byte) ([]byte, error) { - var aw = xdr.AppendWriter(bs) - var xw = xdr.NewWriter(&aw) - _, err := o.EncodeXDRInto(xw) - return []byte(aw), err -} - -func (o Address) EncodeXDRInto(xw *xdr.Writer) (int, error) { - if l := len(o.IP); l > 16 { - return xw.Tot(), xdr.ElementSizeExceeded("IP", l, 16) - } - xw.WriteBytes(o.IP) - xw.WriteUint16(o.Port) - return xw.Tot(), xw.Error() -} - -func (o *Address) DecodeXDR(r io.Reader) error { - xr := xdr.NewReader(r) - return o.DecodeXDRFrom(xr) -} - -func (o *Address) UnmarshalXDR(bs []byte) error { - var br = bytes.NewReader(bs) - var xr = xdr.NewReader(br) - return o.DecodeXDRFrom(xr) -} - -func (o *Address) DecodeXDRFrom(xr *xdr.Reader) error { - o.IP = xr.ReadBytesMax(16) - o.Port = xr.ReadUint16() - return xr.Error() -} diff --git a/lib/osutil/ping.go b/lib/osutil/ping.go new file mode 100644 index 00000000..d02bf2d6 --- /dev/null +++ b/lib/osutil/ping.go @@ -0,0 +1,27 @@ +// 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 osutil + +import ( + "net" + "time" +) + +// TCPPing returns the duration required to establish a TCP connection +// to the given host. ICMP packets require root priviledges, hence why we use +// tcp. +func TCPPing(address string) (time.Duration, error) { + dialer := net.Dialer{ + Deadline: time.Now().Add(time.Second), + } + start := time.Now() + conn, err := dialer.Dial("tcp", address) + if conn != nil { + conn.Close() + } + return time.Since(start), err +}