From 34c04babbe2b361af9dfd843d0664fd4d062c40f Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Tue, 23 Jun 2015 13:55:30 +0100 Subject: [PATCH 01/11] Large refactoring/feature commit 1. Change listen addresses to URIs 2. Break out connectionSvc to support listeners and dialers based on schema 3. Add relay announcement and lookups part of discovery service --- cmd/stfinddevice/main.go | 14 +- cmd/syncthing/connections.go | 159 ++++------- cmd/syncthing/connections_tcp.go | 95 +++++++ cmd/syncthing/main.go | 9 +- gui/syncthing/device/editDeviceModalView.html | 2 +- lib/auto/gui.files.go | 6 +- lib/config/config.go | 49 +++- lib/config/config_test.go | 25 +- lib/config/testdata/overridenvalues.xml | 2 + lib/config/testdata/v12.xml | 13 + lib/discover/client.go | 2 +- lib/discover/client_test.go | 28 +- lib/discover/client_udp.go | 35 ++- lib/discover/discover.go | 268 +++++++++++++----- lib/discover/discover_test.go | 42 ++- lib/discover/packets.go | 17 +- lib/discover/packets_xdr.go | 185 ++++++------ lib/osutil/ping.go | 27 ++ 18 files changed, 657 insertions(+), 321 deletions(-) create mode 100644 cmd/syncthing/connections_tcp.go create mode 100644 lib/config/testdata/v12.xml create mode 100644 lib/osutil/ping.go diff --git a/cmd/stfinddevice/main.go b/cmd/stfinddevice/main.go index 165c85e71..c0cee884b 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 b0774681e..35a383e00 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 000000000..10f2ebed8 --- /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 9c2c1ce60..ffa0dc8da 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 85cab08c1..0606d1337 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 2d0d62024..db2a0fb17 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 7abca84ef..737fca5a1 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 0a8b39423..aae70f439 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 a67ec9a2b..7ab01d06a 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 000000000..0e8ebdc16 --- /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 9b47d8a7e..64f43d8cf 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 a8271b46a..9c3592760 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 2da767810..8283e595d 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 87502a4d3..df6359e46 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 c49756842..a6469ea1a 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 eed22c77e..482b53f01 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 04863cbf8..643e54ab1 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 000000000..d02bf2d65 --- /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 +} From bb876eac8280a3e03e2ed93bf60fd34854f2d1d3 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Sun, 28 Jun 2015 16:05:29 +0100 Subject: [PATCH 02/11] Connections have types --- cmd/syncthing/connections.go | 74 ++++++++++++++++++++------------ cmd/syncthing/connections_tcp.go | 8 +++- lib/model/connection.go | 36 ++++++++++++++++ lib/model/model.go | 56 +++++++++++------------- lib/model/model_test.go | 19 +++++++- 5 files changed, 133 insertions(+), 60 deletions(-) create mode 100644 lib/model/connection.go diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index 35a383e00..161e06a71 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -12,6 +12,7 @@ import ( "io" "net" "net/url" + "sync" "time" "github.com/syncthing/protocol" @@ -22,7 +23,7 @@ import ( ) type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error) -type ListenerFactory func(*url.URL, *tls.Config, chan<- *tls.Conn) +type ListenerFactory func(*url.URL, *tls.Config, chan<- intermediateConnection) var ( dialers = make(map[string]DialerFactory, 0) @@ -37,17 +38,27 @@ type connectionSvc struct { myID protocol.DeviceID model *model.Model tlsCfg *tls.Config - conns chan *tls.Conn + conns chan intermediateConnection + + mut sync.RWMutex + connType map[protocol.DeviceID]model.ConnectionType } -func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.Model, tlsCfg *tls.Config) *connectionSvc { +type intermediateConnection struct { + conn *tls.Conn + connType model.ConnectionType +} + +func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config) *connectionSvc { svc := &connectionSvc{ Supervisor: suture.NewSimple("connectionSvc"), cfg: cfg, myID: myID, - model: model, + model: mdl, tlsCfg: tlsCfg, - conns: make(chan *tls.Conn), + conns: make(chan intermediateConnection), + + connType: make(map[protocol.DeviceID]model.ConnectionType), } // There are several moving parts here; one routine per listening address @@ -114,15 +125,15 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model. func (s *connectionSvc) handle() { next: - for conn := range s.conns { - cs := conn.ConnectionState() + for c := range s.conns { + cs := c.conn.ConnectionState() // We should have negotiated the next level protocol "bep/1.0" as part // of the TLS handshake. Unfortunately this can't be a hard error, // because there are implementations out there that don't support // protocol negotiation (iOS for one...). if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != bepProtocolName { - l.Infof("Peer %s did not negotiate bep/1.0", conn.RemoteAddr()) + l.Infof("Peer %s did not negotiate bep/1.0", c.conn.RemoteAddr()) } // We should have received exactly one certificate from the other @@ -130,8 +141,8 @@ next: // connection. certs := cs.PeerCertificates if cl := len(certs); cl != 1 { - l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, conn.RemoteAddr()) - conn.Close() + l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.conn.RemoteAddr()) + c.conn.Close() continue } remoteCert := certs[0] @@ -142,7 +153,7 @@ next: // clients between the same NAT gateway, and global discovery. if remoteID == myID { l.Infof("Connected to myself (%s) - should not happen", remoteID) - conn.Close() + c.conn.Close() continue } @@ -154,7 +165,7 @@ next: // connections still established... if s.model.ConnectedTo(remoteID) { l.Infof("Connected to already connected device (%s)", remoteID) - conn.Close() + c.conn.Close() continue } @@ -172,35 +183,42 @@ next: // Incorrect certificate name is something the user most // likely wants to know about, since it's an advanced // config. Warn instead of Info. - l.Warnf("Bad certificate from %s (%v): %v", remoteID, conn.RemoteAddr(), err) - conn.Close() + l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.conn.RemoteAddr(), err) + c.conn.Close() continue next } // If rate limiting is set, and based on the address we should // limit the connection, then we wrap it in a limiter. - limit := s.shouldLimit(conn.RemoteAddr()) + limit := s.shouldLimit(c.conn.RemoteAddr()) - wr := io.Writer(conn) + wr := io.Writer(c.conn) if limit && writeRateLimit != nil { - wr = &limitedWriter{conn, writeRateLimit} + wr = &limitedWriter{c.conn, writeRateLimit} } - rd := io.Reader(conn) + rd := io.Reader(c.conn) if limit && readRateLimit != nil { - rd = &limitedReader{conn, readRateLimit} + rd = &limitedReader{c.conn, readRateLimit} } - name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr()) + name := fmt.Sprintf("%s-%s (%s)", c.conn.LocalAddr(), c.conn.RemoteAddr(), c.connType) protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression) l.Infof("Established secure connection to %s at %s", remoteID, name) if debugNet { - l.Debugf("cipher suite: %04X in lan: %t", conn.ConnectionState().CipherSuite, !limit) + l.Debugf("cipher suite: %04X in lan: %t", c.conn.ConnectionState().CipherSuite, !limit) } - s.model.AddConnection(conn, protoConn) + s.model.AddConnection(model.Connection{ + c.conn, + protoConn, + c.connType, + }) + s.mut.Lock() + s.connType[remoteID] = c.connType + s.mut.Unlock() continue next } } @@ -208,14 +226,14 @@ next: if !s.cfg.IgnoredDevice(remoteID) { events.Default.Log(events.DeviceRejected, map[string]string{ "device": remoteID.String(), - "address": conn.RemoteAddr().String(), + "address": c.conn.RemoteAddr().String(), }) - l.Infof("Connection from %s with unknown device ID %s", conn.RemoteAddr(), remoteID) + l.Infof("Connection from %s (%s) with unknown device ID %s", c.conn.RemoteAddr(), c.connType, remoteID) } else { - l.Infof("Connection from %s with ignored device ID %s", conn.RemoteAddr(), remoteID) + l.Infof("Connection from %s (%s) with ignored device ID %s", c.conn.RemoteAddr(), c.connType, remoteID) } - conn.Close() + c.conn.Close() } } @@ -271,7 +289,9 @@ func (s *connectionSvc) connect() { continue } - s.conns <- conn + s.conns <- intermediateConnection{ + conn, model.ConnectionTypeBasicDial, + } continue nextDevice } } diff --git a/cmd/syncthing/connections_tcp.go b/cmd/syncthing/connections_tcp.go index 10f2ebed8..d2d73fd71 100644 --- a/cmd/syncthing/connections_tcp.go +++ b/cmd/syncthing/connections_tcp.go @@ -11,6 +11,8 @@ import ( "net" "net/url" "strings" + + "github.com/syncthing/syncthing/lib/model" ) func init() { @@ -56,7 +58,7 @@ func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) { return tc, nil } -func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- *tls.Conn) { +func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- intermediateConnection) { tcaddr, err := net.ResolveTCPAddr("tcp", uri.Host) if err != nil { l.Fatalln("listen (BEP/tcp):", err) @@ -90,6 +92,8 @@ func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- *tls.Conn) { continue } - conns <- tc + conns <- intermediateConnection{ + tc, model.ConnectionTypeBasicAccept, + } } } diff --git a/lib/model/connection.go b/lib/model/connection.go new file mode 100644 index 000000000..6489e015c --- /dev/null +++ b/lib/model/connection.go @@ -0,0 +1,36 @@ +// 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 model + +import ( + "net" + + "github.com/syncthing/protocol" +) + +type Connection struct { + net.Conn + protocol.Connection + Type ConnectionType +} + +const ( + ConnectionTypeBasicAccept ConnectionType = iota + ConnectionTypeBasicDial +) + +type ConnectionType int + +func (t ConnectionType) String() string { + switch t { + case ConnectionTypeBasicAccept: + return "basic-accept" + case ConnectionTypeBasicDial: + return "basic-dial" + } + return "unknown" +} diff --git a/lib/model/model.go b/lib/model/model.go index 2e97321b0..a276913c6 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -87,10 +87,9 @@ type Model struct { folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef fmut sync.RWMutex // protects the above - protoConn map[protocol.DeviceID]protocol.Connection - rawConn map[protocol.DeviceID]io.Closer + conn map[protocol.DeviceID]Connection deviceVer map[protocol.DeviceID]string - pmut sync.RWMutex // protects protoConn and rawConn + pmut sync.RWMutex // protects conn and deviceVer reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist rvmut sync.RWMutex // protects reqValidationCache @@ -130,8 +129,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName, folderIgnores: make(map[string]*ignore.Matcher), folderRunners: make(map[string]service), folderStatRefs: make(map[string]*stats.FolderStatisticsReference), - protoConn: make(map[protocol.DeviceID]protocol.Connection), - rawConn: make(map[protocol.DeviceID]io.Closer), + conn: make(map[protocol.DeviceID]Connection), deviceVer: make(map[protocol.DeviceID]string), reqValidationCache: make(map[string]time.Time), @@ -243,14 +241,14 @@ func (m *Model) ConnectionStats() map[string]interface{} { m.fmut.RLock() var res = make(map[string]interface{}) - conns := make(map[string]ConnectionInfo, len(m.protoConn)) - for device, conn := range m.protoConn { + conns := make(map[string]ConnectionInfo, len(m.conn)) + for device, conn := range m.conn { ci := ConnectionInfo{ Statistics: conn.Statistics(), ClientVersion: m.deviceVer[device], } - if nc, ok := m.rawConn[device].(remoteAddrer); ok { - ci.Address = nc.RemoteAddr().String() + if addr := m.conn[device].RemoteAddr(); addr != nil { + ci.Address = addr.String() } conns[device.String()] = ci @@ -586,8 +584,11 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon "clientVersion": cm.ClientVersion, } - if conn, ok := m.rawConn[deviceID].(*tls.Conn); ok { - event["addr"] = conn.RemoteAddr().String() + if conn, ok := m.conn[deviceID]; ok { + addr := conn.RemoteAddr() + if addr != nil { + event["addr"] = addr.String() + } } m.pmut.Unlock() @@ -693,12 +694,11 @@ func (m *Model) Close(device protocol.DeviceID, err error) { } m.fmut.RUnlock() - conn, ok := m.rawConn[device] + conn, ok := m.conn[device] if ok { closeRawConn(conn) } - delete(m.protoConn, device) - delete(m.rawConn, device) + delete(m.conn, device) delete(m.deviceVer, device) m.pmut.Unlock() } @@ -860,7 +860,7 @@ func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) { // ConnectedTo returns true if we are connected to the named device. func (m *Model) ConnectedTo(deviceID protocol.DeviceID) bool { m.pmut.RLock() - _, ok := m.protoConn[deviceID] + _, ok := m.conn[deviceID] m.pmut.RUnlock() if ok { m.deviceWasSeen(deviceID) @@ -927,28 +927,24 @@ func (m *Model) SetIgnores(folder string, content []string) error { // AddConnection adds a new peer connection to the model. An initial index will // be sent to the connected peer, thereafter index updates whenever the local // folder changes. -func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection) { - deviceID := protoConn.ID() +func (m *Model) AddConnection(conn Connection) { + deviceID := conn.ID() m.pmut.Lock() - if _, ok := m.protoConn[deviceID]; ok { + if _, ok := m.conn[deviceID]; ok { panic("add existing device") } - m.protoConn[deviceID] = protoConn - if _, ok := m.rawConn[deviceID]; ok { - panic("add existing device") - } - m.rawConn[deviceID] = rawConn + m.conn[deviceID] = conn - protoConn.Start() + conn.Start() cm := m.clusterConfig(deviceID) - protoConn.ClusterConfig(cm) + conn.ClusterConfig(cm) m.fmut.RLock() for _, folder := range m.deviceFolders[deviceID] { fs := m.folderFiles[folder] - go sendIndexes(protoConn, folder, fs, m.folderIgnores[folder]) + go sendIndexes(conn, folder, fs, m.folderIgnores[folder]) } m.fmut.RUnlock() m.pmut.Unlock() @@ -1114,7 +1110,7 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) { func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, flags uint32, options []protocol.Option) ([]byte, error) { m.pmut.RLock() - nc, ok := m.protoConn[deviceID] + nc, ok := m.conn[deviceID] m.pmut.RUnlock() if !ok { @@ -1640,7 +1636,7 @@ func (m *Model) Availability(folder, file string) []protocol.DeviceID { availableDevices := []protocol.DeviceID{} for _, device := range fs.Availability(file) { - _, ok := m.protoConn[device] + _, ok := m.conn[device] if ok { availableDevices = append(availableDevices, device) } @@ -1764,7 +1760,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool { // folder. m.pmut.Lock() for _, dev := range cfg.DeviceIDs() { - if conn, ok := m.rawConn[dev]; ok { + if conn, ok := m.conn[dev]; ok { closeRawConn(conn) } } @@ -1812,7 +1808,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool { // disconnect it so that we start sharing the folder with it. // We close the underlying connection and let the normal error // handling kick in to clean up and reconnect. - if conn, ok := m.rawConn[dev]; ok { + if conn, ok := m.conn[dev]; ok { closeRawConn(conn) } diff --git a/lib/model/model_test.go b/lib/model/model_test.go index 724302195..08f3889a9 100644 --- a/lib/model/model_test.go +++ b/lib/model/model_test.go @@ -12,6 +12,7 @@ import ( "fmt" "io/ioutil" "math/rand" + "net" "os" "path/filepath" "strconv" @@ -281,7 +282,11 @@ func BenchmarkRequest(b *testing.B) { id: device1, requestData: []byte("some data to return"), } - m.AddConnection(fc, fc) + m.AddConnection(Connection{ + &net.TCPConn{}, + fc, + ConnectionTypeBasicAccept, + }) m.Index(device1, "default", files, 0, nil) b.ResetTimer() @@ -314,6 +319,18 @@ func TestDeviceRename(t *testing.T) { db, _ := leveldb.Open(storage.NewMemStorage(), nil) m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", db) + + fc := FakeConnection{ + id: device1, + requestData: []byte("some data to return"), + } + + m.AddConnection(Connection{ + &net.TCPConn{}, + fc, + ConnectionTypeBasicAccept, + }) + m.ServeBackground() if cfg.Devices()[device1].Name != "" { t.Errorf("Device already has a name") From c2ccab4361ef8a8f76df6754e2de86f687b64628 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Sun, 28 Jun 2015 20:09:10 +0100 Subject: [PATCH 03/11] Add unsubscribe to config --- lib/config/wrapper.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/config/wrapper.go b/lib/config/wrapper.go index 84799728e..8b054c29a 100644 --- a/lib/config/wrapper.go +++ b/lib/config/wrapper.go @@ -114,6 +114,21 @@ func (w *Wrapper) Subscribe(c Committer) { w.sMut.Unlock() } +// Unsubscribe de-registers the given handler from any future calls to +// configuration changes +func (w *Wrapper) Unsubscribe(c Committer) { + w.sMut.Lock() + for i := range w.subs { + if w.subs[i] == c { + copy(w.subs[i:], w.subs[i+1:]) + w.subs[len(w.subs)-1] = nil + w.subs = w.subs[:len(w.subs)-1] + break + } + } + w.sMut.Unlock() +} + // Raw returns the currently wrapped Configuration object. func (w *Wrapper) Raw() Configuration { return w.cfg From 27465353c19380e3f8ecb9c8158534820df01943 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Sun, 28 Jun 2015 20:09:53 +0100 Subject: [PATCH 04/11] Add incoming connection relay service --- cmd/syncthing/connections.go | 32 +----- cmd/syncthing/main.go | 11 +- cmd/syncthing/relays.go | 193 +++++++++++++++++++++++++++++++++++ lib/model/connection.go | 10 ++ 4 files changed, 215 insertions(+), 31 deletions(-) create mode 100644 cmd/syncthing/relays.go diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index 161e06a71..60098eccb 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -60,36 +60,14 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Mo connType: make(map[protocol.DeviceID]model.ConnectionType), } + cfg.Subscribe(svc) // There are several moving parts here; one routine per listening address // to handle incoming connections, one routine to periodically attempt - // outgoing connections, and lastly one routine to the the common handling - // regardless of whether the connection was incoming or outgoing. It ends - // up as in the diagram below. We embed a Supervisor to manage the - // routines (i.e. log and restart if they crash or exit, etc). - // - // +-----------------+ - // Incoming | +---------------+-+ +-----------------+ - // Connections | | | | | - // -------------->| | listener | | | Outgoing connections via dialers - // | | (1 per listen | | svc.connect |-----------------------------------> - // | | address) | | | - // +-+ | | | - // +-----------------+ +-----------------+ - // v v - // | | - // | | - // +------------+-----------+ - // | - // | svc.conns - // v - // +-----------------+ - // | | - // | | - // | svc.handle |------> model.AddConnection() - // | | - // | | - // +-----------------+ + // outgoing connections, one routine to the the common handling + // regardless of whether the connection was incoming or outgoing. + // Furthermore, a relay service which handles incoming requests to connect + // via the relays. // // TODO: Clean shutdown, and/or handling config changes on the fly. We // partly do this now - new devices and addresses will be picked up, but diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index ffa0dc8da..15e355679 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -668,6 +668,13 @@ func syncthingMain() { l.Fatalln("Bad listen address:", err) } + // Start the relevant services + + connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg) + relaySvc := newRelaySvc(cfg, tlsCfg, connectionSvc.conns) + connectionSvc.Add(relaySvc) + mainSvc.Add(connectionSvc) + // Start discovery localPort := addr.Port @@ -681,10 +688,6 @@ func syncthingMain() { mainSvc.Add(upnpSvc) } - connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg) - cfg.Subscribe(connectionSvc) - mainSvc.Add(connectionSvc) - if cpuProfile { f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid())) if err != nil { diff --git a/cmd/syncthing/relays.go b/cmd/syncthing/relays.go new file mode 100644 index 000000000..af3da4bdd --- /dev/null +++ b/cmd/syncthing/relays.go @@ -0,0 +1,193 @@ +// 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" + "time" + + "github.com/syncthing/relaysrv/client" + "github.com/syncthing/relaysrv/protocol" + "github.com/syncthing/syncthing/lib/config" + "github.com/syncthing/syncthing/lib/model" + "github.com/syncthing/syncthing/lib/sync" + + "github.com/thejerf/suture" +) + +func newRelaySvc(cfg *config.Wrapper, tlsCfg *tls.Config, conns chan<- intermediateConnection) *relaySvc { + svc := &relaySvc{ + Supervisor: suture.New("relaySvc", suture.Spec{ + Log: func(log string) { + if debugNet { + l.Infoln(log) + } + }, + FailureBackoff: 5 * time.Minute, + FailureDecay: float64((10 * time.Minute) / time.Second), + FailureThreshold: 5, + }), + cfg: cfg, + tlsCfg: tlsCfg, + + tokens: make(map[string]suture.ServiceToken), + clients: make(map[string]*client.ProtocolClient), + mut: sync.NewRWMutex(), + + invitations: make(chan protocol.SessionInvitation), + } + + rcfg := cfg.Raw() + svc.CommitConfiguration(rcfg, rcfg) + cfg.Subscribe(svc) + + receiver := &invitationReceiver{ + tlsCfg: tlsCfg, + conns: conns, + invitations: svc.invitations, + } + + svc.receiverToken = svc.Add(receiver) + + return svc +} + +type relaySvc struct { + *suture.Supervisor + cfg *config.Wrapper + tlsCfg *tls.Config + + receiverToken suture.ServiceToken + tokens map[string]suture.ServiceToken + clients map[string]*client.ProtocolClient + mut sync.RWMutex + invitations chan protocol.SessionInvitation +} + +func (s *relaySvc) VerifyConfiguration(from, to config.Configuration) error { + for _, addr := range to.Options.RelayServers { + _, err := url.Parse(addr) + if err != nil { + return err + } + } + return nil +} + +func (s *relaySvc) CommitConfiguration(from, to config.Configuration) bool { + existing := make(map[string]struct{}, len(to.Options.RelayServers)) + for _, addr := range to.Options.RelayServers { + uri, err := url.Parse(addr) + if err != nil { + if debugNet { + l.Debugln("Failed to parse relay address", addr, err) + } + continue + } + + existing[uri.String()] = struct{}{} + + _, ok := s.tokens[uri.String()] + if !ok { + if debugNet { + l.Debugln("Connecting to relay", uri) + } + c := client.NewProtocolClient(uri, s.tlsCfg.Certificates, s.invitations) + s.tokens[uri.String()] = s.Add(c) + s.mut.Lock() + s.clients[uri.String()] = c + s.mut.Unlock() + } + } + + for uri, token := range s.tokens { + _, ok := existing[uri] + if !ok { + err := s.Remove(token) + delete(s.tokens, uri) + s.mut.Lock() + delete(s.clients, uri) + s.mut.Unlock() + if debugNet { + l.Debugln("Disconnecting from relay", uri, err) + } + } + } + + return true +} + +func (s *relaySvc) 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() + } + s.mut.RUnlock() + return status +} + +type invitationReceiver struct { + invitations chan protocol.SessionInvitation + tlsCfg *tls.Config + conns chan<- intermediateConnection + stop chan struct{} +} + +func (r *invitationReceiver) Serve() { + if r.stop != nil { + return + } + r.stop = make(chan struct{}) + + for { + select { + case inv := <-r.invitations: + if debugNet { + l.Debugln("Received relay invitation", inv) + } + conn, err := client.JoinSession(inv) + if err != nil { + if debugNet { + l.Debugf("Failed to join relay session %s: %v", inv, err) + } + continue + } + + setTCPOptions(conn.(*net.TCPConn)) + + var tc *tls.Conn + + if inv.ServerSocket { + tc = tls.Server(conn, r.tlsCfg) + } else { + tc = tls.Client(conn, r.tlsCfg) + } + err = tc.Handshake() + if err != nil { + l.Infof("TLS handshake (BEP/relay %s): %v", inv, err) + tc.Close() + continue + } + r.conns <- intermediateConnection{ + tc, model.ConnectionTypeRelayAccept, + } + case <-r.stop: + return + } + } +} + +func (r *invitationReceiver) Stop() { + if r.stop == nil { + return + } + r.stop <- struct{}{} + r.stop = nil +} diff --git a/lib/model/connection.go b/lib/model/connection.go index 6489e015c..9fb79923d 100644 --- a/lib/model/connection.go +++ b/lib/model/connection.go @@ -21,6 +21,8 @@ type Connection struct { const ( ConnectionTypeBasicAccept ConnectionType = iota ConnectionTypeBasicDial + ConnectionTypeRelayAccept + ConnectionTypeRelayDial ) type ConnectionType int @@ -31,6 +33,14 @@ func (t ConnectionType) String() string { return "basic-accept" case ConnectionTypeBasicDial: return "basic-dial" + case ConnectionTypeRelayAccept: + return "relay-accept" + case ConnectionTypeRelayDial: + return "relay-dial" } return "unknown" } + +func (t ConnectionType) IsDirect() bool { + return t == ConnectionTypeBasicAccept || t == ConnectionTypeBasicDial +} From a388fb0bb762f07a017836f96a3bcbe9535f8f02 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Sun, 28 Jun 2015 21:09:03 +0100 Subject: [PATCH 05/11] Check relays for available devices --- cmd/syncthing/connections.go | 115 +++++++++++++++++++++--- lib/config/config.go | 1 + lib/config/config_test.go | 2 + lib/config/testdata/overridenvalues.xml | 1 + 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index 60098eccb..3c997a96c 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -16,9 +16,12 @@ import ( "time" "github.com/syncthing/protocol" + + "github.com/syncthing/relaysrv/client" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/model" + "github.com/thejerf/suture" ) @@ -40,6 +43,8 @@ type connectionSvc struct { tlsCfg *tls.Config conns chan intermediateConnection + lastRelayCheck map[protocol.DeviceID]time.Time + mut sync.RWMutex connType map[protocol.DeviceID]model.ConnectionType } @@ -58,7 +63,8 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Mo tlsCfg: tlsCfg, conns: make(chan intermediateConnection), - connType: make(map[protocol.DeviceID]model.ConnectionType), + connType: make(map[protocol.DeviceID]model.ConnectionType), + lastRelayCheck: make(map[protocol.DeviceID]time.Time), } cfg.Subscribe(svc) @@ -135,13 +141,23 @@ next: continue } - // We should not already be connected to the other party. TODO: This - // could use some better handling. If the old connection is dead but - // hasn't timed out yet we may want to drop *that* connection and keep - // this one. But in case we are two devices connecting to each other - // in parallel we don't want to do that or we end up with no - // connections still established... - if s.model.ConnectedTo(remoteID) { + // If we have a relay connection, and the new incoming connection is + // not a relay connection, we should drop that, and prefer the this one. + s.mut.RLock() + ct, ok := s.connType[remoteID] + s.mut.RUnlock() + if ok && !ct.IsDirect() && c.connType.IsDirect() { + if debugNet { + l.Debugln("Switching connections", remoteID) + } + s.model.Close(remoteID, fmt.Errorf("switching connections")) + } else if s.model.ConnectedTo(remoteID) { + // We should not already be connected to the other party. TODO: This + // could use some better handling. If the old connection is dead but + // hasn't timed out yet we may want to drop *that* connection and keep + // this one. But in case we are two devices connecting to each other + // in parallel we don't want to do that or we end up with no + // connections still established... l.Infof("Connected to already connected device (%s)", remoteID) c.conn.Close() continue @@ -224,15 +240,22 @@ func (s *connectionSvc) connect() { continue } - if s.model.ConnectedTo(deviceID) { + connected := s.model.ConnectedTo(deviceID) + + s.mut.RLock() + ct, ok := s.connType[deviceID] + s.mut.RUnlock() + if connected && ok && ct.IsDirect() { continue } var addrs []string + var relays []string for _, addr := range deviceCfg.Addresses { if addr == "dynamic" { if discoverer != nil { - t, _ := discoverer.Lookup(deviceID) + t, r := discoverer.Lookup(deviceID) + relays = append(relays, r...) if len(t) == 0 { continue } @@ -267,11 +290,83 @@ func (s *connectionSvc) connect() { continue } + if connected { + s.model.Close(deviceID, fmt.Errorf("switching connections")) + } + s.conns <- intermediateConnection{ conn, model.ConnectionTypeBasicDial, } continue nextDevice } + + // Only connect via relays if not already connected + // Also, do not set lastRelayCheck time if we have no relays, + // as otherwise when we do discover relays, we might have to + // wait up to RelayReconnectIntervalM to connect again. + if connected || len(relays) == 0 { + continue nextDevice + } + + reconIntv := time.Duration(s.cfg.Options().RelayReconnectIntervalM) * time.Minute + if last, ok := s.lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv { + if debugNet { + l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last) + } + continue nextDevice + } else if debugNet { + l.Debugln("Trying relay connections to", deviceID, relays) + } + + s.lastRelayCheck[deviceID] = time.Now() + + for _, addr := range relays { + uri, err := url.Parse(addr) + if err != nil { + l.Infoln("Failed to parse relay connection url:", addr, err) + continue + } + + inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates) + if err != nil { + if debugNet { + l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err) + } + continue + } else if debugNet { + l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri) + } + + conn, err := client.JoinSession(inv) + if err != nil { + if debugNet { + l.Debugf("Failed to join relay session %s: %v", inv, err) + } + continue + } else if debugNet { + l.Debugln("Sucessfully joined relay session", inv) + } + + setTCPOptions(conn.(*net.TCPConn)) + + var tc *tls.Conn + + if inv.ServerSocket { + tc = tls.Server(conn, s.tlsCfg) + } else { + tc = tls.Client(conn, s.tlsCfg) + } + err = tc.Handshake() + if err != nil { + l.Infof("TLS handshake (BEP/relay %s): %v", inv, err) + tc.Close() + continue + } + s.conns <- intermediateConnection{ + tc, model.ConnectionTypeRelayDial, + } + continue nextDevice + } } time.Sleep(delay) diff --git a/lib/config/config.go b/lib/config/config.go index 737fca5a1..394a2bfb3 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -223,6 +223,7 @@ type OptionsConfiguration struct { MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"` MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"` ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"` + RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"` StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"` UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"` UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"` diff --git a/lib/config/config_test.go b/lib/config/config_test.go index aae70f439..a9c245bce 100644 --- a/lib/config/config_test.go +++ b/lib/config/config_test.go @@ -40,6 +40,7 @@ func TestDefaultValues(t *testing.T) { MaxSendKbps: 0, MaxRecvKbps: 0, ReconnectIntervalS: 60, + RelayReconnectIntervalM: 10, StartBrowser: true, UPnPEnabled: true, UPnPLeaseM: 60, @@ -152,6 +153,7 @@ func TestOverriddenValues(t *testing.T) { MaxSendKbps: 1234, MaxRecvKbps: 2341, ReconnectIntervalS: 6000, + RelayReconnectIntervalM: 20, StartBrowser: false, UPnPEnabled: false, UPnPLeaseM: 90, diff --git a/lib/config/testdata/overridenvalues.xml b/lib/config/testdata/overridenvalues.xml index 7ab01d06a..c6d026fd1 100755 --- a/lib/config/testdata/overridenvalues.xml +++ b/lib/config/testdata/overridenvalues.xml @@ -13,6 +13,7 @@ 1234 2341 6000 + 20 false false 90 From 2c0f8dc546e92892db53e5568a0424955286ce9d Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Fri, 17 Jul 2015 20:23:00 +0100 Subject: [PATCH 06/11] Add dependencies (fixes #1364) --- Godeps/Godeps.json | 8 + .../syncthing/relaysrv/client/client.go | 280 +++++++++ .../syncthing/relaysrv/client/debug.go | 15 + .../syncthing/relaysrv/client/methods.go | 117 ++++ .../syncthing/relaysrv/protocol/packets.go | 65 ++ .../relaysrv/protocol/packets_xdr.go | 567 ++++++++++++++++++ .../syncthing/relaysrv/protocol/protocol.go | 114 ++++ 7 files changed, 1166 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go create mode 100644 Godeps/_workspace/src/github.com/syncthing/relaysrv/client/debug.go create mode 100644 Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go create mode 100644 Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go create mode 100644 Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets_xdr.go create mode 100644 Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/protocol.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 65aa24ad3..b22318ee2 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -41,6 +41,14 @@ "ImportPath": "github.com/syncthing/protocol", "Rev": "ebcdea63c07327a342f65415bbadc497462b8f1f" }, + { + "ImportPath": "github.com/syncthing/relaysrv/client", + "Rev": "7c6a31017968e7c1a69148db1ca3dea71eba8236" + }, + { + "ImportPath": "github.com/syncthing/relaysrv/protocol", + "Rev": "7c6a31017968e7c1a69148db1ca3dea71eba8236" + }, { "ImportPath": "github.com/syndtr/goleveldb/leveldb", "Rev": "b743d92d3215f11c9b5ce8830fafe1f16786adf4" diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go new file mode 100644 index 000000000..94e4eedd2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/client.go @@ -0,0 +1,280 @@ +// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file). + +package client + +import ( + "crypto/tls" + "fmt" + "log" + "net" + "net/url" + "time" + + syncthingprotocol "github.com/syncthing/protocol" + "github.com/syncthing/relaysrv/protocol" + "github.com/syncthing/syncthing/lib/sync" +) + +type ProtocolClient struct { + URI *url.URL + Invitations chan protocol.SessionInvitation + + closeInvitationsOnFinish bool + + config *tls.Config + + timeout time.Duration + + stop chan struct{} + stopped chan struct{} + + conn *tls.Conn + + mut sync.RWMutex + connected bool +} + +func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient { + closeInvitationsOnFinish := false + if invitations == nil { + closeInvitationsOnFinish = true + invitations = make(chan protocol.SessionInvitation) + } + + return &ProtocolClient{ + URI: uri, + Invitations: invitations, + + closeInvitationsOnFinish: closeInvitationsOnFinish, + + config: configForCerts(certs), + + timeout: time.Minute * 2, + + stop: make(chan struct{}), + stopped: make(chan struct{}), + + mut: sync.NewRWMutex(), + connected: false, + } +} + +func (c *ProtocolClient) Serve() { + c.stop = make(chan struct{}) + c.stopped = make(chan struct{}) + defer close(c.stopped) + + if err := c.connect(); err != nil { + if debug { + l.Debugln("Relay connect:", err) + } + return + } + + if debug { + l.Debugln(c, "connected", c.conn.RemoteAddr()) + } + + if err := c.join(); err != nil { + c.conn.Close() + l.Infoln("Relay join:", err) + return + } + + if err := c.conn.SetDeadline(time.Time{}); err != nil { + l.Infoln("Relay set deadline:", err) + return + } + + if debug { + l.Debugln(c, "joined", c.conn.RemoteAddr(), "via", c.conn.LocalAddr()) + } + + defer c.cleanup() + c.mut.Lock() + c.connected = true + c.mut.Unlock() + + messages := make(chan interface{}) + errors := make(chan error, 1) + + go messageReader(c.conn, messages, errors) + + timeout := time.NewTimer(c.timeout) + + for { + select { + case message := <-messages: + timeout.Reset(c.timeout) + if debug { + log.Printf("%s received message %T", c, message) + } + + switch msg := message.(type) { + case protocol.Ping: + if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil { + l.Infoln("Relay write:", err) + return + + } + if debug { + l.Debugln(c, "sent pong") + } + + case protocol.SessionInvitation: + ip := net.IP(msg.Address) + if len(ip) == 0 || ip.IsUnspecified() { + msg.Address = c.conn.RemoteAddr().(*net.TCPAddr).IP[:] + } + c.Invitations <- msg + + default: + l.Infoln("Relay: protocol error: unexpected message %v", msg) + return + } + + case <-c.stop: + if debug { + l.Debugln(c, "stopping") + } + return + + case err := <-errors: + l.Infoln("Relay received:", err) + return + + case <-timeout.C: + if debug { + l.Debugln(c, "timed out") + } + return + } + } +} + +func (c *ProtocolClient) Stop() { + if c.stop == nil { + return + } + + close(c.stop) + <-c.stopped +} + +func (c *ProtocolClient) StatusOK() bool { + c.mut.RLock() + con := c.connected + c.mut.RUnlock() + return con +} + +func (c *ProtocolClient) String() string { + return fmt.Sprintf("ProtocolClient@%p", c) +} + +func (c *ProtocolClient) connect() error { + if c.URI.Scheme != "relay" { + return fmt.Errorf("Unsupported relay schema:", c.URI.Scheme) + } + + conn, err := tls.Dial("tcp", c.URI.Host, c.config) + if err != nil { + return err + } + + if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil { + conn.Close() + return err + } + + if err := performHandshakeAndValidation(conn, c.URI); err != nil { + conn.Close() + return err + } + + c.conn = conn + return nil +} + +func (c *ProtocolClient) cleanup() { + if c.closeInvitationsOnFinish { + close(c.Invitations) + c.Invitations = make(chan protocol.SessionInvitation) + } + + if debug { + l.Debugln(c, "cleaning up") + } + + c.mut.Lock() + c.connected = false + c.mut.Unlock() + + c.conn.Close() +} + +func (c *ProtocolClient) join() error { + if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil { + return err + } + + message, err := protocol.ReadMessage(c.conn) + if err != nil { + return err + } + + switch msg := message.(type) { + case protocol.Response: + if msg.Code != 0 { + return fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message) + } + + default: + return fmt.Errorf("protocol error: expecting response got %v", msg) + } + + return nil +} + +func performHandshakeAndValidation(conn *tls.Conn, uri *url.URL) error { + if err := conn.Handshake(); err != nil { + return err + } + + cs := conn.ConnectionState() + if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != protocol.ProtocolName { + return fmt.Errorf("protocol negotiation error") + } + + q := uri.Query() + relayIDs := q.Get("id") + if relayIDs != "" { + relayID, err := syncthingprotocol.DeviceIDFromString(relayIDs) + if err != nil { + return fmt.Errorf("relay address contains invalid verification id: %s", err) + } + + certs := cs.PeerCertificates + if cl := len(certs); cl != 1 { + return fmt.Errorf("unexpected certificate count: %d", cl) + } + + remoteID := syncthingprotocol.NewDeviceID(certs[0].Raw) + if remoteID != relayID { + return fmt.Errorf("relay id does not match. Expected %v got %v", relayID, remoteID) + } + } + + return nil +} + +func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) { + for { + msg, err := protocol.ReadMessage(conn) + if err != nil { + errors <- err + return + } + messages <- msg + } +} diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/debug.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/debug.go new file mode 100644 index 000000000..935e9fe62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/debug.go @@ -0,0 +1,15 @@ +// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file). + +package client + +import ( + "os" + "strings" + + "github.com/calmh/logger" +) + +var ( + debug = strings.Contains(os.Getenv("STTRACE"), "relay") || os.Getenv("STTRACE") == "all" + l = logger.DefaultLogger +) diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go new file mode 100644 index 000000000..ef6145e9c --- /dev/null +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/client/methods.go @@ -0,0 +1,117 @@ +// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file). + +package client + +import ( + "crypto/tls" + "fmt" + "net" + "net/url" + "strconv" + "time" + + syncthingprotocol "github.com/syncthing/protocol" + "github.com/syncthing/relaysrv/protocol" +) + +func GetInvitationFromRelay(uri *url.URL, id syncthingprotocol.DeviceID, certs []tls.Certificate) (protocol.SessionInvitation, error) { + if uri.Scheme != "relay" { + return protocol.SessionInvitation{}, fmt.Errorf("Unsupported relay scheme:", uri.Scheme) + } + + conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs)) + conn.SetDeadline(time.Now().Add(10 * time.Second)) + if err != nil { + return protocol.SessionInvitation{}, err + } + + if err := performHandshakeAndValidation(conn, uri); err != nil { + return protocol.SessionInvitation{}, err + } + + defer conn.Close() + + request := protocol.ConnectRequest{ + ID: id[:], + } + + if err := protocol.WriteMessage(conn, request); err != nil { + return protocol.SessionInvitation{}, err + } + + message, err := protocol.ReadMessage(conn) + if err != nil { + return protocol.SessionInvitation{}, err + } + + switch msg := message.(type) { + case protocol.Response: + return protocol.SessionInvitation{}, fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message) + case protocol.SessionInvitation: + if debug { + l.Debugln("Received invitation", msg, "via", conn.LocalAddr()) + } + ip := net.IP(msg.Address) + if len(ip) == 0 || ip.IsUnspecified() { + msg.Address = conn.RemoteAddr().(*net.TCPAddr).IP[:] + } + return msg, nil + default: + return protocol.SessionInvitation{}, fmt.Errorf("protocol error: unexpected message %v", msg) + } +} + +func JoinSession(invitation protocol.SessionInvitation) (net.Conn, error) { + addr := net.JoinHostPort(net.IP(invitation.Address).String(), strconv.Itoa(int(invitation.Port))) + + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + + request := protocol.JoinSessionRequest{ + Key: invitation.Key, + } + + conn.SetDeadline(time.Now().Add(10 * time.Second)) + err = protocol.WriteMessage(conn, request) + if err != nil { + return nil, err + } + + message, err := protocol.ReadMessage(conn) + if err != nil { + return nil, err + } + + conn.SetDeadline(time.Time{}) + + switch msg := message.(type) { + case protocol.Response: + if msg.Code != 0 { + return nil, fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message) + } + return conn, nil + default: + return nil, fmt.Errorf("protocol error: expecting response got %v", msg) + } +} + +func configForCerts(certs []tls.Certificate) *tls.Config { + return &tls.Config{ + Certificates: certs, + NextProtos: []string{protocol.ProtocolName}, + ClientAuth: tls.RequestClientCert, + SessionTicketsDisabled: true, + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + }, + } +} diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go new file mode 100644 index 000000000..7ff020115 --- /dev/null +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets.go @@ -0,0 +1,65 @@ +// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file). + +//go:generate -command genxdr go run ../../syncthing/Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go +//go:generate genxdr -o packets_xdr.go packets.go + +package protocol + +import ( + "fmt" + syncthingprotocol "github.com/syncthing/protocol" + "net" +) + +const ( + messageTypePing int32 = iota + messageTypePong + messageTypeJoinRelayRequest + messageTypeJoinSessionRequest + messageTypeResponse + messageTypeConnectRequest + messageTypeSessionInvitation +) + +type header struct { + magic uint32 + messageType int32 + messageLength int32 +} + +type Ping struct{} +type Pong struct{} +type JoinRelayRequest struct{} + +type JoinSessionRequest struct { + Key []byte // max:32 +} + +type Response struct { + Code int32 + Message string +} + +type ConnectRequest struct { + ID []byte // max:32 +} + +type SessionInvitation struct { + From []byte // max:32 + Key []byte // max:32 + Address []byte // max:32 + Port uint16 + ServerSocket bool +} + +func (i SessionInvitation) String() string { + return fmt.Sprintf("%s@%s", syncthingprotocol.DeviceIDFromBytes(i.From), i.AddressString()) +} + +func (i SessionInvitation) GoString() string { + return i.String() +} + +func (i SessionInvitation) AddressString() string { + return fmt.Sprintf("%s:%d", net.IP(i.Address), i.Port) +} diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets_xdr.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets_xdr.go new file mode 100644 index 000000000..f18e18c18 --- /dev/null +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/packets_xdr.go @@ -0,0 +1,567 @@ +// ************************************************************ +// This file is automatically generated by genxdr. Do not edit. +// ************************************************************ + +package protocol + +import ( + "bytes" + "io" + + "github.com/calmh/xdr" +) + +/* + +header 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 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| magic | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| message Type | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| message Length | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct header { + unsigned int magic; + int messageType; + int messageLength; +} + +*/ + +func (o header) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o header) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o header) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o header) 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 header) EncodeXDRInto(xw *xdr.Writer) (int, error) { + xw.WriteUint32(o.magic) + xw.WriteUint32(uint32(o.messageType)) + xw.WriteUint32(uint32(o.messageLength)) + return xw.Tot(), xw.Error() +} + +func (o *header) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *header) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *header) DecodeXDRFrom(xr *xdr.Reader) error { + o.magic = xr.ReadUint32() + o.messageType = int32(xr.ReadUint32()) + o.messageLength = int32(xr.ReadUint32()) + return xr.Error() +} + +/* + +Ping 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 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct Ping { +} + +*/ + +func (o Ping) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o Ping) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o Ping) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o Ping) 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 Ping) EncodeXDRInto(xw *xdr.Writer) (int, error) { + return xw.Tot(), xw.Error() +} + +func (o *Ping) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *Ping) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *Ping) DecodeXDRFrom(xr *xdr.Reader) error { + return xr.Error() +} + +/* + +Pong 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 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct Pong { +} + +*/ + +func (o Pong) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o Pong) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o Pong) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o Pong) 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 Pong) EncodeXDRInto(xw *xdr.Writer) (int, error) { + return xw.Tot(), xw.Error() +} + +func (o *Pong) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *Pong) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *Pong) DecodeXDRFrom(xr *xdr.Reader) error { + return xr.Error() +} + +/* + +JoinRelayRequest 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 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct JoinRelayRequest { +} + +*/ + +func (o JoinRelayRequest) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o JoinRelayRequest) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o JoinRelayRequest) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o JoinRelayRequest) 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 JoinRelayRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) { + return xw.Tot(), xw.Error() +} + +func (o *JoinRelayRequest) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *JoinRelayRequest) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *JoinRelayRequest) DecodeXDRFrom(xr *xdr.Reader) error { + return xr.Error() +} + +/* + +JoinSessionRequest 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 Key | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ Key (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct JoinSessionRequest { + opaque Key<32>; +} + +*/ + +func (o JoinSessionRequest) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o JoinSessionRequest) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o JoinSessionRequest) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o JoinSessionRequest) 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 JoinSessionRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) { + if l := len(o.Key); l > 32 { + return xw.Tot(), xdr.ElementSizeExceeded("Key", l, 32) + } + xw.WriteBytes(o.Key) + return xw.Tot(), xw.Error() +} + +func (o *JoinSessionRequest) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *JoinSessionRequest) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *JoinSessionRequest) DecodeXDRFrom(xr *xdr.Reader) error { + o.Key = xr.ReadBytesMax(32) + return xr.Error() +} + +/* + +Response 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 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Code | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Length of Message | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ Message (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct Response { + int Code; + string Message<>; +} + +*/ + +func (o Response) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o Response) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o Response) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o Response) 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 Response) EncodeXDRInto(xw *xdr.Writer) (int, error) { + xw.WriteUint32(uint32(o.Code)) + xw.WriteString(o.Message) + return xw.Tot(), xw.Error() +} + +func (o *Response) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *Response) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *Response) DecodeXDRFrom(xr *xdr.Reader) error { + o.Code = int32(xr.ReadUint32()) + o.Message = xr.ReadString() + return xr.Error() +} + +/* + +ConnectRequest 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 ID | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ ID (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct ConnectRequest { + opaque ID<32>; +} + +*/ + +func (o ConnectRequest) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o ConnectRequest) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o ConnectRequest) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o ConnectRequest) 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 ConnectRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) { + if l := len(o.ID); l > 32 { + return xw.Tot(), xdr.ElementSizeExceeded("ID", l, 32) + } + xw.WriteBytes(o.ID) + return xw.Tot(), xw.Error() +} + +func (o *ConnectRequest) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *ConnectRequest) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *ConnectRequest) DecodeXDRFrom(xr *xdr.Reader) error { + o.ID = xr.ReadBytesMax(32) + return xr.Error() +} + +/* + +SessionInvitation 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 From | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ From (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Length of Key | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ Key (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Length of Address | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/ / +\ Address (variable length) \ +/ / ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| 0x0000 | Port | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Server Socket (V=0 or 1) |V| ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +struct SessionInvitation { + opaque From<32>; + opaque Key<32>; + opaque Address<32>; + unsigned int Port; + bool ServerSocket; +} + +*/ + +func (o SessionInvitation) EncodeXDR(w io.Writer) (int, error) { + var xw = xdr.NewWriter(w) + return o.EncodeXDRInto(xw) +} + +func (o SessionInvitation) MarshalXDR() ([]byte, error) { + return o.AppendXDR(make([]byte, 0, 128)) +} + +func (o SessionInvitation) MustMarshalXDR() []byte { + bs, err := o.MarshalXDR() + if err != nil { + panic(err) + } + return bs +} + +func (o SessionInvitation) 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 SessionInvitation) EncodeXDRInto(xw *xdr.Writer) (int, error) { + if l := len(o.From); l > 32 { + return xw.Tot(), xdr.ElementSizeExceeded("From", l, 32) + } + xw.WriteBytes(o.From) + if l := len(o.Key); l > 32 { + return xw.Tot(), xdr.ElementSizeExceeded("Key", l, 32) + } + xw.WriteBytes(o.Key) + if l := len(o.Address); l > 32 { + return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 32) + } + xw.WriteBytes(o.Address) + xw.WriteUint16(o.Port) + xw.WriteBool(o.ServerSocket) + return xw.Tot(), xw.Error() +} + +func (o *SessionInvitation) DecodeXDR(r io.Reader) error { + xr := xdr.NewReader(r) + return o.DecodeXDRFrom(xr) +} + +func (o *SessionInvitation) UnmarshalXDR(bs []byte) error { + var br = bytes.NewReader(bs) + var xr = xdr.NewReader(br) + return o.DecodeXDRFrom(xr) +} + +func (o *SessionInvitation) DecodeXDRFrom(xr *xdr.Reader) error { + o.From = xr.ReadBytesMax(32) + o.Key = xr.ReadBytesMax(32) + o.Address = xr.ReadBytesMax(32) + o.Port = xr.ReadUint16() + o.ServerSocket = xr.ReadBool() + return xr.Error() +} diff --git a/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/protocol.go b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/protocol.go new file mode 100644 index 000000000..57a967ac8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/syncthing/relaysrv/protocol/protocol.go @@ -0,0 +1,114 @@ +// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file). + +package protocol + +import ( + "fmt" + "io" +) + +const ( + magic = 0x9E79BC40 + ProtocolName = "bep-relay" +) + +var ( + ResponseSuccess = Response{0, "success"} + ResponseNotFound = Response{1, "not found"} + ResponseAlreadyConnected = Response{2, "already connected"} + ResponseInternalError = Response{99, "internal error"} + ResponseUnexpectedMessage = Response{100, "unexpected message"} +) + +func WriteMessage(w io.Writer, message interface{}) error { + header := header{ + magic: magic, + } + + var payload []byte + var err error + + switch msg := message.(type) { + case Ping: + payload, err = msg.MarshalXDR() + header.messageType = messageTypePing + case Pong: + payload, err = msg.MarshalXDR() + header.messageType = messageTypePong + case JoinRelayRequest: + payload, err = msg.MarshalXDR() + header.messageType = messageTypeJoinRelayRequest + case JoinSessionRequest: + payload, err = msg.MarshalXDR() + header.messageType = messageTypeJoinSessionRequest + case Response: + payload, err = msg.MarshalXDR() + header.messageType = messageTypeResponse + case ConnectRequest: + payload, err = msg.MarshalXDR() + header.messageType = messageTypeConnectRequest + case SessionInvitation: + payload, err = msg.MarshalXDR() + header.messageType = messageTypeSessionInvitation + default: + err = fmt.Errorf("Unknown message type") + } + + if err != nil { + return err + } + + header.messageLength = int32(len(payload)) + + headerpayload, err := header.MarshalXDR() + if err != nil { + return err + } + + _, err = w.Write(append(headerpayload, payload...)) + return err +} + +func ReadMessage(r io.Reader) (interface{}, error) { + var header header + if err := header.DecodeXDR(r); err != nil { + return nil, err + } + + if header.magic != magic { + return nil, fmt.Errorf("magic mismatch") + } + + switch header.messageType { + case messageTypePing: + var msg Ping + err := msg.DecodeXDR(r) + return msg, err + case messageTypePong: + var msg Pong + err := msg.DecodeXDR(r) + return msg, err + case messageTypeJoinRelayRequest: + var msg JoinRelayRequest + err := msg.DecodeXDR(r) + return msg, err + case messageTypeJoinSessionRequest: + var msg JoinSessionRequest + err := msg.DecodeXDR(r) + return msg, err + case messageTypeResponse: + var msg Response + err := msg.DecodeXDR(r) + return msg, err + case messageTypeConnectRequest: + var msg ConnectRequest + err := msg.DecodeXDR(r) + return msg, err + case messageTypeSessionInvitation: + var msg SessionInvitation + err := msg.DecodeXDR(r) + return msg, err + } + + return nil, fmt.Errorf("Unknown message type") +} From 8f2db99c86f624a922dd8280f70681f0c6f7904c Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Fri, 17 Jul 2015 21:22:07 +0100 Subject: [PATCH 07/11] Expose connection type and relay status in the UI --- cmd/syncthing/connections.go | 84 ++++++++----------- cmd/syncthing/connections_tcp.go | 16 ++-- cmd/syncthing/gui.go | 3 + cmd/syncthing/main.go | 12 +-- cmd/syncthing/verbose.go | 2 +- gui/assets/lang/lang-en.json | 4 +- gui/index.html | 19 ++++- gui/syncthing/core/syncthingController.js | 17 +++- lib/auto/gui.files.go | 8 +- lib/discover/discover.go | 54 +++++------- lib/model/connection.go | 6 ++ lib/model/model.go | 4 + lib/osutil/osutil.go | 20 +++++ lib/relay/debug.go | 19 +++++ cmd/syncthing/relays.go => lib/relay/relay.go | 38 +++++---- 15 files changed, 183 insertions(+), 123 deletions(-) create mode 100644 lib/relay/debug.go rename cmd/syncthing/relays.go => lib/relay/relay.go (83%) diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index 3c997a96c..f6f8bd7f6 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -16,17 +16,17 @@ import ( "time" "github.com/syncthing/protocol" - "github.com/syncthing/relaysrv/client" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/model" + "github.com/syncthing/syncthing/lib/osutil" "github.com/thejerf/suture" ) type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error) -type ListenerFactory func(*url.URL, *tls.Config, chan<- intermediateConnection) +type ListenerFactory func(*url.URL, *tls.Config, chan<- model.IntermediateConnection) var ( dialers = make(map[string]DialerFactory, 0) @@ -41,7 +41,7 @@ type connectionSvc struct { myID protocol.DeviceID model *model.Model tlsCfg *tls.Config - conns chan intermediateConnection + conns chan model.IntermediateConnection lastRelayCheck map[protocol.DeviceID]time.Time @@ -49,11 +49,6 @@ type connectionSvc struct { connType map[protocol.DeviceID]model.ConnectionType } -type intermediateConnection struct { - conn *tls.Conn - connType model.ConnectionType -} - func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config) *connectionSvc { svc := &connectionSvc{ Supervisor: suture.NewSimple("connectionSvc"), @@ -61,7 +56,7 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Mo myID: myID, model: mdl, tlsCfg: tlsCfg, - conns: make(chan intermediateConnection), + conns: make(chan model.IntermediateConnection), connType: make(map[protocol.DeviceID]model.ConnectionType), lastRelayCheck: make(map[protocol.DeviceID]time.Time), @@ -110,14 +105,14 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Mo func (s *connectionSvc) handle() { next: for c := range s.conns { - cs := c.conn.ConnectionState() + cs := c.Conn.ConnectionState() // We should have negotiated the next level protocol "bep/1.0" as part // of the TLS handshake. Unfortunately this can't be a hard error, // because there are implementations out there that don't support // protocol negotiation (iOS for one...). if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != bepProtocolName { - l.Infof("Peer %s did not negotiate bep/1.0", c.conn.RemoteAddr()) + l.Infof("Peer %s did not negotiate bep/1.0", c.Conn.RemoteAddr()) } // We should have received exactly one certificate from the other @@ -125,8 +120,8 @@ next: // connection. certs := cs.PeerCertificates if cl := len(certs); cl != 1 { - l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.conn.RemoteAddr()) - c.conn.Close() + l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.Conn.RemoteAddr()) + c.Conn.Close() continue } remoteCert := certs[0] @@ -137,7 +132,7 @@ next: // clients between the same NAT gateway, and global discovery. if remoteID == myID { l.Infof("Connected to myself (%s) - should not happen", remoteID) - c.conn.Close() + c.Conn.Close() continue } @@ -146,7 +141,7 @@ next: s.mut.RLock() ct, ok := s.connType[remoteID] s.mut.RUnlock() - if ok && !ct.IsDirect() && c.connType.IsDirect() { + if ok && !ct.IsDirect() && c.ConnType.IsDirect() { if debugNet { l.Debugln("Switching connections", remoteID) } @@ -159,7 +154,7 @@ next: // in parallel we don't want to do that or we end up with no // connections still established... l.Infof("Connected to already connected device (%s)", remoteID) - c.conn.Close() + c.Conn.Close() continue } @@ -177,41 +172,41 @@ next: // Incorrect certificate name is something the user most // likely wants to know about, since it's an advanced // config. Warn instead of Info. - l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.conn.RemoteAddr(), err) - c.conn.Close() + l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.Conn.RemoteAddr(), err) + c.Conn.Close() continue next } // If rate limiting is set, and based on the address we should // limit the connection, then we wrap it in a limiter. - limit := s.shouldLimit(c.conn.RemoteAddr()) + limit := s.shouldLimit(c.Conn.RemoteAddr()) - wr := io.Writer(c.conn) + wr := io.Writer(c.Conn) if limit && writeRateLimit != nil { - wr = &limitedWriter{c.conn, writeRateLimit} + wr = &limitedWriter{c.Conn, writeRateLimit} } - rd := io.Reader(c.conn) + rd := io.Reader(c.Conn) if limit && readRateLimit != nil { - rd = &limitedReader{c.conn, readRateLimit} + rd = &limitedReader{c.Conn, readRateLimit} } - name := fmt.Sprintf("%s-%s (%s)", c.conn.LocalAddr(), c.conn.RemoteAddr(), c.connType) + name := fmt.Sprintf("%s-%s (%s)", c.Conn.LocalAddr(), c.Conn.RemoteAddr(), c.ConnType) protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression) l.Infof("Established secure connection to %s at %s", remoteID, name) if debugNet { - l.Debugf("cipher suite: %04X in lan: %t", c.conn.ConnectionState().CipherSuite, !limit) + l.Debugf("cipher suite: %04X in lan: %t", c.Conn.ConnectionState().CipherSuite, !limit) } s.model.AddConnection(model.Connection{ - c.conn, + c.Conn, protoConn, - c.connType, + c.ConnType, }) s.mut.Lock() - s.connType[remoteID] = c.connType + s.connType[remoteID] = c.ConnType s.mut.Unlock() continue next } @@ -220,14 +215,14 @@ next: if !s.cfg.IgnoredDevice(remoteID) { events.Default.Log(events.DeviceRejected, map[string]string{ "device": remoteID.String(), - "address": c.conn.RemoteAddr().String(), + "address": c.Conn.RemoteAddr().String(), }) - l.Infof("Connection from %s (%s) with unknown device ID %s", c.conn.RemoteAddr(), c.connType, remoteID) + l.Infof("Connection from %s (%s) with unknown device ID %s", c.Conn.RemoteAddr(), c.ConnType, remoteID) } else { - l.Infof("Connection from %s (%s) with ignored device ID %s", c.conn.RemoteAddr(), c.connType, remoteID) + l.Infof("Connection from %s (%s) with ignored device ID %s", c.Conn.RemoteAddr(), c.ConnType, remoteID) } - c.conn.Close() + c.Conn.Close() } } @@ -294,7 +289,7 @@ func (s *connectionSvc) connect() { s.model.Close(deviceID, fmt.Errorf("switching connections")) } - s.conns <- intermediateConnection{ + s.conns <- model.IntermediateConnection{ conn, model.ConnectionTypeBasicDial, } continue nextDevice @@ -347,7 +342,10 @@ func (s *connectionSvc) connect() { l.Debugln("Sucessfully joined relay session", inv) } - setTCPOptions(conn.(*net.TCPConn)) + err = osutil.SetTCPOptions(conn.(*net.TCPConn)) + if err != nil { + l.Infoln(err) + } var tc *tls.Conn @@ -362,7 +360,7 @@ func (s *connectionSvc) connect() { tc.Close() continue } - s.conns <- intermediateConnection{ + s.conns <- model.IntermediateConnection{ tc, model.ConnectionTypeRelayDial, } continue nextDevice @@ -414,19 +412,3 @@ 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 index d2d73fd71..1c051e293 100644 --- a/cmd/syncthing/connections_tcp.go +++ b/cmd/syncthing/connections_tcp.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/syncthing/syncthing/lib/model" + "github.com/syncthing/syncthing/lib/osutil" ) func init() { @@ -46,7 +47,10 @@ func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) { return nil, err } - setTCPOptions(conn) + err = osutil.SetTCPOptions(conn) + if err != nil { + l.Infoln(err) + } tc := tls.Client(conn, tlsCfg) err = tc.Handshake() @@ -58,7 +62,7 @@ func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) { return tc, nil } -func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- intermediateConnection) { +func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) { tcaddr, err := net.ResolveTCPAddr("tcp", uri.Host) if err != nil { l.Fatalln("listen (BEP/tcp):", err) @@ -81,8 +85,10 @@ func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- intermediateConn l.Debugln("connect from", conn.RemoteAddr()) } - tcpConn := conn.(*net.TCPConn) - setTCPOptions(tcpConn) + err = osutil.SetTCPOptions(conn.(*net.TCPConn)) + if err != nil { + l.Infoln(err) + } tc := tls.Server(conn, tlsCfg) err = tc.Handshake() @@ -92,7 +98,7 @@ func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- intermediateConn continue } - conns <- intermediateConnection{ + conns <- model.IntermediateConnection{ tc, model.ConnectionTypeBasicAccept, } } diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 36e52e3ec..c19567f66 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -628,6 +628,9 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) { if cfg.Options().GlobalAnnEnabled && discoverer != nil { res["extAnnounceOK"] = discoverer.ExtAnnounceOK() } + if relaySvc != nil { + res["relayClientStatus"] = relaySvc.ClientStatus() + } cpuUsageLock.RLock() var cpusum float64 for _, p := range cpuUsagePercent { diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 15e355679..41c3c883e 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -34,8 +34,10 @@ 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/symlinks" "github.com/syncthing/syncthing/lib/upgrade" + "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/errors" "github.com/syndtr/goleveldb/leveldb/opt" @@ -110,6 +112,7 @@ var ( readRateLimit *ratelimit.Bucket stop = make(chan int) discoverer *discover.Discoverer + relaySvc *relay.Svc cert tls.Certificate lans []*net.IPNet ) @@ -671,14 +674,14 @@ func syncthingMain() { // Start the relevant services connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg) - relaySvc := newRelaySvc(cfg, tlsCfg, connectionSvc.conns) + relaySvc = relay.NewSvc(cfg, tlsCfg, connectionSvc.conns) connectionSvc.Add(relaySvc) mainSvc.Add(connectionSvc) // Start discovery localPort := addr.Port - discoverer = discovery(localPort) + discoverer = discovery(localPort, relaySvc) // Start UPnP. The UPnP service will restart global discovery if the // external port changes. @@ -908,10 +911,9 @@ func shutdown() { stop <- exitSuccess } -func discovery(extPort int) *discover.Discoverer { +func discovery(extPort int, relaySvc *relay.Svc) *discover.Discoverer { opts := cfg.Options() - disc := discover.NewDiscoverer(myID, opts.ListenAddress, opts.RelayServers) - + disc := discover.NewDiscoverer(myID, opts.ListenAddress, relaySvc) if opts.LocalAnnEnabled { l.Infoln("Starting local discovery announcements") disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr) diff --git a/cmd/syncthing/verbose.go b/cmd/syncthing/verbose.go index 7e985b0af..e94ad4690 100644 --- a/cmd/syncthing/verbose.go +++ b/cmd/syncthing/verbose.go @@ -74,7 +74,7 @@ func (s *verboseSvc) formatEvent(ev events.Event) string { return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"]) case events.DeviceConnected: data := ev.Data.(map[string]string) - return fmt.Sprintf("Connected to device %v at %v", data["id"], data["addr"]) + return fmt.Sprintf("Connected to device %v at %v (type %s)", data["id"], data["addr"], data["type"]) case events.DeviceDisconnected: data := ev.Data.(map[string]string) return fmt.Sprintf("Disconnected from device %v", data["id"]) diff --git a/gui/assets/lang/lang-en.json b/gui/assets/lang/lang-en.json index ff1ff5bb7..fd89ca253 100644 --- a/gui/assets/lang/lang-en.json +++ b/gui/assets/lang/lang-en.json @@ -49,7 +49,7 @@ "Edit Folder": "Edit Folder", "Editing": "Editing", "Enable UPnP": "Enable UPnP", - "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "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.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.", "Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.", "Error": "Error", "External File Versioning": "External File Versioning", @@ -120,6 +120,8 @@ "Quick guide to supported patterns": "Quick guide to supported patterns", "RAM Utilization": "RAM Utilization", "Random": "Random", + "Relayed via": "Relayed via", + "Relays": "Relays", "Release Notes": "Release Notes", "Remove": "Remove", "Rescan": "Rescan", diff --git a/gui/index.html b/gui/index.html index c58509280..fd244d652 100755 --- a/gui/index.html +++ b/gui/index.html @@ -385,6 +385,19 @@ + +  Relays + + + OK + + + + {{relayClientsTotal-relayClientsFailed.length}}/{{relayClientsTotal}} + + + +  Uptime {{system.uptime | duration:"m"}} @@ -430,7 +443,11 @@ {{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B) -  Address + + + Address + Relayed via + {{deviceAddr(deviceCfg)}} diff --git a/gui/syncthing/core/syncthingController.js b/gui/syncthing/core/syncthingController.js index a3d847276..4980af052 100755 --- a/gui/syncthing/core/syncthingController.js +++ b/gui/syncthing/core/syncthingController.js @@ -176,6 +176,7 @@ angular.module('syncthing.core') outbps: 0, inBytesTotal: 0, outBytesTotal: 0, + type: arg.data.type, address: arg.data.addr }; $scope.completion[arg.data.id] = { @@ -346,14 +347,24 @@ angular.module('syncthing.core') $http.get(urlbase + '/system/status').success(function (data) { $scope.myID = data.myID; $scope.system = data; + $scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0; - var failed = []; + var failedAnnounce = []; for (var server in data.extAnnounceOK) { if (!data.extAnnounceOK[server]) { - failed.push(server); + failedAnnounce.push(server); } } - $scope.announceServersFailed = failed; + $scope.announceServersFailed = failedAnnounce; + + $scope.relayClientsTotal = data.relayClientStatus ? Object.keys(data.relayClientStatus).length : 0; + var failedRelays = []; + for (var relay in data.relayClientStatus) { + if (!data.relayClientStatus[relay]) { + failedRelays.push(relay); + } + } + $scope.relayClientsFailed = failedRelays; console.log("refreshSystem", data); diff --git a/lib/auto/gui.files.go b/lib/auto/gui.files.go index db2a0fb17..9101f756a 100644 --- a/lib/auto/gui.files.go +++ b/lib/auto/gui.files.go @@ -5,7 +5,7 @@ import ( ) const ( - AssetsBuildDate = "Wed, 19 Aug 2015 19:52:35 GMT" + AssetsBuildDate = "Wed, 19 Aug 2015 20:10:12 GMT" ) func Assets() map[string][]byte { @@ -24,7 +24,7 @@ func Assets() map[string][]byte { assets["assets/lang/lang-de.json"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/9x8zW4kR3L/fZ8id4DBnwSa/Z9de/cwBwvzKY1nNMMVhxIsDGBkd2V3l7q6qreyihRJ0PBhH8PAXgYG/AI66WS+iZ/Ev19EZlZWd5PSwgcD1kFqVkZmRkbGd0Tq5jcG/zx6Zmq3tF154Uzdb2auNc3CFPbKm6Jxvv5/ndnYtTPe1d5NHz01j16VtRvmPKuv7aoyF01tPtqlq41rl+WsM2sHsNqclXU9fTQZtrrEcj80rblwrS8xaWOvTN10ZubMvNlsseiscuay7FZm27qLsul9hPWy/cuSu/fOfGX7bReXKX1n3EVXTU1dzlfAueyAU7tybUmkvlUw/FqHTVw1oHX6xrx1V1wcP0/O5qvq7mfvXZUAZk3fcfju30ifs6t63q3KepnG5x3RkwXWneyThoqCn78q6+t+cffzcjRiXuKAc0eAL11797kzq8Nwr5uqcC3hcJBrhxPWpb8PmDReyIQvOOM9aOXNPfO+yCa2zusR5JfbGXH5WLbfha3nTs74qr10ZefabnfQvGjqRbnsW0vajECdeTsaizOryry0nZUtK/AD/nD5aHMJxmvqqw3Z49yD8cw3btu0HW7li3RTXWuXfb0U5rQCDgZ433fX+OgxTk4Rjq1sP8uJUW1Xdua6cq4IhL/8fJUgMOtHHKC2Fdl2Y+vCrPCvCqTuVi5yLLCZmje4VovPjWndpoHIEGBR4lSLttnIX/5K6KS3FoUs7ACGO22bZWuxS79o+4VsBIw6U0AUAmeXru252StvNr33MkSilcaCQoXbmOtetlm1APYAdzW/fmgL7uDqbsG9Mlm9j7xCEiVmu0vMYfIV9iRve5BHLxjnk2swZd21TdHPMV1hIO24bygAWxSAAqEgXT4OgnSXrqqmiRdUVNxEzjhz5YY0wEkrngMIOl47rtWXdTHB3LbAH7afrwjMSR7nDvKWwSfc+67ZgBvnpt+C6oUyvg1fwQPOnG8LkNbHGc+humzrFn31WxHRBjcCYvw2jfdLWeO1WwHH+PXF6bk578qqvE5igXu+hng1Le6ssr7rBxXzApe+dFUj5L/7C+6O9+23bdM166ZKqupF5UBiqCtjF52qjGfgmrvPPXm/tgMLv6gaL6qH+q50d38dSPBCOZqDb/Vnkw+BWUDXFdbrPe7LdsrDnW072g5rKqj+OBnAtuU11LiEDaR2gYPgblvzPa8Md1KDh7pptsGWOiaQ5C3/LDfK3gNMXTvRueZV2zZRMc5w4STKYkznZltStihrrvIOiLdO74nsCEyAy596R9ZaE3ZQYPnUBoakhLjrTAqO/n1gzhWGYIL+8z/M75/87g8q7g11FpiNurBryxnYqfVPudj+BPI1JiwpobU5r3GPvrv7ubt27dO4zUtXuU6O8e7uJzJlPR6RywPDyGA3jIlIvXk5mBz5a2cY23blopwnxgzSEgfWI2UdZr23m8ySuZp/jiFuHqtIP741RzePrdqVx7fH5tLWnSjIuV7s1EQ7phO+yHC4udFvt1zkJixyi0U2dz9RnRvKHvUvuQH6zOxb1i/GePkM6zQCQVdclJJfQkmAgwdCNvNeWDtS6GWzzj4kqMu6amxhvrF6V/HDiXzYgdKdviKn48Zxd/i0t1RQwO/uPhc4U4CNQK+KUtyU5862MxrYOh/Z9zZmh+EOexuHgQM+B3asLR2589P6lAD8r7Fwji7E9AxQQF8tKDzMrYUbAIH79KjcPqW5+fTI2Oh+QAIxUFyBsco5BsAuW9cumnZjbFLZBe8Nl39FRUTBC9PFenyknjdF34JBoPiN6DazDFfrsPqb06enum10dExDVZXvi5kT029ESkdWIUxp11iNXgcMLlVbZlP1tOWyblpn4IXSvPsJzKLjUURtTvV6ZuI6QxQJC4rBpndq3ib8F6gBza8adFg9qsKxnXkV/ZTXVLffJsdEvAz1MNRTuMg9iTj7tcWsAj6M2yQbtnQ4b0WbGWYO9yl7nPYw53AslIcE4u7nwR3zrSuhr0TDjeaNcXsAJwKDYJtSzISZldAeMMCBsoXapqpp1lS44BAzF+vpp/BlHL2Q188+qgfmrzxPNk0bXvfLtlwsgKMoE3EvznrqVzGexPHuc7C++FaG+6GieQ7vAgufyDq6MCDABTjEpejyaX4CRZkOobg8Ux9jGR/8QD1G67aVpXPY0F0S1W5mV0MMMqAOYHV2JkAbsc8AA53oTDAFytDYycHqToyt4GiBg8UXH6Ikep9lPYoaqNCBW44nf8xXzezhk9FXonew2eLvNBer2/97h6Y/JkZDnYbAdwhyC+G7Bioper0QfHgBeyC56xv9YuihTt1iB9L4Liq3eSVqYUQNer2MfEecGpSeejvRgTYix/QraHARV4xdY5mydDCZq0CmnPGjW8142HIu/viGqBUabM76uvAmiX1OrEPWZTwYHJT8IgYnJYB8bX3wb/XX6NpODE4GNTMbjrgz/dR2q509tgtbjKH8DsQQFH95/kYUNf4zfAERcSvwkNRxwh7eXzatmPXwuzOgJ1zffmnFQrTQGmIkWjBUu6juPkPVPLAk9Jec+b3MIXf+rQu+Kz1j3kNR/VF9rKvRtD20DBR/Gzwa+T0y6V9WzQzW5mW0xAIl3xLn+T5fbQfenLn2Qk8Zph21x+YNWO9HryPjiWddREWhW/nSJ5b6ylVbTcFUi7TpV82GFngpE/mH/A6DYnRl5E1S7+Mxso8Y7xxIbfQuYDJTXv0kMSzlctd8pGk1nCFqL3qIuCzmsY7els//vz8Wv08+SDaEN2COJIAFhxTHABOobKEWm3UpAFcOYr6tsBt6QlcN2CWoXQBhTdpRqA/JDeSqtKwb2Fs6dEMGcAEtSrdnlMAxa4s4r3AjLVrWK1vR26Qsfi4kIQf1MFrfaLqwrxH6isqZQZ3W4GsYjlrNhhv5Um9SFiEIqWYAlsJhA1DMDwaNuYRHUfOwdF3x+aicuqkpGklBuh+hUAsnhD7frN1KVSc1HIi8pFOGyc8dnXCOHF1Pn0+paTR8DplHHCBdwlvnttHARGUSjI1rSZTh4t9Z4A79+bpEsCfs3N79dPdX3PF1z/hvgBOgihmZOR0kUS/vaNiccZstw+vkmJnRLO+cxCrf9xXBYW0kUE8gQZ+ebSmjiZHfNfNdcX7XrO+TZoVOMqmQuyKZAZmjj01nq+N9YHPU6UiY9LWkjc81JyNim+V/fcjVDMA/lhs46c+ChDM2BCdV2bm+dp2FE2DNh7q6Up0Kq6Ifh2v5uqxlodetcyTC2pxtrQZRMoRzLGBogPVr13l4LdALtd+S87OtJOsH+91syYh/7l3vNI0mopLlRL6z4GPxrnGLRlk+rdJXXWkqd+EqegbF3LaFOUL4ga2Y9cPoFrdSlJT6BopUQFVvfCtOi6VPCxp4Wk+EKSKgQODossRSovovRoBmZPySI9ulS3nvgqp+X7rh22UWamr6+cuRWBJicAP2EtQZ2EgiBHBPIN43OlbWw5dDsc5bapgHIp33TRdQ/qqsL92Axoe3/Ih/x78XC02rDRA4So7o3V9I5D1MP2xTmUB/Dnf7oRenjhpRqUlNMkrXdgdAh7Ds0ARnPsx+cOsh0YCZy+ZvsC3QZIdtywcQsC3hr2pW0o9ykuAncfuCBzackS5X9GKD1RF/F1666AOWf3q6s+a74PfOWyf5gHJhgBnLUUFPw4eZmo+Y2UEOHH3o1s7pBB79y7GZ25qTNTcJg2b8Co7XHGRbaHx8CkcPV7MZuYzqxG6AC5UQnVl8V2QgGwTsl13IYxIPIbhgQlpPQTdvPhKb+uT7KFZARuwh441nszXki8nymPKMPjTlbkQjyVAOkRLQ76tC4oBOwtujytkLUfbdlYS3pGHhFhYq4FBIVdYZzY+niQRlLSfOqgjABtcZtFdC0DcVKXLUgCBQVks4/UY0RmHHhaV873JM3kF1JAt+imN4Vv2gvasuBDf6DbfsmHFaSKJEszxkXBuKh0Hby1Gev4HKha7zIWUSjnP302IBh6bC/sw5YMUSujQGiYBxWsbcKyTWtBOVxvQ7qF7akF8rueUldfXA3SxWustYAABL9jtDo0JKDicx004lJemWP/XlfG2WPcUNwgNDx+nggm3mgp6RVysYjcqVXcg9FeJOpdwxnX4mMnzX9uuuz1zOb559vVuI4KdnezWIb1gI2KgHAe8v1Ry+Cbf2nrcWrM1h8g8zmB0QVzIWnoYhDwEO6t7w996geVZVqRpU3wv1hoe/0Iz9GT6U4e8Mb6lZhL3093jIvHcu5GUjBKX37id48LuwwcyIuiBSS/3sMjiqJC1/AqCLeiCOn1klyVmQv3QeIh+tWPodx0D7eRc0gBbcyCUrJiMklRDTKmW3EnH57u7zqlIpiXkA6PmJFMwLcXBf9poJGMu2uNe1pCT8UK3JtteNsu2lhp+lM3a3H/sWggQ/F73iMkpGhN1Djn8wqWeuI+F9iEpgc6tKbFACICqS/JUVRp8zH0RGD/kgOaCHLcCRBt9mbxbOsYf7eKVsBT8skSbV8VJ2MM2uUoohL++7oC9G8wrZLxQwMC59EQME8xFlqOm4NpmSUIY2X4PjaXakusdUAwtmsMwh7RQZTu71bQ9MzJuXIYEwNgyylJpYK6YkJohO4mnNsiIsPN5y0LpA8DKkgrCwpbs6utnmUtW1gy2DR5SKW9HgRTy9xBODW2ELKPuu9JqeHOXkxGFAtCjuGXySaFWZaZkm8ba0EV0nHvtQRZMokTh2wWmg2Y8rDZCStMEeUj+QhFxtXsDeMPjWrogQa07/5yftpVBdRK9Lth5UxUAITxercotOnYpfPKkad73CEK3tHT4kBVkWrFn56W2lfulEvpNFgnBjajnLdMqq7wqcOK+GLeyqza9eIQyLxbEO+m1DzcQcZbk0QxVtZ2bJCQdLISWiMOYz7o8QzgBauXsjMOo7yWoPERgi7KvjsPq1q+qHIi+nSZAAdth3SphsKEd5yPEW8kMG8Tsxx1nTt7jpF43GzFLnnjdDoIzbWy4dNcUBmpx1PYw4Y6EHySJm8XnbXIbkZPipHQEZ7Vkchbs8V5cl/LXOAZrtNtZbPf8YjqFuj0wMP+MI46BTtkHMm+rXZTfBeWd5pHSwkYIQkQyjtpkcQLNX7O6ZOTrK4EtDxpzGaQpw2cOPTtXayJPT/ZXKWjJQfqdnwDeL7pJmAApaunDoYFNtuWbxdLwV1Mjq7nPVjZoIzFlagIKsNqewF0399AAOXmoM6tHsHuRe52Y0X93ze6bnqmB/tneIaKmvoL1IyYlRo8TqHmM53NOsgm0V50ISmOLm1a6LzQMgzhROdtdeYb3/+td/H6PAlCUcwRi/tS2FbGZbaY+iCGqlyBtpYuSH07CjBMcMGoYtL1LPyVQkh4kw48S5m04PXG9+OPfjlolzYbLsXPjvnN0v+Cina92fmfSYmhCCtG6B21kJgzBnTfKE68rzqZrtTItSbH2nlvqVT0QQAzXL5u2et9C6itb2GVeUK3bLQJtJELiyQEyDIWlWkJIbGe+MdkwYRcgpCd0wrHne5E2zNW/YX2pOE6aWpULnEzJqGKleZ0ysrAZrwSwAVFjLflRoMZ+UjNbj+hlCkOrK2AtbVtKRYDtz87hvq8e3qZ0U3Gw3Gya8apPpJa2lZZEMNTFbSMgvIsrm5gYr3d6OsBnn2wflYFkPZcmPOQwLRr0gxtPs+KwgpNsEm0QgNd6IIEdLJ/THGXhVNlkYP9EqXchOhxaMbmdfz9tKUh0j/9DmIFHyeBeIi2R5arfaZKrs4+BcwE1grKKtvTPEpuuEcOZJFLZdBNQqR5uRO4Dj1UAQJ0QXbRCyPIuGlYTgBn3S/pV/MMFz/PQI6NuqWcZEU+7rTTWTqxUP8NoqlHCT73cU2gmOE96rUnswrnutaRajo0iqB57Rp6wbBrh8T69VkDl5qcjEVFP0/oIbBLvQS8eXEn9q3rngBIR6yXN2NCF+5xflzQHXcGGxoJSlWUhEqJn2akv56CUH0UoOghpVSssFZONKGlWpvINfjCPO19KaA+Ixsc0kDGK0EFB6xPNKO7vdDh3a5s0iZHJCmkJS7N7J0lrrLqjZUm0bOmojmGXhYiDT0paByV1MTIfGbBB8J1+itgXu31IEdclYCh7T1LxqdSg5UdK8k0u58PEwztInaOxmPvRwTHJn7ETLMuFGYmPuyZA/g2DQ/2I7Ia7wDDa8c6JHnwdES1a11n296IJWC93Nhl4uW44mIbmmId5rV2vrj+YMBjd7fL+d+HCDsKRkKdtgzAUMbiEXPCQTrfnD72k9/vDHLH9K7oJCYE6O5MHPhoEBM0162fpEwE/0uvyeBM2cTAp8mSRHypRaQctFZmyNl3c/V125jKZYmpm9ElCSdcSXlATGQmDKVci1romitDo/lxyXnYUbet9Dsbd1WPDXS9SIvAt62anPm0Uzw1a1jZN2Lp9ntLdM4BJKgmbHSuM+bOsqfTwh0KP0bOJ3YSIT22+vJbjPF0rGOXGm5HR5Kg5cC2+Z070Zae+UAtZkRR6uj84eOzLu1ebjNo1f1ujDipuQYAgp+jwXcfTHvx/YUnr/EO/448OcOYlsmShfANGj6fFE2NIcnRzLCNUrtAiYyBz98/FofQRq9x1IrKMw4FqSHM+djvbi+pPRiGxM+guHQpOAOdvjqWakhSJ9u8OfE/O9XVWBE2kPTvt63Z0o3hlnnhD/iUCFXuDwFUeIu6pX5H6J0H1dwp18+OZ6MXEsbMbqxuFlhXcP8sSYL9lWc5ApdtYMsU7M0qoxpjF6OiSmRA5X9IutGV7+mLXb8vUPm0f+7kmQOrFU+bTCXt07i0vuwmMleQV13xwM8lK6spJJm1Bxpnm9b8qlc+vog4uOh/gNjd7PZpq+wM8hzu6eapkmKAT6p+TiEEoxAQ48z9xaPQfh0mCLcCnaYTBh332Y/dEuFegHx7BQ1proDnWEwYLhJVcElHmUJWmBlK1npZcSWl5ZH2/PBtXvGirWGcuAissuI8VLZx0Tsk3TFPgp1TKv4SNHhh2IFdvd1g1bd+vAW+MCaHSk8j3zaxrUT3j4Rh1xkKPhw48OKiohNYUkbxnThQSU6329F7fuyo2UctZsDhlY5Yi3AIabiLeE8ScJKCvoteSkwSOtwDBLGgoogk7z57YaNTVeNO0yED/iyZ3khifYY4b77TsnXZGjiZIrKWk9aZDjBY79yk3ojliwO6Jgd4R4BAw452yGHxO5qU92nxrOXHfJ4OiJ0P53T54QOWYzAXOctIm8i6k7b7trqZVt8s6LGi6S73Y6L3ymtYU10sZyN9eX0rItpFiH3YBDH3BQZyE/6M7LyF/POeXoheRA3QE9QUhMwC8Zzh0kIncspPs0eBHw2r2o5SlYAhK4cXbgm8DMvdaEVcjlmi9dRUFNj8bkwdQCfNNk7K1l4GnGMxmP3IMoTMXAvQHhiTS7L8rQTBbII21WwqWJSMrPpBuVUBtamu4tUN+DQtr+f+fSHjSTp/fZxtES0o1RSXfGw9J09OQpvivocVRcpBpXOHkXV8jlYu8hryTBuBAf8GqTyPEIGS2uRjP9CxiB/sEcJ0U6KsX+IjqSH4rmbZcyV+IjtBKuFcMrEKx7JZeb+qf1eaW0lneSomubNiQ6m+oiGBi6aum1YHrXkaVtNcfFW498NzI3oTmkkF4wvsCg0lQBYkomQxw7SzJz/BI6b2pgfU/fNB/sUhjWEnF/gQs5kL4/tXyZtm7a2QPZ+/N6XYdiy3k9c0xidMOYVHILHVxqCXEYjOVsDo0q2edbyvrL0AP4bN2x6jAMSk2KI4dSwVmD3/m4py/8aT425uZxOMvjWwWUNwbMq9zchJHb2/G8vDx/eN/RSy39cxik0dYGXs0NwLsdpnrczsePp2fiQYbW8PCBCvJAK3Wo/Q+1j3BzWY/ozojPW9aHaxx1rSdQisVYHOIrCs0Mq+CoH9+tbL3nweIc7se5cxpNDcKstsbSb3PivmaqILzQHTkRB+QpPUoM8sKkMCsWov816zu0247dLo3QiTQNQxhK1iNarh/UG7Izz/ij64Z2NGaqa/FfkwB9R51gi0Kz8MOTw4naVpxwU0pMKa9ch6cZSe3Jk+VRetFrq06j3UnODg+Jsv8LQHjFRclONX8v4aqPmZ9YHeXLZU0Zsga75P+HgaG9PGiU9xnLZAipTR84mkZuh48WmxykvJtlAcu8swMqYJV5bbEBwCRdPGeNgv+riFKUOJWwUEpqpGAphNtgxjifBiBvInj+EJFGbRbyHJGEHUi2H81mT680PzjZ6dVIbmBqQ9Ap8ekoHYGYAABvNlkHw6HGBfVXvmybu7+eiJnQoqz2QfYxz15YPkMP6O/FKP+kNct/tOlD0ysJNVygj8Bebr4ZTMaABkOgSGlNHPDV0sFoMO204Cu9Yvcd61utqIO/Dr5oLWPD6Su+4r/7PDyayB73pue82gcU8gWfAKI/H99+eiRIZ2954+vd3ZZCzLrRWbe3fPQpebDpo9/c/jcAAAD//wEAAP//qf57gr5EAAA=") assets["assets/lang/lang-el.json"], _ = base64.StdEncoding.DecodeString("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") assets["assets/lang/lang-en-GB.json"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/+Q7624ct9X/8xT8DAifBCj7Ofma/PCPGr7EjeHYUX1pkMJAwZ3h7rKaGU5IjtYbQUWfpkBfo4/SJ+k5h4cccnZXshXJTlD/sMhz4+Ht3Dh7/pmAf3ceiE4tpddnSnRDO1dWmIWo5caJ2ijX/a8XrTxVwqnOqdmdex/KcDyOsgbEX40VZ8o6bTrobURnvJgrUZm2B5HzRom19ivRW3WmzeAirUsj/zIhUZuTp+KZ2pBMbkbM3Aye4NSI0MojP8G5GTF1TVD4M0LEYxi4UoyIvQz/xDS1shHPvQyP01wQ9H4kykEjpVXOMQU1S4zKcSrDnsmuUqw4tyc48ch0C70crMTp5pQTTORrGvFYekmUsT3izFo86Ey3aXE73ji5VOKl6o31ulveZ57LaZKsfiXnyusqcKVexHdCvfPKdrLB89DKrhYr+K9RTviVikcBRM7EUw8oABthVWvgOCPBQsPxWVjTUs9taMph4cMZvNUB0iz2rEPQYB8ycW9ETWfOgYphs2AMuCyyE7rz1tRDBdc20MBdgf2C6yPrGqhAWb/SLiJB/bVqGp76LciNOg/etHCiKjH0SytrPrrbUKZ/CNddWrUYmv9BwrwbKYYlyaC/DHt08ka88brRP6djnUCuOM+PYE+XqjG04mMnYhsFUwYDIeTCh4s8BSVK41TAYyNCw8EhODczjOr8sVivVCcGBysnfTgpXlqPplaKRncq8r4f8Si9R1sQ5551E0XXKTJx4htrTZjaFJZoe41nF8+yapwCLSzrtQuxg8tYvdRwk6ZMCT7ybAC08uJf/xRf3v3iq3CTDBoNOPhokrzVczgt1t1jYR/AwKM8Vo3yNAFuFfB6RNQjhg7z08cBFzsTbA17pBe6SmduN6LkeiFbldFSt6Q4Pwh36eBCHJ4fyGDlDy6OxFp2nqxOFTZuJqIDCQz3M7nn5wF2gULOWcjFewkptHGjzHTbHmvHzLx4eT/SmGrAMzyuTQFIVOuuMbIWLyVvUAGYUPFoY2+CZzuad5nim1qT86e/GSxz6Hk3pxhdet7NKHjU2IyYTmK88uakOyFs1k0UYE6Cq4GYqpfgd+GavL2j+3to99/eETI6eLg3gKg3nWx1BQjYvV7ZhbGtkMmS1rgL4KU2aB7wVjD7LIz/sQYrZqeXnbFKQPSGntUdg09RKI2MV6bYFXRRZjRchbX6JnrtJ+iA/5TcNBHuwzHvEwnwGvy5aumgF/1Ig6wnA/i87y2fhCkopywV2DcugmF+rSYjLeYa7iT4OV6IOhj+xphTNGqw9qIiV+VmEBso9MxPHrwOEYfbONR2lka7YbGZxkEWBjzk9mfOxwCc45wg36q+kRj8GAwZyK6K+Ua8gogIIgWIn6KqNyZvv441nHN0mm0P/SRdQ2Rz4+rfylBbM+ut8WRog0Pl/YNsqab9M3AXY6AG1wgc4RZJHq3FUA5yOh8iOQVKOR9vddUMcAzsZMqfSIe4Fskgl7Y49Nhnj50S+1w6nzNHQEl1Iv0qo6FuQeFGbLITf3jzFKH4Z4QIiHRXGA2EWABEObc2tmbKvej9EuCi2j3chMo4v9Mwt04USeNOeORRnbLsiFM74hozBzv6ONp9opnC9tCKV8qesdZ7UCXnKx+1yPtM861qesTR3wgzLXqPJTGNHcY+JeuHKG4VcNxe8jojwQiaUCbTWhBn0EjfgatFG4thDKx3C5HD4TP98P/cETFegs4kWAu3LGVkYY+xLFLLFvPDjRlsNCdABFuK1h7SVqs6hCUzAkbIgFfACGQWh78V2Un3mDKG0VIv4WORh6/4Up/BeYRxMIYC8KGeqZmoDRWA1DuwALXihbsWJ4/7TKk+OmPawBLAVN9Ju0Tz80RbR0FjCUhUBGkwTa8UaFEH0i1oTu+U6hIZdRKWrVJoRKipplduCioo07XJu9sU4vC18bI5mlAmMHM8p3Lcm5CfI20JSFTvdDu04sGSacZupFBegm+U4vuuoTmUgEilO2J8YpXC+Z2KV70Mkfk+VOSkAgz4DdPjqfhpUENg2wWPPEPjtWjUmWrQ/9SVtLU4hNC2WqGXQmwPm1hrvCUGzBSRhst7bV4e+4ViWxgaCbrOkpGsl+FHz5f1RnxxaIt+pDGEMWN/V8y8A5rofdQvtBj+/TOEwf+xv1gQAP5ECKiaa1f0I02farGxGTEDBQNocwibdbcpxmB+GzhSL81+63wZOkqAnbMaYplQQQrDTWFMi9FDjGvYolIEpCAQr0KxsR8wwBE/cCRUWUXpmV4IGBqL72zOwG/PxGvg9HDuFEZVVlaYPx3+7UhUskPmUC0CYy3cCjK7ChZhEbKmX4ci+apQ/WiMlYFuaGqKBT2lK4eNknCJVdv7DaUrqGytFhIu2a6gWnfZ5I5madK3PU6cE4hx+FzROWQMgWyAwbLB4s3VgtLckE7jEZP86sFl0KDyDYgpNVrLUP7Iu5EC31LUmrDcLDFFMTojK+HM88dBV6diOeA1gJPmhh6xsMJ9FmNdTcTSXj54Pq3qJlBZ1X0JoYlpiSC0IpzX7QWuG6ELQKLCJC6gqZXgDs5ygFOrgIsHTTPiqFfin2Jp40zmRAk0UmJBlymoWWLEC6W47jWBlHRsu7NewuM9DDhqMfyVDDOmvxEGKkYvkNoRBwtXeb4Y4akA926F+SBlczGv1X5Fp/jDGLZHCeiMiZ7+ssRxOsp7MKRRPC6RCwK4HXHISghq5NDM/xb9HTQOTCgMPjr0/ciCO0O4ka2AFvTZQt4f6QtoTl+TqETI3UQBt1Borl0rmwwhv2GJ55CHo9GkdwjZUiIvwVFwgh63OezLjQkbtVtzbh+bGQafqYBN1ujwU70+2ukoE+6GH9zo3mQNht1rF+o3RdmCHBdkGRSIgG+MzqADVeMEP/aov3i+Q1+TO+cYAKVm93NUzKHDb9TCB4/4C+d77VHTfAdfw+BBC25PcAJfvOIjzzYwUmvs74p592ASX7cExL6QH+0MlfnGkN9AZnMUBF+PNY7cwp0oIucJJNJBug5r+ciETC3vRgovl0uFd37X9PciR264zg+tWXP1qQSMVLCRkBk4Jom9hDd9H5xRbEZMiAEIw82Iwej9xBpvKtPsrGhdQZHJibPlZoYJpQx8059DVg7+Y8Bwd93NIv1+gi0puqOqg5u8TDqz8Gu0zmAI6V0f40m8/Mos7pXDXEvCth6OyqghHJhMZILbxRtCyZ2sGWqL0ylItPCuw83HBToWwfDjIwTmAr0180a1wTlTlYliok75+CAJ05pBWOntBuT9++//KIe/BfmXzkG9gzBbKzoxmXj4W+EDOwBpEKt+GhQmRRxiW7WABV7RDmIpErXkFc9rZ6GIlYTivXPezfZP+dOowyuECR8YCotfiYGtcOmCh1eBYd7oqtkIeSZ1Q8+d0ovzg8E2Bxc0pWvxnwP/xUWhQ1m1HC+lxJcYfG7AJBXyUX2G48yyGbYYdcSJw4pGouCZIKkpRCelP96A2Szr5GAxFA9fws0b2Z0mtS6h2CkHxlf03ErXhdP0hRm6Ovrwt+GR+/eCY6y3d8AzycYsY6UgD1dmoQ4X6sK1dCt+HkrhyyE/Ox7t0PgT6pKtDVwku+nxMA6UzVrKZtGS0GtUDQdxQ597odXjiM1bCekrPqWDHn0jPSbh7jjmM07/zGrIvh+/UBRPF5zJc7JLFVCnSHR4Hqvx7qbnMLiFLWmWpTFh+nIp9Xg2f8tTKHbCUwAynpBUbsL3agFJs65pHmP1RoqvvkRL9tXXWQXKeYsXD6snaFygaTCsxMpCmFP4thVmS1q5rWMzV8TEBydb51+rgtkqLjAoTJ8v4scTAr/1aBV9ZOHy0l+PhTGkogxM4RPKNq1VTfggmKiLsldamI86Zj7X+MS71z5eQrFTTss5KVcs8/T18OvfjVtIn8hAEO6Odu/icdzCNNcaBj+cHR3TForDz48IM+BTmqsgSheHfzkq5EMSsGMavzYFL13FodMQhFwyi0iwLYV2/opd3U1TyuLAWXPVLbgENIL3xnoEnd4VBk0yfXsOJ/FU9ZCI0hP1/9/ls0oWMmer5WYvF4qc0oMk+p5+Hw8gj2FZvG6IqeXHNDTr+1jWShVL818z5d07jc88cObR/vLBwA+xVxay2Z8hMtuxUJdwOOTIxsmVG68i/1YC78u+I/vhjDtG9bqlevkpPlqPa3MIFhJX+Jg8MeDvJqLs2cLi0h1tKXQjMnNd+al2gU+1NT7VkvvCVKLCbz/L+Zvu8+kvTubKrzG0vkvL8sXdu6gLVpaAJlP/lofJZjT5JcwHbfu1ePePHbdgQR9isWOEUM6RHZyJPytrRKvkuDn7NLmOpJ16gSkeTwRLPaYPN1H0juGvYtg5SmK45up/IH+mw6WO6EoPhN9PQUCE78eXH8fDu/cAHkiPovhrcufjh0en6AuuEAMLxTY/zfD6AkYtNuSBrIKoF9+D4xfEssFMv6vHz//CD2bou0g8jgq/8eVClWnORuN9gwKTlgCl6lH5E7T8UfZKmiiLbtEjWLUdddaXw3yuAf1Qb6OZ/0132nGxOzYThp616oDidsLFVz5uRXiPl+wxfxaU9RKeavMBGZoJk777mXzxw13x2ojzA16Gg4uMEAc5P2fMxUXJxysxdhK2+B1A3k0U6K0CkloR7pT49vXrk1cUefBXmNtApuYVR5rYLDEuff1ZAqZUeAjLwxe/4A1ltHBMQ6zqITneCm9AL/WuUioE4OMdCkZZYoCiqBiS3cBZodcnUoFX4ge8WLKuQyVy/AHJcfAtwAVuGiXTT5fG732TEaGfjxW1HBc+CTA0zVuVv3cOIbvYPUZ8wKVHr6yaovPH56WioWKEEd8xRbJcFRZk8Ue0mownmixSmd6EYD8gw4KdjPz4Jpo/rP7GNOZ1/jE81fw4vsf8aIYgJISeXmCh2tPvPthy0HTfh4wlLvD3GPX0Z0c7oEyv43diOv82LPvxVfqlVPicgPPOt0ASmgcXb++QktlvrfbynIfmBfF8dvEfAAAA//8BAAD//9nkHK6kPQAA") - assets["assets/lang/lang-en.json"], _ = base64.StdEncoding.DecodeString("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") + assets["assets/lang/lang-en.json"], _ = base64.StdEncoding.DecodeString("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") assets["assets/lang/lang-es-ES.json"], _ = base64.StdEncoding.DecodeString("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") assets["assets/lang/lang-es.json"], _ = base64.StdEncoding.DecodeString("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") assets["assets/lang/lang-fi.json"], _ = base64.StdEncoding.DecodeString("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") @@ -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") @@ -79,7 +79,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("H4sIAAAJbogA/9x9+3fbNtLo7/0rGN3cSq4VOekjZ9eut8f1o+uvSZwTJ+13jzebQ0mQxIYiVT7seJvcv/3O4EESxAAEZae79+PpaWRyAAwGg5nBYDAIk2UZh9lknc7LmI2G+W0yK1ZRspzM0owNd74I4IHfySJajhYlfIzSZPQwTmch/nqZpdfRnGU7wR8cEh/j42RVrOPvnqdzNiqyku1MVmG+epmxRfRhNHww3DngRT/VbRVZGscsGw0vFTbH1cvhOFBoBKOH+SzdsHHwcFUUm3Hd8jh4Br9idsmy62gGAKfXLCnyJpbDMmdBXmTRrBgefFG93tsLNll0HRZsb8XiDcuCOeCZRFhrXoNdhxnAsesTAAwOg8cH2pckvI6WgEiyPLoJb+H7IoxzpsOkSRwljP6WsbwIMyxff68Aqt4jVjVdRs3O4aNRYBKWRXrMR1G8H+0caNA5K86TAoDDWFJ1AgMEiKzGwZPH8LTgBUEnHM9mXZ90UpbTOJrt8fpoQsq2Zul6EzPercPgj08H5ndE3fXtPEFmAQhkMQomYZxsOV0Jy7I0w29Xb41vMDdYTBdb356fwJfh0PgyZ0h3S43i4yv2mxOnRRrD7OmC2mRpkc7S+HgFc5nNTY6qhnOTZgUwbEjXI77DtLyO2I21FoGTBZWcseQUCUmTpNwss3DOzpNFCgBJGccWwlwWYeGkiQMAyLEEtrV8vQbcrUyWMDbnBCRGTHx7nRZhrM937ftxmWUwL16GS5zZTyxQ+Pky+hcHMWtahFHMsaB6z791taJBnXGCAVyZ8AnI5hb4FlZtIBStMdKdKSLo4mZ6W7B8P3g81t4uoGZ8W8uHZs2jmyiZpzc7kyn8OxpO2QJ0TpnEaTjXxHxbtBnyVZ/zn3ZM9B+C1hpU+kHMlctyBjM0H7ia0qUo6AwpPquqgOnDbLYa7UxiqHPHjQWO/1E9mbWWhtl6uB8MT1g8HLc/zKNMfgtG8HunDYFaGwFQBLa/FWk5W+HHN5s5jN6wORQEeuczB3JFBpr7UWpF0PJdoReCbLh5NIuyWcwepY9g6BMrtmEO2ijK3w9p1qlHVWqiixfPzl+cuoYyWgQjqXW//DJ4UCvZNiA+GSvKLNF1XkO34YOESmOcGcvR8M35Ba962Bx1URHXope30J91W+vKj0Ix2z9K1cWlngXqpBacFoizWnKO2khy+2myZMWozOJpCJbRbjDcyznOe1JmDncmuZgwo5rGwFEhRT5D3CJgi5o7Qukqc4Oto+Lvr1+/5ArEE8Hr2Z5QW9sgpynEz4OfIKDUe9vgqKtMB5J2trfWpitgUVurW5WRqgtYfCgbVWtyNPxfCStu0uw9pxj0HuwosC6Hq2jOhjsmdF1jN2y+KgsuPqyQNiWgxMXZmY+8aGmajx+DB4Im9yQzFgslNEiyE3RFrDpkl5X0O23cuog1rBhe08cMaTgOwsxoHmz+U5gnBRgwNyuWBGGANQSgMuMAzYx8EvzKAlx6FStcfoVFmQNR5vB3Ckx2264M3oJkLDMWpGUBVYbFMA9ushTWp1/8uYPVpAR2/F5HDEGg0okkyOEhmJgUnKTKUSCHN+CTfxwkaRGEiaA1f0WWVAOLkqw1D1rd4cQIGHSljdrfDoNvHz9GBdp4+f1h8N1f/+pEeMmSEinUhWIvqdEs5Ss9mmW6pUi7BIp555zilOs5y+RQXL4+en367vjvRy9+Oj3xm27oLJgLBYbDYeoHHL3mKvYKQeQi6q1DU5jAfKyZVEKTIjW7bS/K5JqQv+R/tPQMPsAm5wsQGKJQsArzgI8oiBK0H2Fgxyg0QKYktwHABHGUF0G6oCqSq/koD9IpTuGCcbmzBqkAOhoaSWCJK8srYLBVbqm68hVYCfEttJ2Wy9WEnLuSJnziDiW2Q9t80FZdGqFaS0/1bMlPzy6Oj569O39xcvrf7968PAHm8uQq2mD0bPXk9Jfz49N3J+eXxxcvXpwee7cKXAPjRDhsrhRrT6L52z7Grx+mPdHk0rwDScfEshUxVlxVe8l0Y66r1QNa0fU5Sn7EZTlfsbvq8IAK53N0q+xXgmaCb0xetcqF2sXn1/F3hcDnyePHXa14GDLK9wnmr9u30JCYwrE4STd8uCZldgTW+wYFklVBCyFW5uiaEesLNJBRlk0ZiK6ERSDCsiBUFSUpOphnaDzMTbpDbTcsuAmTAg2hMH/PrSawnjL8ex2+ZyDKZqsUXRPBj2ggsWAOK7WCl6Gqg2LTconVrIN5mSFy3CcbxugBLjfjIE+xlpxLyVmavo8AAUCarAyQKaI1QyGKvxdRBhL1OsqjAsQtiuoCDDZVCwjjDXAQoxELk3ldH4Cu0wxtRDBqFmkJ2gD+lwfhMh0jdpISVD2/l2AHoFvGVDCoLDmKvyCGqI7SWbkGfCYCQ1wQxuGMjfZGP+zDf//8OPnq4B/5Vzt1IfjrH4fwv9HVPw/efrUz+erhzsd/wv/3xsHg4ZMBYQtwcVFXYNMJLVQAuUFd6HAAq0ncaZgk6Q0w7G4wOFiHHx4Bk/FP3zwOvgq+/hb+983Tx4+tlpxFvgCCuw2yfN9s6VGgaoV/cBvAhj8+aB+VTsOII3Nf6k2K71en/9VDelv877VEEl/eCoPKW5WcXTw7OX21HS5tL3+Ni7SEYLgf4TDfEUfQc2fnP727PPrFF8GSOwy5w1N6qBQKvdwfQozuRQl3A27hBGlt8HAaNN/189v4stfFry+eXRydvHv56uKnV6eXl/4WeS53J2iTXGwdkpsU+CxAI4y4qBKjHyWiPoo+qpqr2n5sV6dXCRZnVeGVYxVQ9QRq1ICvsArCRFUFMgbaac63EFBk5BP5AszoCdfn9qIwABGbn2Xp+iKLllHSqMT41Ku6UxB+N6B1GVlj/dWj0k0Zx1r35AvPosJZ1iyLbzoKg1KDpUuR3aLyztISFGW5qapDu6AIYhbmqLA3LJsBZ6IS56oX5PlNWsJaacqaYNNIqOlpzMz1DD6oFFQL3wdPcLkve7pLEnbXHL1dxQvfHwZ23wA+Dcr4qgxerMX8gjWt5iQ+AqN9+S9t5+LT7sy+8canbEWefeqlvQZB6X35rxsO6Lavftghpw3zPp/Uf3WUOEkTVhXAP2h4QuLYVDo+hIBrbd3aPGQPRnWRCtbGWQ07Xuy6qo3QQ9mwiyXluvIFFJRt+lsztdtsq+Y5eZSgbhGmS2Q3CCUrMEq6ivbquXqmGQvf20HouetvBOJj7uyrnwdWf+3gJL3hG8kvJehgbLBZX/Pu8s3z50ev/s89ueas7jJl4uTleh1mt22PB1hjs2f6ZnwPp4fsyvHF85fPTl+fX7y4R0fjA3Ol37RXnRYeWYKwZ0jOsBQnqVrDtmxY7CKoYYzuMOXcLCn0sA98Kkk2W28as7VH95FqWBj9CQPh8BjYZijGxUVJexvOpAk+2I/dwy7yQMuEKYdd3TV0cR+6T0RPcNsQ8NjDGvvy5+mrVxevepjcPjONdrjWPuncjaS2jsCdHm35MpabWeNgxUKMkBoHYoliWfWJTRhtV+kPrGc/aNa23651v139vvz3U1MEHOiBinM2BbNxBjoI/q/WHBVE1Q/N7TuitRRW955hyM1AA8f1qShBiAW9/SsoT04GAgzHR77t2OHmpCVXofPpniDiDwLBwyG8ZgluOr55dX4MLAyWTVKoDvdZn7bGVIhynbXsmrFTkrcfTb3p1B9L4o8DuTqnyvsujCt4Efk55pv/1q1jfIiRs8SEVjQ1fQv0dEGGW4X5sYr/fBDlp+tNcXsxRX+J7qk1vBKt2FHxg5QNLVfvO9wbYsmR8HlfFjjfaUgNcPJbGiUjmMpGyALdyjJOp2F8lCScehhkBjPb1RpZQGvV7u3KjVrlexJV+Q0EZXYazlbNycC/HC/o0ANTMShwWeX5yf24/GkKK7TzNCsUpoBLmLUZ2AhnFb+eh5uW919CtIoL9psAp+cjvaodgmR2i5+UudYtFNmEEjHbDlKj4eNqoEbmQCmpQsmGtjxosx4X/NXEdbCKVITa/ow9TsWutlSAXVvdujyTQi9s45KUcd/ceMDf1iETLVmVgYQK9RmtImyFbfKhUBP+4ufgB431TICdScySZbEK9tvWKj582a3ii6mt5sqkzTkmaNUSbTiW3A9M8CtRl3PVKpCabMp8NRLgW24f2Il6pvot2gJ+NU1fQsMKvhqQmtXb1UyyLaH+TdvGM/oaHzoCmzfajsLmSLVmbO2XaaxluElDDByNlvAUNVYe0iJ6KwD5LrMxrJaqOM7Wqs7w6xZSwpB3Y4s7hkd1cAi+OGtM+y2DyazGaa0rfxANcgNVtg0wX9672Wpbq9vXqY1xMgtYdh2s89Ks4cp3jY6Pc52OD7lWx8djve4mQZ/lOsfEumSnCYSPfenuXLWrjpsrd7opa/3k4t1LUtZzC6WlNrvGDlrfp0ilA+b7mASN+JxedgE/9pfiual6+5xmzwJ10AhhH1VHCHeA0HytRbPdnBiBxulDqIsAwB0jS7CBiFZDKcuji6CK52GxmqzDDyNY7o2073VoEOBrBjG9NsCwL8Xcor4bVYvQJUfbWlySo3ENztr6p2AWFrNVMCKjgS1kIYSIrRdU3IV91uldqKMq+S6g6Rmpzyg0y1okXDRXZlu3oQYW+sUNHtTdsKy4haLODYKt5JmVDVXPrqL5Wzsjyq8dnGBAOblQa9nOh+qzT9t+TMjJ1MmIJGEsrEh3xQLcw3jWD8zSixdaCVTlPofNzNvgoD3lOpNB271XetXBYNJR7CKFQPOzUUEdGuup3aBMLzKYjjqLi9HCDVBiOxL07dV/XoCRfeya26uEwxP6iAd2q54OcJ2A28jKhT1wrgV0dLEysAoHX25UvKDtyLKtHMvoourE8IF9qPouS7baMqc4Dyns5xbfhFnOXvAGbaz96d7Gvz4AzLfoqcFfnIhAeDxo89enB8T3KAMJm2a4C/Pk6Td/+ZZwvfHaJ4s4XObBl8FI1bnbKA2aHtcx5CfrEled9W3NiDrmQW9YVq615K7cv+YaWVG5D/LidDHZhL0Qlyp9fA0GRxGjvGbZkvTCceGjQhQIz24EEo+cPfB+UtxueOywKj8weZnDhTOZaqPBj7xiE14gKjxzBEjbEczR/71kJZTZEnlR+t+EOh4j2xZxLPvno91OXiGKO2CoJUdVOZmyQIj6RoKRii6oGCwuSyojxsbQMk2FSAh/y6623gTHq5G+ooGeUF1ODBsFJfg9Ylin1uhJPSrTh46btbWt6WHkATHo0Y4sMM9f+e3YU+bcgG/H5HvCLzTYwozTk8fQa5bKBygdrLUbsFG6wwXagKy8ZnGYF5d4xOeQny1Ez8zIA9x21LS75El4m3NHkmptp16cOluUrib45y9PvyWPi3iZ9o0xv+MSZxx8/R3PL0Wx1pmW7+eOrCWswW1YS0871MFaRmBro7RHuGoDutrpxdE7IwxGN6pG8UlYEAzaUeIOO3EU4zRau3fGqTJ78JY00Wc55NqVl8Uj9YpyR7hFc9XrMtfwEh+InXrkCLQs0gUVX4T79Pz0JJ44rlI7kWeOlR1bJu8TPOzu3KbCo3iLglU56IIqISAG3eKkCcRBcDxLPWUBw2AcA+8HdozFOfI744mt1PWqQAi59/09LIzcLeSrMANydTbh6EeUXIdxNHe1kxfpZtPVjDoxxHjStHqNbaXdgYGlLI6sIP1cPkjhWMdsGc5ux1U2Dn72ErN1gKUykJADlGQI8NObc8sqiEAkmsdsiAdHHN1BO4rv5AZ/o4/TKpzTskgX5DrsCwK8RSXrbDwGIec5GdUglY0oquaUbpQkVuNVWg9JFufwCNVkXXASdaqEA5jxpPl6FiaJLRGBam6TRRhk7t/cYT1BXfVGySLtWamck65ab8KM96hPxYrfNeocNnmq/aV7Ds3Rwja8ICQztilBcCMi8VKc3BKGflfkmFs19FEKT9oGoElLqgUZiMh3POyH4e1tGHNrMyuqk3FkgxF3xIoG92iQBk5tPc3x4Ps7izgFywKac6vr2oJuqWtrYF2DVAJGWK55o4RST10Eg6HD82ne2qm5F0WEW3b4WjtiNWsJ7gas4gYO+Tja4w7VrCDEOe+m67B60yVnygFBJQfNQOGdRLkkF5uT03XeAHBPW0kEQ4l8VhaBLrzh3NFbRv5P4h5aSfJ++rAPrfQEmRxE82Gfbmkv+owR435cIw4zJ4kWvu0aMdM+xAIOeYOfJzK/i0sbqD7+4DUvmhmd/3O62Ug0vRsM/7fTklD9dXcXdOz8REYKGh01wwW58x8DAJoB+VUgdxQXrJnM0dWfpKIFmnvqt9tZjGSSrStR88C9WCLSBlspJWu+evzWg0FehGuKYu6VcAXTw8YZDLpMnJq7EkDKUZUO6EERk28neTnNiwyDTJ66bRBYdecnNKnII3CKB43ibb+FQc8+xBxJ618W3fEkrh9l+5F1C5qyeVRcsgKzMuVOioKsfy7yK2EuQnR/QHnd0aHovN5cbFSgjLpOAYHpNFL07k1dyaTMTpNwKkLVR9TnKgsVLpq7atPmGsEZBMXNSvDygDcid6wTtwacukzg704sf3pz7ibasoyo7K9yBKlcQ9T6Krxm1QEy9ySaoWQJ/uvy4sUE74ZIltHi1jhk1i4EI2tmzManOrRJmyN46KWARd+j1zAZec7rzSaORFrxvd/yNBl22Sct0nDX9ybNXbE/Y+zjmONM+cFJH/jnCb5pjFC/ABze9W2dxjR79JIJOeajzdM1E37RmdjZM3mp2vF7oDic/V6GcU4LhrE58XZ4DltnYZggY31CEcJeYmJZTxyv2Ow9wlnT1jE56TFZXZTz3641ASnM6pUALcy+d6S8dRbkCwZ71rOmO3orxCyuSQ+8HpGR+M4hQAn6SKbpvg/6m5Lbvy+kND/0BQTOffJ1z/WZNw7krqWTsOqWlOAmimMehMFzIbJqlvLgPL7l4V7uqlmHd0G8fnYJ5vOhPv/khw46m7e2mCnW7d06AkVxy/fxlCoM+LGo2Mxga9P55EjW1oKJCSm0LNq7IcK6KgJS2isR4syo4WqoHbvGo8/kyejhWyKU5z0jQ8SsXVSJCOiPg3cYFokpEXaDwWWRDd5OclDixWjvKhi/3d0DCyXcNFr/4E47xK3bDxMwPYw9yYojLMdxbXSurR8z/3u3ZTXAjNiDbgV6NL8OMQOAU4FqJKxJGsqyx/bz+fZOINaqgl5Yqw1O9yZx45IDc4Ja8o63MXSaZbK8M2NA626rtlFyeRPhgYIbNsVonVrUgdwSkUo2119LCNnONVUVwiIR+zIkmM0lI20s366X9OeZTQGPvo7WLC0Lj5wg4oahSXVbT6NN9ZNo1IwsUI9VfBOJ/z+52E8p+Tuy3zr8Lc3eVBeMdFybIRrdmlddF5mQCwev+WFHjuyH75UnfWp0CgrZ6+dIabd4s41Hh/iUtxDciRWc49a458B/4IzLEbZccTV66rypz+KtsbpWCUehakZETVYlH2I+ApbMR398GtceORpFbBKIfPoBDAwrsYlmJu+kA53JBC6156/60JkeRqswZzHfZjgjb9xrFNP3kqi6+uUk8cGmcWLbpI8zN8spEDjNJg9Bir/MOJEN/yRyXz30nrMoopzw1AyN5r83rhXJV+nN0F0zjJ9P1S4nCd9UxAsm5IWqzYeYkhlbAmXoowxNgqpaATFVhLINzSbxHg2fhFr0fLIbsNok2A+G81tYakTtK+maD+7HYAnAA+DXrAjR+eMoEOGFp/NyxqB+rnPtoC2e3YcJRFvUtKFtlQiEqm+V8Wb1qlwny1fIdgUu4Bkbz6lAtEgqyKYPpUWObbNhaPte97cR1liN6/LLb3tMt2Bt2B20DfDzJJjhjL9hQRxi8CRMAybuM5AbLOswKXF5Pgac1zBn+bdomaRZGLdry9gszeYTB2K8IJNSyczrpX82aekMWozm2xBRe1EFQouT6Fq4HBkArUPwo8wG7YmPnlxyL5xCcQvN0/RS1bJsvueJatQsESGNgi6l94LdnFQbzIb5Zd+C5LuzxDgowu6r5CAGhL/68FYbHuqiS03QVpdB4w7DEpT1OsqlD6667UER05NRiF0sZ0DFHJN2B5GOEGGZavaqNgrSmzUcD708WU4PliEoED1an4pEFtzTG0TB9231IEIX4NPubkd8kixxFb1tRkscEhvzXod+sKImsWhtLvtFe3TxsSTNdkYfiaQdKX2bYYs+/OygjSGdqtg7c+IddCTUiRqSX1/w/7GKNFmon16sy7ckkI2lrQWuLHeO4SMOJOGtEU7D1TLhKK3bMfmaGNtraU3IHvOxxlh0yj7L1LNlinrytbjPCZv2PoqlU49PTXfP2oqySRV7L/yPaXlswX1GUwwfyhzrnE+unjolZ1/DjFbVHjaQfoXhXC9fHcV0uzG4YBKFmD2u0Lmz0pZuDW1gtxctuyt3Nl5SvPCuFsMuDyfniftfCJr5WTvW0PVeaWe8UDQO2jkK72a6JIYmBUlp8ju/7li3aBq97VoFdc8fB3VAR/sMZ70eaEiOJjO0WU0TkMSutXH4shEJmLvdyOiRfoa3vfrzn0j0ZLKfK4YR8MXbC/9WTSvGklNx261GXTf3zWIWZqcq61TnZmrVSE1lgfuV3hMZ7fsoePKWo9ln/4BXsccR6/CVLrKIJfP4luKQvDDczbVFtO18qYP2PGZNzj3z8P/qqseZlu5aqwKjIPB7m/HIgFSo000XrjJ9ebCGbicX79BbMgWObAdTy7RhHt5gjPZoKF0BwoCcbMJipd2KmrCb6zAuDW7nckd+w8gp9Vuu9yazVZgdFaPHO/wE4v8lI4l1Z0QDg3oQBd8Bn8I3YMR2KyLcF9YPoyfOwXH65KdZepMzvHfDwBBWPeEaHQGBRHK/QsHMBdU/TUVrmMxsAne5whBdRSpxls/JVmo02gE5zcOtbW6wjuYkj1FuP3rCeUEfWPx+yZDMQIctecTa4uNx8KiDL6ga1JqqFl2W/Ta9WGWoGDtspKXi0/RVot2V0L3BZh0HDIICXHCl3TiJZQGReYwwt3GRhflqFiZkdmOyGlUCj3P/UrdqW56RdSy0specKFyv1fh41qTgj0FloUW129V1MeO57k3SssDkKi2SG9e63Q+l8wjPI/nTWcB/HipLXDzrEdA/M7bpQeD3AP4nEbYIl0sGq6AetFVFPhN5K4x8KawKPA8/HPGD4Y0jzL4UX4uyMtuPIxyTbprPIBV623cmqXJ9G5X0y186BL7Z5HWj1J/DYxhWkiWWJPBkNarE5+GwCh/PmhT8cbpeh9yZ5ju6ogBJZU9K2HuRpElbCHmo77bI9wD6+BHXHPySfTbPA5D7QVLGcc4vo69Ow+UepkNTFHZBQKvfedgV1hnYBxraMq+Ev/vco4GhMTxq2fbVt6UYkD/gmXNinkDoXyzDS/1XEY9gDfIVv684SYtArtHa9d1E0JbYN1iEZVwEwi5PF9DV7ybBZRqkCVSds0KDgskfFcO8XVs4K/iuQz3eE8O00lNtdMjo7pOUnrIeetN7EnhO6DYYNXLW6BdrPJxYJfSI7xLYeMZ3hfM5sazxXM2Yg9AyuT03fXUyZgxT7KjJdgntPPWZausoOYny92cZYy9F2hOPQmkmejLIYMDS9cCjSC8J6yVTvQRJUxb2EnR2zu8nIO8o8drHxT0nmlcxPM70Is3WIP7+Re8QW6cdsXf35867o2R+iRmaCLeCiqq1zEa5eyCqqXcPWmW7yUdO5Gi+r7J8+0xyE6g1i/dhEptA+pzdD56YIHyG7lcTlL6Fy5yP+3I6mvDa7CPv9arn2n7wHfFZn1n7OLEcUNo82ufTyAHdnDXQBwL/1hyhgbQZsc8nRG853HafyH02+rzMf9wE42nPRIlfwbQhNp68JtlCD8zu2m9u7zE7/Uv67uKiCkj38VM3e+q5zegtKCyxUh52Qp2m9RhPw1dxUj7DSUbeVZVZbC4LdGNzvZ0IvoZp83djp4/2D7aSHJ+fYLiJtT5buIkdgYZ38lC0b9txNDraFf/Q5ktLxINPDIBeLd+Zt3aJSNpYw9oMqU5PaV3FdXOtT/d+qLJAqCotQfDDl3wNPrRlleAwTUcm1jms7udGbDTNQtPXpK5JcIOotBO4R0GOkNV34zciDo/qduMhKrzLaKDX0xyFWnvf4xBQ/mHvYj+7vLOe1He6XfsNQKEGQNVpG4ONxxgIP6gahaZwa60/vpKeUvu5kqHm3CQGlrSoHPU1PZeO6pom132yDO327lNSkK5PiWO3f9he8BfNy3s3ZnX6b7cTFqrKOwlvYTCbnNCyqO+RB2i/dI+Cx708wkY115Y26Ug+3datUqHX9d3dfPW3X+0BhC07todNrwcSLvR6VL8x48SjQXVXsDuukK8ycsIyrmhGmciYKMRtm9q3v+me4dPICteM//QIYOP4aBFijVgdRa0qH2O7kZ12KA+nlXPlIxrkMfF0/JkA4Ad7B+OgI+WEQOSsZkC/Uxw1x5rjoF05IQz9Fi9bgqasUb2qqrd0KL/WbCNsq0d0eD3+dwwDlygLhlB4W+Kg8XEEgnfdZvGpHeQpWnZwhgTpYAic58SsNM8f9FmM3v95TUog8QDwAweYSM9tAN3Vm0DenD+6q8xGOp3zuGl32OVnOAsrR1a2/mNZFClmXQiLIhsNVcIvTBtQ/SazdhB3y4tA8FxdG2m5Qp50quFVwEZvemb343n8BA4yzk399fGjHp1I0YPmdOvknkBnONCcJZNpLko1ghqtp9zVwy+WB3vmCKQEikV9WPgn0KHhkEoL03xUHROwbEeNTsv0D/+Ase2qotV27Ul0lvKhgv2wvw0HuzPT9hAZB2g0MfnCHbGsiL1IZ6URU05jZ4ehc2uZ08A3f4JlZosDbkf6/O5/1h9l279JZNHh4neWOFQwrqhx3zEf+UTbUSdVcX5ZbTqKiqw4enn+M7vVaDgzTTGMFQ83kYAUuzyXIgr5m6+7XPzpzZtXLzMwadhN52FvIwebPZ1TJuusZydMrP7TvyunrgcVQ5728s0rr915LYUekcsTAzOehRms94NiFYrLh2L8Oy8CpnZsRJ7SQK4c/XV/TTw7gUlTbRZHCbt7D9tZQT8HsshweGElsdKzYKzfuCxtsbbdZdxdbSIq6rkrP5I4WTLRGwV6gKqrUIl8ohqcfg/nk14zA8fijN+s6T8a2u2fjSUaf622+Azai8/3Q/s2BrZrANx9X4W56Lq45cvvZqMHZG8d566cqfdUrWSl9XUrzgt5OhuQcNruNXUWEoRVFs19rngiNSy/B15V4ncXfIfUnmLQY5dC4lA9U2b5ZwPVs3+2j55oAtUn9acPegjyiqsPL50sCmWtAp3Z60RgyVEcUw4f//HO+e6iR0tW516/tu6Dr6bleoNTnnR52i6+B1xQ0OgYbbIo9cMIS3yJdVvhsF2dg0ReChFbV6TBUsaMZtFyBZYFjjPekQFLOHUFIlAaasuZHiSKmO8eQvOYf5U3b9MeB5Zy4qJls6i6jJla76sB7HMErSEJde2qVL5tKaPd5KqGdzAOKj+2eY+reozr8LudsFvn8pfWoLDLvRdFspT616EJht13eab0XRDDeZjdRAlu6jwPZ8HFZfDfxB7RcJ6FS5B18S0CnvA/zuLb4MfLEwoaDDI2zflOEYamWaCgi4mEuoCfFqiEFRLoBSssMGABlx8Q5Bn/QUCIfLs85uBX+ZOAAlYKs4hDXcqfOtSnq9bQpPlbdBoZbw/MAQizGWh0YgS++ctTbPGbr4NpVFBohev5028R5Om3VpBsjQBHr553YYxoUDjj+wPyqkiaG3FlPQ5wIQ2cBb+DEf7mnYS/dtyXU0XJpixwb/Kslbv2PbsdB9YjvvC1cacOP8F5GAzfuS8ufR9tOi/mlKH0ImSfV5qU6ylz37YpQfrXPU1T3GZ2Vj7DGwKm6Yctqk+nuCN3N6IoQPRmWIYSlNQmZ+U8DdZhlEzU9bwwivAj4GZ5JJLxh3keLZNGvm/8gpfMZCkUydTCEo32/wcAAP//AQAA//9WvR/v+MkAAA==") + 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/discover/discover.go b/lib/discover/discover.go index df6359e46..8f6f320a5 100644 --- a/lib/discover/discover.go +++ b/lib/discover/discover.go @@ -22,13 +22,14 @@ import ( "github.com/syncthing/syncthing/lib/beacon" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/osutil" + "github.com/syncthing/syncthing/lib/relay" "github.com/syncthing/syncthing/lib/sync" ) type Discoverer struct { myID protocol.DeviceID listenAddrs []string - relays []Relay + relaySvc *relay.Svc localBcastIntv time.Duration localBcastStart time.Time cacheLifetime time.Duration @@ -56,11 +57,11 @@ var ( ErrIncorrectMagic = errors.New("incorrect magic number") ) -func NewDiscoverer(id protocol.DeviceID, addresses []string, relayAdresses []string) *Discoverer { +func NewDiscoverer(id protocol.DeviceID, addresses []string, relaySvc *relay.Svc) *Discoverer { return &Discoverer{ myID: id, listenAddrs: addresses, - relays: measureLatency(relayAdresses), + relaySvc: relaySvc, localBcastIntv: 30 * time.Second, cacheLifetime: 5 * time.Minute, negCacheCutoff: 3 * time.Minute, @@ -143,7 +144,7 @@ func (d *Discoverer) StartGlobal(servers []string, extPort uint16) { } d.extPort = extPort - pkt := d.announcementPkt() + pkt := d.announcementPkt(true) wg := sync.NewWaitGroup() clients := make(chan Client, len(servers)) for _, address := range servers { @@ -317,49 +318,32 @@ func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry { return devices } -func (d *Discoverer) announcementPkt() *Announce { +func (d *Discoverer) announcementPkt(allowExternal bool) *Announce { var addrs []string - if d.extPort != 0 { + if d.extPort != 0 && allowExternal { addrs = []string{fmt.Sprintf("tcp://:%d", d.extPort)} } else { - for _, aurl := range d.listenAddrs { - uri, err := url.Parse(aurl) - if err != nil { - if debug { - l.Debugf("discovery: failed to parse listen address %s: %s", aurl, err) - } - continue + addrs = resolveAddrs(d.listenAddrs) + } + + relayAddrs := make([]string, 0) + if d.relaySvc != nil { + status := d.relaySvc.ClientStatus() + for uri, ok := range status { + if ok { + relayAddrs = append(relayAddrs, uri) } - 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", aurl, uri.Host) - } - if len(addr.IP) == 0 || addr.IP.IsUnspecified() { - uri.Host = fmt.Sprintf(":%d", addr.Port) - } else if bs := addr.IP.To4(); bs != nil { - uri.Host = fmt.Sprintf("%s:%d", bs.String(), addr.Port) - } else if bs := addr.IP.To16(); bs != nil { - uri.Host = fmt.Sprintf("[%s]:%d", bs.String(), addr.Port) - } - addrs = append(addrs, uri.String()) } } + return &Announce{ Magic: AnnouncementMagic, - This: Device{d.myID[:], addrs, d.relays}, + This: Device{d.myID[:], addrs, measureLatency(relayAddrs)}, } } func (d *Discoverer) sendLocalAnnouncements() { - var addrs = resolveAddrs(d.listenAddrs) - - var pkt = Announce{ - Magic: AnnouncementMagic, - This: Device{d.myID[:], addrs, d.relays}, - } + var pkt = d.announcementPkt(false) msg := pkt.MustMarshalXDR() for { diff --git a/lib/model/connection.go b/lib/model/connection.go index 9fb79923d..194a71a16 100644 --- a/lib/model/connection.go +++ b/lib/model/connection.go @@ -7,11 +7,17 @@ package model import ( + "crypto/tls" "net" "github.com/syncthing/protocol" ) +type IntermediateConnection struct { + Conn *tls.Conn + ConnType ConnectionType +} + type Connection struct { net.Conn protocol.Connection diff --git a/lib/model/model.go b/lib/model/model.go index a276913c6..617615e0e 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -219,6 +219,7 @@ type ConnectionInfo struct { protocol.Statistics Address string ClientVersion string + Type ConnectionType } func (info ConnectionInfo) MarshalJSON() ([]byte, error) { @@ -227,6 +228,7 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) { "inBytesTotal": info.InBytesTotal, "outBytesTotal": info.OutBytesTotal, "address": info.Address, + "type": info.Type.String(), "clientVersion": info.ClientVersion, }) } @@ -249,6 +251,7 @@ func (m *Model) ConnectionStats() map[string]interface{} { } if addr := m.conn[device].RemoteAddr(); addr != nil { ci.Address = addr.String() + ci.Type = conn.Type } conns[device.String()] = ci @@ -585,6 +588,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon } if conn, ok := m.conn[deviceID]; ok { + event["type"] = conn.Type.String() addr := conn.RemoteAddr() if addr != nil { event["addr"] = addr.String() diff --git a/lib/osutil/osutil.go b/lib/osutil/osutil.go index 64802518e..f4e15885a 100644 --- a/lib/osutil/osutil.go +++ b/lib/osutil/osutil.go @@ -11,10 +11,12 @@ import ( "errors" "fmt" "io" + "net" "os" "path/filepath" "runtime" "strings" + "time" "github.com/calmh/du" "github.com/syncthing/syncthing/lib/sync" @@ -221,3 +223,21 @@ func DiskFreePercentage(path string) (freePct float64, err error) { u, err := du.Get(path) return (float64(u.FreeBytes) / float64(u.TotalBytes)) * 100, err } + +// SetTCPOptions sets syncthings default TCP options on a TCP connection +func SetTCPOptions(conn *net.TCPConn) error { + var err error + if err = conn.SetLinger(0); err != nil { + return err + } + if err = conn.SetNoDelay(false); err != nil { + return err + } + if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil { + return err + } + if err = conn.SetKeepAlive(true); err != nil { + return err + } + return nil +} diff --git a/lib/relay/debug.go b/lib/relay/debug.go new file mode 100644 index 000000000..b1841a026 --- /dev/null +++ b/lib/relay/debug.go @@ -0,0 +1,19 @@ +// 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 relay + +import ( + "os" + "strings" + + "github.com/calmh/logger" +) + +var ( + debug = strings.Contains(os.Getenv("STTRACE"), "relay") || os.Getenv("STTRACE") == "all" + l = logger.DefaultLogger +) diff --git a/cmd/syncthing/relays.go b/lib/relay/relay.go similarity index 83% rename from cmd/syncthing/relays.go rename to lib/relay/relay.go index af3da4bdd..59461ad20 100644 --- a/cmd/syncthing/relays.go +++ b/lib/relay/relay.go @@ -4,7 +4,7 @@ // 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 +package relay import ( "crypto/tls" @@ -16,16 +16,17 @@ import ( "github.com/syncthing/relaysrv/protocol" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/model" + "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/sync" "github.com/thejerf/suture" ) -func newRelaySvc(cfg *config.Wrapper, tlsCfg *tls.Config, conns chan<- intermediateConnection) *relaySvc { - svc := &relaySvc{ - Supervisor: suture.New("relaySvc", suture.Spec{ +func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) *Svc { + svc := &Svc{ + Supervisor: suture.New("Svc", suture.Spec{ Log: func(log string) { - if debugNet { + if debug { l.Infoln(log) } }, @@ -58,7 +59,7 @@ func newRelaySvc(cfg *config.Wrapper, tlsCfg *tls.Config, conns chan<- intermedi return svc } -type relaySvc struct { +type Svc struct { *suture.Supervisor cfg *config.Wrapper tlsCfg *tls.Config @@ -70,7 +71,7 @@ type relaySvc struct { invitations chan protocol.SessionInvitation } -func (s *relaySvc) VerifyConfiguration(from, to config.Configuration) error { +func (s *Svc) VerifyConfiguration(from, to config.Configuration) error { for _, addr := range to.Options.RelayServers { _, err := url.Parse(addr) if err != nil { @@ -80,12 +81,12 @@ func (s *relaySvc) VerifyConfiguration(from, to config.Configuration) error { return nil } -func (s *relaySvc) CommitConfiguration(from, to config.Configuration) bool { +func (s *Svc) CommitConfiguration(from, to config.Configuration) bool { existing := make(map[string]struct{}, len(to.Options.RelayServers)) for _, addr := range to.Options.RelayServers { uri, err := url.Parse(addr) if err != nil { - if debugNet { + if debug { l.Debugln("Failed to parse relay address", addr, err) } continue @@ -95,7 +96,7 @@ func (s *relaySvc) CommitConfiguration(from, to config.Configuration) bool { _, ok := s.tokens[uri.String()] if !ok { - if debugNet { + if debug { l.Debugln("Connecting to relay", uri) } c := client.NewProtocolClient(uri, s.tlsCfg.Certificates, s.invitations) @@ -114,7 +115,7 @@ func (s *relaySvc) CommitConfiguration(from, to config.Configuration) bool { s.mut.Lock() delete(s.clients, uri) s.mut.Unlock() - if debugNet { + if debug { l.Debugln("Disconnecting from relay", uri, err) } } @@ -123,7 +124,7 @@ func (s *relaySvc) CommitConfiguration(from, to config.Configuration) bool { return true } -func (s *relaySvc) ClientStatus() map[string]bool { +func (s *Svc) ClientStatus() map[string]bool { s.mut.RLock() status := make(map[string]bool, len(s.clients)) for uri, client := range s.clients { @@ -136,7 +137,7 @@ func (s *relaySvc) ClientStatus() map[string]bool { type invitationReceiver struct { invitations chan protocol.SessionInvitation tlsCfg *tls.Config - conns chan<- intermediateConnection + conns chan<- model.IntermediateConnection stop chan struct{} } @@ -149,18 +150,21 @@ func (r *invitationReceiver) Serve() { for { select { case inv := <-r.invitations: - if debugNet { + if debug { l.Debugln("Received relay invitation", inv) } conn, err := client.JoinSession(inv) if err != nil { - if debugNet { + if debug { l.Debugf("Failed to join relay session %s: %v", inv, err) } continue } - setTCPOptions(conn.(*net.TCPConn)) + err = osutil.SetTCPOptions(conn.(*net.TCPConn)) + if err != nil { + l.Infoln(err) + } var tc *tls.Conn @@ -175,7 +179,7 @@ func (r *invitationReceiver) Serve() { tc.Close() continue } - r.conns <- intermediateConnection{ + r.conns <- model.IntermediateConnection{ tc, model.ConnectionTypeRelayAccept, } case <-r.stop: From 687fbb0a7e8116c0c19947559ee07cb6ccb0923a Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Thu, 23 Jul 2015 20:01:25 +0100 Subject: [PATCH 08/11] Discovery clients now take an announcer, global discovery is delayed --- cmd/syncthing/connections.go | 5 +---- cmd/syncthing/main.go | 10 ++++++++-- lib/discover/client.go | 10 +++++++--- lib/discover/client_test.go | 22 ++++++++++++++++++---- lib/discover/client_udp.go | 30 ++++++++++++++++++------------ lib/discover/discover.go | 13 ++++++++----- lib/discover/discover_test.go | 4 ++-- 7 files changed, 62 insertions(+), 32 deletions(-) diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index f6f8bd7f6..d881729da 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -250,11 +250,8 @@ func (s *connectionSvc) connect() { if addr == "dynamic" { if discoverer != nil { t, r := discoverer.Lookup(deviceID) - relays = append(relays, r...) - if len(t) == 0 { - continue - } addrs = append(addrs, t...) + relays = append(relays, r...) } } else { addrs = append(addrs, addr) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 41c3c883e..18c4ac164 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -920,8 +920,14 @@ func discovery(extPort int, relaySvc *relay.Svc) *discover.Discoverer { } if opts.GlobalAnnEnabled { - l.Infoln("Starting global discovery announcements") - disc.StartGlobal(opts.GlobalAnnServers, uint16(extPort)) + 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, uint16(extPort)) + }() + } return disc diff --git a/lib/discover/client.go b/lib/discover/client.go index 64f43d8cf..a9bee2d56 100644 --- a/lib/discover/client.go +++ b/lib/discover/client.go @@ -14,7 +14,11 @@ import ( "github.com/syncthing/protocol" ) -type Factory func(*url.URL, *Announce) (Client, error) +type Announcer interface { + Announcement() Announce +} + +type Factory func(*url.URL, Announcer) (Client, error) var ( factories = make(map[string]Factory) @@ -26,7 +30,7 @@ func Register(proto string, factory Factory) { factories[proto] = factory } -func New(addr string, pkt *Announce) (Client, error) { +func New(addr string, announcer Announcer) (Client, error) { uri, err := url.Parse(addr) if err != nil { return nil, err @@ -35,7 +39,7 @@ func New(addr string, pkt *Announce) (Client, error) { if !ok { return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme) } - client, err := factory(uri, pkt) + client, err := factory(uri, announcer) if err != nil { return nil, err } diff --git a/lib/discover/client_test.go b/lib/discover/client_test.go index 9c3592760..15897584a 100644 --- a/lib/discover/client_test.go +++ b/lib/discover/client_test.go @@ -24,6 +24,14 @@ 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 { @@ -33,7 +41,7 @@ func TestUDP4Success(t *testing.T) { port := conn.LocalAddr().(*net.UDPAddr).Port address := fmt.Sprintf("udp4://127.0.0.1:%d", port) - pkt := &Announce{ + pkt := Announce{ Magic: AnnouncementMagic, This: Device{ device[:], @@ -41,8 +49,11 @@ func TestUDP4Success(t *testing.T) { nil, }, } + ann := &FakeAnnouncer{ + pkt: pkt, + } - client, err := New(address, pkt) + client, err := New(address, ann) if err != nil { t.Fatal(err) } @@ -137,7 +148,7 @@ func TestUDP4Failure(t *testing.T) { address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port) - pkt := &Announce{ + pkt := Announce{ Magic: AnnouncementMagic, This: Device{ device[:], @@ -145,8 +156,11 @@ func TestUDP4Failure(t *testing.T) { nil, }, } + ann := &FakeAnnouncer{ + pkt: pkt, + } - client, err := New(address, pkt) + client, err := New(address, ann) if err != nil { t.Fatal(err) } diff --git a/lib/discover/client_udp.go b/lib/discover/client_udp.go index 8283e595d..3a65c9e2e 100644 --- a/lib/discover/client_udp.go +++ b/lib/discover/client_udp.go @@ -20,12 +20,13 @@ import ( func init() { for _, proto := range []string{"udp", "udp4", "udp6"} { - Register(proto, func(uri *url.URL, pkt *Announce) (Client, error) { + Register(proto, func(uri *url.URL, announcer Announcer) (Client, error) { c := &UDPClient{ - wg: sync.NewWaitGroup(), - mut: sync.NewRWMutex(), + announcer: announcer, + wg: sync.NewWaitGroup(), + mut: sync.NewRWMutex(), } - err := c.Start(uri, pkt) + err := c.Start(uri) if err != nil { return nil, err } @@ -37,22 +38,20 @@ func init() { type UDPClient struct { url *url.URL - id protocol.DeviceID - 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, pkt *Announce) error { +func (d *UDPClient) Start(uri *url.URL) error { d.url = uri - d.id = protocol.DeviceIDFromBytes(pkt.This.ID) d.stop = make(chan struct{}) params := uri.Query() @@ -79,11 +78,11 @@ func (d *UDPClient) Start(uri *url.URL, pkt *Announce) error { } d.wg.Add(1) - go d.broadcast(pkt.MustMarshalXDR()) + go d.broadcast() return nil } -func (d *UDPClient) broadcast(pkt []byte) { +func (d *UDPClient) broadcast() { defer d.wg.Done() conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress) @@ -126,7 +125,14 @@ func (d *UDPClient) broadcast(pkt []byte) { l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote) } - _, err := conn.WriteTo(pkt, remote) + ann := d.announcer.Announcement() + pkt, err := ann.MarshalXDR() + if err != nil { + timer.Reset(d.errorRetryInterval) + continue + } + + _, err = conn.WriteTo(pkt, remote) if err != nil { if debug { l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err) @@ -137,7 +143,7 @@ func (d *UDPClient) broadcast(pkt []byte) { time.Sleep(1 * time.Second) - pkt, err := d.Lookup(d.id) + pkt, err := d.Lookup(protocol.DeviceIDFromBytes(ann.This.ID)) if err != nil && debug { l.Debugf("discover %s: broadcast: Self-lookup failed: %v", d.url, err) } else if debug { diff --git a/lib/discover/discover.go b/lib/discover/discover.go index 8f6f320a5..fa3fc5837 100644 --- a/lib/discover/discover.go +++ b/lib/discover/discover.go @@ -144,14 +144,13 @@ func (d *Discoverer) StartGlobal(servers []string, extPort uint16) { } d.extPort = extPort - pkt := d.announcementPkt(true) 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, pkt) + client, err := New(addr, d) if err != nil { l.Infoln("Error creating discovery client", addr, err) return @@ -318,7 +317,11 @@ func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry { return devices } -func (d *Discoverer) announcementPkt(allowExternal bool) *Announce { +func (d *Discoverer) Announcement() Announce { + return d.announcementPkt(true) +} + +func (d *Discoverer) announcementPkt(allowExternal bool) Announce { var addrs []string if d.extPort != 0 && allowExternal { addrs = []string{fmt.Sprintf("tcp://:%d", d.extPort)} @@ -326,7 +329,7 @@ func (d *Discoverer) announcementPkt(allowExternal bool) *Announce { addrs = resolveAddrs(d.listenAddrs) } - relayAddrs := make([]string, 0) + var relayAddrs []string if d.relaySvc != nil { status := d.relaySvc.ClientStatus() for uri, ok := range status { @@ -336,7 +339,7 @@ func (d *Discoverer) announcementPkt(allowExternal bool) *Announce { } } - return &Announce{ + return Announce{ Magic: AnnouncementMagic, This: Device{d.myID[:], addrs, measureLatency(relayAddrs)}, } diff --git a/lib/discover/discover_test.go b/lib/discover/discover_test.go index a6469ea1a..8d4f81cc5 100644 --- a/lib/discover/discover_test.go +++ b/lib/discover/discover_test.go @@ -84,14 +84,14 @@ func TestGlobalDiscovery(t *testing.T) { clients := []*DummyClient{c1, c2} - Register("test1", func(uri *url.URL, pkt *Announce) (Client, error) { + 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, pkt *Announce) (Client, error) { + Register("test2", func(uri *url.URL, ann Announcer) (Client, error) { c3.url = uri return c3, nil }) From 6cccd9b6fc69492e96e48bb4b831ae22db04af89 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Thu, 23 Jul 2015 20:39:44 +0100 Subject: [PATCH 09/11] Add dynamic relay lookup (DDoS relays.syncthing.net!) --- lib/config/config.go | 2 +- lib/config/config_test.go | 1 + lib/relay/relay.go | 73 +++++++++++++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/lib/config/config.go b/lib/config/config.go index 394a2bfb3..786068832 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -219,7 +219,7 @@ type OptionsConfiguration struct { 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:""` + RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net"` MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"` MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"` ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"` diff --git a/lib/config/config_test.go b/lib/config/config_test.go index a9c245bce..e9c41075e 100644 --- a/lib/config/config_test.go +++ b/lib/config/config_test.go @@ -37,6 +37,7 @@ func TestDefaultValues(t *testing.T) { LocalAnnEnabled: true, LocalAnnPort: 21025, LocalAnnMCAddr: "[ff32::5222]:21026", + RelayServers: []string{"dynamic+https://relays.syncthing.net"}, MaxSendKbps: 0, MaxRecvKbps: 0, ReconnectIntervalS: 60, diff --git a/lib/relay/relay.go b/lib/relay/relay.go index 59461ad20..ec7974f95 100644 --- a/lib/relay/relay.go +++ b/lib/relay/relay.go @@ -8,7 +8,9 @@ package relay import ( "crypto/tls" + "encoding/json" "net" + "net/http" "net/url" "time" @@ -82,7 +84,8 @@ func (s *Svc) VerifyConfiguration(from, to config.Configuration) error { } func (s *Svc) CommitConfiguration(from, to config.Configuration) bool { - existing := make(map[string]struct{}, len(to.Options.RelayServers)) + existing := make(map[string]*url.URL, len(to.Options.RelayServers)) + for _, addr := range to.Options.RelayServers { uri, err := url.Parse(addr) if err != nil { @@ -91,32 +94,74 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool { } continue } + existing[uri.String()] = uri + } - existing[uri.String()] = struct{}{} + // Expand dynamic addresses into a set of relays + for key, uri := range existing { + if uri.Scheme != "dynamic+http" && uri.Scheme != "dynamic+https" { + continue + } + delete(existing, key) - _, ok := s.tokens[uri.String()] + uri.Scheme = uri.Scheme[8:] + + data, err := http.Get(uri.String()) + if err != nil { + if debug { + l.Debugln("Failed to lookup dynamic relays", err) + } + continue + } + + var ann dynamicAnnouncement + err = json.NewDecoder(data.Body).Decode(&ann) + data.Body.Close() + if err != nil { + if debug { + l.Debugln("Failed to lookup dynamic relays", err) + } + continue + } + for _, relayAnn := range ann.Relays { + ruri, err := url.Parse(relayAnn.URL) + if err != nil { + if debug { + l.Debugln("Failed to parse dynamic relay address", relayAnn.URL, err) + } + continue + } + if debug { + l.Debugln("Found", ruri, "via", uri) + } + existing[ruri.String()] = ruri + } + } + + for key, uri := range existing { + _, ok := s.tokens[key] if !ok { if debug { l.Debugln("Connecting to relay", uri) } c := client.NewProtocolClient(uri, s.tlsCfg.Certificates, s.invitations) - s.tokens[uri.String()] = s.Add(c) + s.tokens[key] = s.Add(c) s.mut.Lock() - s.clients[uri.String()] = c + s.clients[key] = c s.mut.Unlock() } } - for uri, token := range s.tokens { - _, ok := existing[uri] + for key, token := range s.tokens { + _, ok := existing[key] if !ok { err := s.Remove(token) - delete(s.tokens, uri) + delete(s.tokens, key) s.mut.Lock() - delete(s.clients, uri) + delete(s.clients, key) s.mut.Unlock() if debug { - l.Debugln("Disconnecting from relay", uri, err) + l.Debugln("Disconnecting from relay", key, err) } } } @@ -195,3 +240,11 @@ func (r *invitationReceiver) Stop() { r.stop <- struct{}{} r.stop = nil } + +type dynamicAnnouncement struct { + Relays []relayAnnouncement `json:"relays"` +} + +type relayAnnouncement struct { + URL string `json:"url"` +} From 031804827fc8acb7bec1a8f8b676f57ecbfcc29c Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Fri, 24 Jul 2015 19:55:52 +0100 Subject: [PATCH 10/11] Do not start relay service unless explicitly asked for, or global announcement server is running --- cmd/syncthing/main.go | 9 ++++++--- lib/config/config.go | 1 + lib/config/config_test.go | 2 ++ lib/config/testdata/overridenvalues.xml | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 18c4ac164..6409c9da1 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -630,7 +630,7 @@ func syncthingMain() { m.StartDeadlockDetector(time.Duration(it) * time.Second) } } else if !IsRelease || IsBeta { - m.StartDeadlockDetector(20 * 60 * time.Second) + m.StartDeadlockDetector(20 * time.Minute) } // Clear out old indexes for other devices. Otherwise we'll start up and @@ -674,10 +674,13 @@ func syncthingMain() { // Start the relevant services connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg) - relaySvc = relay.NewSvc(cfg, tlsCfg, connectionSvc.conns) - connectionSvc.Add(relaySvc) mainSvc.Add(connectionSvc) + if opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn { + relaySvc = relay.NewSvc(cfg, tlsCfg, connectionSvc.conns) + connectionSvc.Add(relaySvc) + } + // Start discovery localPort := addr.Port diff --git a/lib/config/config.go b/lib/config/config.go index 786068832..289ba1569 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -224,6 +224,7 @@ type OptionsConfiguration struct { MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"` ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"` RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"` + RelayWithoutGlobalAnn bool `xml:"relayWithoutGlobalAnn" json:"relayWithoutGlobalAnn" default:"false"` StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"` UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"` UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"` diff --git a/lib/config/config_test.go b/lib/config/config_test.go index e9c41075e..ac741e518 100644 --- a/lib/config/config_test.go +++ b/lib/config/config_test.go @@ -42,6 +42,7 @@ func TestDefaultValues(t *testing.T) { MaxRecvKbps: 0, ReconnectIntervalS: 60, RelayReconnectIntervalM: 10, + RelayWithoutGlobalAnn: false, StartBrowser: true, UPnPEnabled: true, UPnPLeaseM: 60, @@ -155,6 +156,7 @@ func TestOverriddenValues(t *testing.T) { MaxRecvKbps: 2341, ReconnectIntervalS: 6000, RelayReconnectIntervalM: 20, + RelayWithoutGlobalAnn: true, StartBrowser: false, UPnPEnabled: false, UPnPLeaseM: 90, diff --git a/lib/config/testdata/overridenvalues.xml b/lib/config/testdata/overridenvalues.xml index c6d026fd1..c9e06e530 100755 --- a/lib/config/testdata/overridenvalues.xml +++ b/lib/config/testdata/overridenvalues.xml @@ -14,6 +14,7 @@ 2341 6000 20 + true false false 90 From 1e8b185377a333cb88ee03dc0e410a0b3b294fcf Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Fri, 24 Jul 2015 20:07:26 +0100 Subject: [PATCH 11/11] Add switch to disable relays --- cmd/syncthing/connections.go | 14 +++++++++++--- cmd/syncthing/main.go | 2 +- lib/config/config.go | 1 + lib/config/config_test.go | 2 ++ lib/config/testdata/overridenvalues.xml | 1 + 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cmd/syncthing/connections.go b/cmd/syncthing/connections.go index d881729da..2ad8e86c8 100644 --- a/cmd/syncthing/connections.go +++ b/cmd/syncthing/connections.go @@ -45,8 +45,9 @@ type connectionSvc struct { lastRelayCheck map[protocol.DeviceID]time.Time - mut sync.RWMutex - connType map[protocol.DeviceID]model.ConnectionType + mut sync.RWMutex + connType map[protocol.DeviceID]model.ConnectionType + relaysEnabled bool } func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config) *connectionSvc { @@ -59,6 +60,7 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Mo conns: make(chan model.IntermediateConnection), connType: make(map[protocol.DeviceID]model.ConnectionType), + relaysEnabled: cfg.Options().RelaysEnabled, lastRelayCheck: make(map[protocol.DeviceID]time.Time), } cfg.Subscribe(svc) @@ -239,6 +241,7 @@ func (s *connectionSvc) connect() { s.mut.RLock() ct, ok := s.connType[deviceID] + relaysEnabled := s.relaysEnabled s.mut.RUnlock() if connected && ok && ct.IsDirect() { continue @@ -296,7 +299,8 @@ func (s *connectionSvc) connect() { // Also, do not set lastRelayCheck time if we have no relays, // as otherwise when we do discover relays, we might have to // wait up to RelayReconnectIntervalM to connect again. - if connected || len(relays) == 0 { + // Also, do not try relays if we are explicitly told not to. + if connected || len(relays) == 0 || !relaysEnabled { continue nextDevice } @@ -394,6 +398,10 @@ func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error } func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool { + s.mut.Lock() + s.relaysEnabled = to.Options.RelaysEnabled + s.mut.Unlock() + // We require a restart if a device as been removed. newDevices := make(map[protocol.DeviceID]bool, len(to.Devices)) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 6409c9da1..bc18cdaf7 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -676,7 +676,7 @@ func syncthingMain() { connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg) mainSvc.Add(connectionSvc) - if opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn { + if opts.RelaysEnabled && (opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn) { relaySvc = relay.NewSvc(cfg, tlsCfg, connectionSvc.conns) connectionSvc.Add(relaySvc) } diff --git a/lib/config/config.go b/lib/config/config.go index 289ba1569..40ebdf655 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -223,6 +223,7 @@ type OptionsConfiguration struct { MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"` MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"` ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"` + RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"` RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"` RelayWithoutGlobalAnn bool `xml:"relayWithoutGlobalAnn" json:"relayWithoutGlobalAnn" default:"false"` StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"` diff --git a/lib/config/config_test.go b/lib/config/config_test.go index ac741e518..06fbe9f3a 100644 --- a/lib/config/config_test.go +++ b/lib/config/config_test.go @@ -41,6 +41,7 @@ func TestDefaultValues(t *testing.T) { MaxSendKbps: 0, MaxRecvKbps: 0, ReconnectIntervalS: 60, + RelaysEnabled: true, RelayReconnectIntervalM: 10, RelayWithoutGlobalAnn: false, StartBrowser: true, @@ -155,6 +156,7 @@ func TestOverriddenValues(t *testing.T) { MaxSendKbps: 1234, MaxRecvKbps: 2341, ReconnectIntervalS: 6000, + RelaysEnabled: false, RelayReconnectIntervalM: 20, RelayWithoutGlobalAnn: true, StartBrowser: false, diff --git a/lib/config/testdata/overridenvalues.xml b/lib/config/testdata/overridenvalues.xml index c9e06e530..41a08154c 100755 --- a/lib/config/testdata/overridenvalues.xml +++ b/lib/config/testdata/overridenvalues.xml @@ -13,6 +13,7 @@ 1234 2341 6000 + false 20 true false