Archived
1 parent
a7169a6348
commit
b0cd7be39b
41 files changed
+1936
-1503
No files matched your search
Generated
+3
-3
@@ -43,11 +43,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/syncthing/relaysrv/client",
|
"ImportPath": "github.com/syncthing/relaysrv/client",
|
||||||
"Rev": "7fe1fdd8c751df165ea825bc8d3e895f118bb236"
|
"Rev": "6e126fb97e2ff566d35f8d8824e86793d22b2147"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/syncthing/relaysrv/protocol",
|
"ImportPath": "github.com/syncthing/relaysrv/protocol",
|
||||||
"Rev": "7fe1fdd8c751df165ea825bc8d3e895f118bb236"
|
"Rev": "6e126fb97e2ff566d35f8d8824e86793d22b2147"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/thejerf/suture",
|
"ImportPath": "github.com/thejerf/suture",
|
||||||
"Rev": "fc7aaeabdc43fe41c5328efa1479ffea0b820978"
|
"Rev": "860b44045335c64a6d54ac7eed22a3aedfc687c9"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/vitrun/qart/coding",
|
"ImportPath": "github.com/vitrun/qart/coding",
|
||||||
|
|||||||
+19
-1
@@ -32,6 +32,7 @@ type ProtocolClient struct {
|
|||||||
|
|
||||||
mut sync.RWMutex
|
mut sync.RWMutex
|
||||||
connected bool
|
connected bool
|
||||||
|
latency time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient {
|
func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient {
|
||||||
@@ -168,6 +169,13 @@ func (c *ProtocolClient) StatusOK() bool {
|
|||||||
return con
|
return con
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) Latency() time.Duration {
|
||||||
|
c.mut.RLock()
|
||||||
|
lat := c.latency
|
||||||
|
c.mut.RUnlock()
|
||||||
|
return lat
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ProtocolClient) String() string {
|
func (c *ProtocolClient) String() string {
|
||||||
return fmt.Sprintf("ProtocolClient@%p", c)
|
return fmt.Sprintf("ProtocolClient@%p", c)
|
||||||
}
|
}
|
||||||
@@ -177,11 +185,21 @@ func (c *ProtocolClient) connect() error {
|
|||||||
return fmt.Errorf("Unsupported relay schema:", c.URI.Scheme)
|
return fmt.Errorf("Unsupported relay schema:", c.URI.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := tls.Dial("tcp", c.URI.Host, c.config)
|
t0 := time.Now()
|
||||||
|
tcpConn, err := net.Dial("tcp", c.URI.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mut.Lock()
|
||||||
|
c.latency = time.Since(t0)
|
||||||
|
c.mut.Unlock()
|
||||||
|
|
||||||
|
conn := tls.Client(tcpConn, c.config)
|
||||||
|
if err = conn.Handshake(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return err
|
return err
|
||||||
|
|||||||
+25
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
syncthingprotocol "github.com/syncthing/protocol"
|
syncthingprotocol "github.com/syncthing/protocol"
|
||||||
@@ -20,10 +21,10 @@ func GetInvitationFromRelay(uri *url.URL, id syncthingprotocol.DeviceID, certs [
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs))
|
conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs))
|
||||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return protocol.SessionInvitation{}, err
|
return protocol.SessionInvitation{}, err
|
||||||
}
|
}
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
|
||||||
if err := performHandshakeAndValidation(conn, uri); err != nil {
|
if err := performHandshakeAndValidation(conn, uri); err != nil {
|
||||||
return protocol.SessionInvitation{}, err
|
return protocol.SessionInvitation{}, err
|
||||||
@@ -97,6 +98,29 @@ func JoinSession(invitation protocol.SessionInvitation) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRelay(uri *url.URL, certs []tls.Certificate, sleep time.Duration, times int) bool {
|
||||||
|
id := syncthingprotocol.NewDeviceID(certs[0].Certificate[0])
|
||||||
|
invs := make(chan protocol.SessionInvitation, 1)
|
||||||
|
c := NewProtocolClient(uri, certs, invs)
|
||||||
|
go c.Serve()
|
||||||
|
defer func() {
|
||||||
|
close(invs)
|
||||||
|
c.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < times; i++ {
|
||||||
|
_, err := GetInvitationFromRelay(uri, id, certs)
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Incorrect response code") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
time.Sleep(sleep)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func configForCerts(certs []tls.Certificate) *tls.Config {
|
func configForCerts(certs []tls.Certificate) *tls.Config {
|
||||||
return &tls.Config{
|
return &tls.Config{
|
||||||
Certificates: certs,
|
Certificates: certs,
|
||||||
|
|||||||
+2
-1
@@ -7,8 +7,9 @@ package protocol
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
syncthingprotocol "github.com/syncthing/protocol"
|
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
syncthingprotocol "github.com/syncthing/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+2
-8
@@ -6,10 +6,8 @@ Suture
|
|||||||
Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" ->
|
Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" ->
|
||||||
"sutree" -> "suture" -> holds your code together when it's trying to die.
|
"sutree" -> "suture" -> holds your code together when it's trying to die.
|
||||||
|
|
||||||
This is intended to be a production-quality library going into code that I
|
This library has hit maturity, and isn't expected to be changed
|
||||||
will be very early on the phone tree to support when it goes down. However,
|
radically. This can also be imported via gopkg.in/thejerf/suture.v1 .
|
||||||
it has not been deployed into something quite that serious yet. (I will
|
|
||||||
update this statement when that changes.)
|
|
||||||
|
|
||||||
It is intended to deal gracefully with the real failure cases that can
|
It is intended to deal gracefully with the real failure cases that can
|
||||||
occur with supervision trees (such as burning all your CPU time endlessly
|
occur with supervision trees (such as burning all your CPU time endlessly
|
||||||
@@ -24,10 +22,6 @@ This module is fully covered with [godoc](http://godoc.org/github.com/thejerf/su
|
|||||||
including an example, usage, and everything else you might expect from a
|
including an example, usage, and everything else you might expect from a
|
||||||
README.md on GitHub. (DRY.)
|
README.md on GitHub. (DRY.)
|
||||||
|
|
||||||
This is not currently tagged with particular git tags for Go as this is
|
|
||||||
currently considered to be alpha code. As I move this into production and
|
|
||||||
feel more confident about it, I'll give it relevant tags.
|
|
||||||
|
|
||||||
Code Signing
|
Code Signing
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
+80
-16
@@ -7,41 +7,105 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
var timeout = 5 * time.Second
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
|
|
||||||
|
func main() {
|
||||||
var server string
|
var server string
|
||||||
|
|
||||||
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22027", "Announce server")
|
flag.StringVar(&server, "server", "", "Announce server (blank for default set)")
|
||||||
|
flag.DurationVar(&timeout, "timeout", timeout, "Query timeout")
|
||||||
|
flag.Usage = usage
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(flag.Args()) != 1 || server == "" {
|
if flag.NArg() != 1 {
|
||||||
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22027\"] <device>", os.Args[0])
|
flag.Usage()
|
||||||
os.Exit(64)
|
os.Exit(64)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := protocol.DeviceIDFromString(flag.Args()[0])
|
id, err := protocol.DeviceIDFromString(flag.Args()[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil, nil)
|
if server != "" {
|
||||||
discoverer.StartGlobal([]string{server}, nil)
|
checkServers(id, server)
|
||||||
addresses, relays := discoverer.Lookup(id)
|
} else {
|
||||||
for _, addr := range addresses {
|
checkServers(id, config.DefaultDiscoveryServers...)
|
||||||
log.Println("address:", addr)
|
|
||||||
}
|
|
||||||
for _, addr := range relays {
|
|
||||||
log.Println("relay:", addr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type checkResult struct {
|
||||||
|
server string
|
||||||
|
direct []string
|
||||||
|
relays []discover.Relay
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServers(deviceID protocol.DeviceID, servers ...string) {
|
||||||
|
t0 := time.Now()
|
||||||
|
resc := make(chan checkResult)
|
||||||
|
for _, srv := range servers {
|
||||||
|
srv := srv
|
||||||
|
go func() {
|
||||||
|
res := checkServer(deviceID, srv)
|
||||||
|
res.server = srv
|
||||||
|
resc <- res
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ = range servers {
|
||||||
|
res := <-resc
|
||||||
|
|
||||||
|
u, _ := url.Parse(res.server)
|
||||||
|
fmt.Printf("%s (%v):\n", u.Host, time.Since(t0))
|
||||||
|
|
||||||
|
if res.error != nil {
|
||||||
|
fmt.Println(" " + res.error.Error())
|
||||||
|
}
|
||||||
|
for _, addr := range res.direct {
|
||||||
|
fmt.Println(" address:", addr)
|
||||||
|
}
|
||||||
|
for _, rel := range res.relays {
|
||||||
|
fmt.Printf(" relay: %s (%d ms)\n", rel.URL, rel.Latency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServer(deviceID protocol.DeviceID, server string) checkResult {
|
||||||
|
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{error: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make(chan checkResult, 1)
|
||||||
|
|
||||||
|
time.AfterFunc(timeout, func() {
|
||||||
|
res <- checkResult{error: errors.New("timeout")}
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
direct, relays, err := disco.Lookup(deviceID)
|
||||||
|
res <- checkResult{direct: direct, relays: relays, error: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return <-res
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Printf("Usage:\n\t%s [options] <device ID>\n\nOptions:\n", os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
@@ -32,6 +32,17 @@ func newAddressLister(upnpSvc *upnpSvc, cfg *config.Wrapper) *addressLister {
|
|||||||
// port number - this means that the outside address of a NAT gateway should
|
// port number - this means that the outside address of a NAT gateway should
|
||||||
// be substituted.
|
// be substituted.
|
||||||
func (e *addressLister) ExternalAddresses() []string {
|
func (e *addressLister) ExternalAddresses() []string {
|
||||||
|
return e.addresses(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllAddresses returns a list of addresses that are our best guess for where
|
||||||
|
// we are reachable from the local network. Same conditions as
|
||||||
|
// ExternalAddresses, but private IPv4 addresses are included.
|
||||||
|
func (e *addressLister) AllAddresses() []string {
|
||||||
|
return e.addresses(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
|
||||||
var addrs []string
|
var addrs []string
|
||||||
|
|
||||||
// Grab our listen addresses from the config. Unspecified ones are passed
|
// Grab our listen addresses from the config. Unspecified ones are passed
|
||||||
@@ -56,6 +67,9 @@ func (e *addressLister) ExternalAddresses() []string {
|
|||||||
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
|
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
|
||||||
// A public address; include as is.
|
// A public address; include as is.
|
||||||
addrs = append(addrs, "tcp://"+addr.String())
|
addrs = append(addrs, "tcp://"+addr.String())
|
||||||
|
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
|
||||||
|
// A private IPv4 address.
|
||||||
|
addrs = append(addrs, "tcp://"+addr.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +81,6 @@ func (e *addressLister) ExternalAddresses() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infoln("External addresses:", addrs)
|
|
||||||
|
|
||||||
return addrs
|
return addrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ type connectionSvc struct {
|
|||||||
myID protocol.DeviceID
|
myID protocol.DeviceID
|
||||||
model *model.Model
|
model *model.Model
|
||||||
tlsCfg *tls.Config
|
tlsCfg *tls.Config
|
||||||
discoverer *discover.Discoverer
|
discoverer discover.Finder
|
||||||
conns chan model.IntermediateConnection
|
conns chan model.IntermediateConnection
|
||||||
relaySvc *relay.Svc
|
relaySvc *relay.Svc
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ type connectionSvc struct {
|
|||||||
relaysEnabled bool
|
relaysEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config, discoverer *discover.Discoverer, relaySvc *relay.Svc) *connectionSvc {
|
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config, discoverer discover.Finder, relaySvc *relay.Svc) *connectionSvc {
|
||||||
svc := &connectionSvc{
|
svc := &connectionSvc{
|
||||||
Supervisor: suture.NewSimple("connectionSvc"),
|
Supervisor: suture.NewSimple("connectionSvc"),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -264,13 +264,14 @@ func (s *connectionSvc) connect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var addrs []string
|
var addrs []string
|
||||||
var relays []string
|
var relays []discover.Relay
|
||||||
for _, addr := range deviceCfg.Addresses {
|
for _, addr := range deviceCfg.Addresses {
|
||||||
if addr == "dynamic" {
|
if addr == "dynamic" {
|
||||||
if s.discoverer != nil {
|
if s.discoverer != nil {
|
||||||
t, r := s.discoverer.Lookup(deviceID)
|
if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
|
||||||
addrs = append(addrs, t...)
|
addrs = append(addrs, t...)
|
||||||
relays = append(relays, r...)
|
relays = append(relays, r...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addrs = append(addrs, addr)
|
addrs = append(addrs, addr)
|
||||||
@@ -333,7 +334,7 @@ func (s *connectionSvc) connect() {
|
|||||||
s.lastRelayCheck[deviceID] = time.Now()
|
s.lastRelayCheck[deviceID] = time.Now()
|
||||||
|
|
||||||
for _, addr := range relays {
|
for _, addr := range relays {
|
||||||
uri, err := url.Parse(addr)
|
uri, err := url.Parse(addr.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Infoln("Failed to parse relay connection url:", addr, err)
|
l.Infoln("Failed to parse relay connection url:", addr, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
+31
-19
@@ -33,6 +33,7 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
@@ -58,14 +59,15 @@ type apiSvc struct {
|
|||||||
assetDir string
|
assetDir string
|
||||||
model *model.Model
|
model *model.Model
|
||||||
eventSub *events.BufferedSubscription
|
eventSub *events.BufferedSubscription
|
||||||
discoverer *discover.Discoverer
|
discoverer *discover.CachingMux
|
||||||
|
relaySvc *relay.Svc
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
fss *folderSummarySvc
|
fss *folderSummarySvc
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
systemConfigMut sync.Mutex
|
systemConfigMut sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.Discoverer) (*apiSvc, error) {
|
func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc) (*apiSvc, error) {
|
||||||
svc := &apiSvc{
|
svc := &apiSvc{
|
||||||
id: id,
|
id: id,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -73,6 +75,7 @@ func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir strin
|
|||||||
model: m,
|
model: m,
|
||||||
eventSub: eventSub,
|
eventSub: eventSub,
|
||||||
discoverer: discoverer,
|
discoverer: discoverer,
|
||||||
|
relaySvc: relaySvc,
|
||||||
systemConfigMut: sync.NewMutex(),
|
systemConfigMut: sync.NewMutex(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +167,6 @@ func (s *apiSvc) Serve() {
|
|||||||
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
|
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
|
||||||
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
|
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
|
||||||
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
|
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
|
||||||
postRestMux.HandleFunc("/rest/system/discovery", s.postSystemDiscovery) // device addr
|
|
||||||
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
|
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
|
||||||
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
|
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
|
||||||
postRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
postRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
||||||
@@ -630,11 +632,30 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
res["alloc"] = m.Alloc
|
res["alloc"] = m.Alloc
|
||||||
res["sys"] = m.Sys - m.HeapReleased
|
res["sys"] = m.Sys - m.HeapReleased
|
||||||
res["tilde"] = tilde
|
res["tilde"] = tilde
|
||||||
if cfg.Options().GlobalAnnEnabled && s.discoverer != nil {
|
if cfg.Options().LocalAnnEnabled || cfg.Options().GlobalAnnEnabled {
|
||||||
res["extAnnounceOK"] = s.discoverer.ExtAnnounceOK()
|
res["discoveryEnabled"] = true
|
||||||
|
discoErrors := make(map[string]string)
|
||||||
|
discoMethods := 0
|
||||||
|
for disco, err := range s.discoverer.ChildErrors() {
|
||||||
|
discoMethods++
|
||||||
|
if err != nil {
|
||||||
|
discoErrors[disco] = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res["discoveryMethods"] = discoMethods
|
||||||
|
res["discoveryErrors"] = discoErrors
|
||||||
}
|
}
|
||||||
if relaySvc != nil {
|
if s.relaySvc != nil {
|
||||||
res["relayClientStatus"] = relaySvc.ClientStatus()
|
res["relaysEnabled"] = true
|
||||||
|
relayClientStatus := make(map[string]bool)
|
||||||
|
relayClientLatency := make(map[string]int)
|
||||||
|
for _, relay := range s.relaySvc.Relays() {
|
||||||
|
latency, ok := s.relaySvc.RelayStatus(relay)
|
||||||
|
relayClientStatus[relay] = ok
|
||||||
|
relayClientLatency[relay] = int(latency / time.Millisecond)
|
||||||
|
}
|
||||||
|
res["relayClientStatus"] = relayClientStatus
|
||||||
|
res["relayClientLatency"] = relayClientLatency
|
||||||
}
|
}
|
||||||
cpuUsageLock.RLock()
|
cpuUsageLock.RLock()
|
||||||
var cpusum float64
|
var cpusum float64
|
||||||
@@ -679,25 +700,16 @@ func (s *apiSvc) showGuiError(l logger.LogLevel, err string) {
|
|||||||
guiErrorsMut.Unlock()
|
guiErrorsMut.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) postSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var qs = r.URL.Query()
|
|
||||||
var device = qs.Get("device")
|
|
||||||
var addr = qs.Get("addr")
|
|
||||||
if len(device) != 0 && len(addr) != 0 && s.discoverer != nil {
|
|
||||||
s.discoverer.Hint(device, []string{addr})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
devices := map[string][]discover.CacheEntry{}
|
devices := make(map[string]discover.CacheEntry)
|
||||||
|
|
||||||
if s.discoverer != nil {
|
if s.discoverer != nil {
|
||||||
// Device ids can't be marshalled as keys so we need to manually
|
// Device ids can't be marshalled as keys so we need to manually
|
||||||
// rebuild this map using strings. Discoverer may be nil if discovery
|
// rebuild this map using strings. Discoverer may be nil if discovery
|
||||||
// has not started yet.
|
// has not started yet.
|
||||||
for device, entries := range s.discoverer.All() {
|
for device, entry := range s.discoverer.Cache() {
|
||||||
devices[device.String()] = entries
|
devices[device.String()] = entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-36
@@ -114,7 +114,6 @@ var (
|
|||||||
writeRateLimit *ratelimit.Bucket
|
writeRateLimit *ratelimit.Bucket
|
||||||
readRateLimit *ratelimit.Bucket
|
readRateLimit *ratelimit.Bucket
|
||||||
stop = make(chan int)
|
stop = make(chan int)
|
||||||
relaySvc *relay.Svc
|
|
||||||
cert tls.Certificate
|
cert tls.Certificate
|
||||||
lans []*net.IPNet
|
lans []*net.IPNet
|
||||||
)
|
)
|
||||||
@@ -689,8 +688,7 @@ func syncthingMain() {
|
|||||||
|
|
||||||
var addrList *addressLister
|
var addrList *addressLister
|
||||||
|
|
||||||
// Start UPnP. The UPnP service will restart global discovery if the
|
// Start UPnP
|
||||||
// external port changes.
|
|
||||||
|
|
||||||
if opts.UPnPEnabled {
|
if opts.UPnPEnabled {
|
||||||
upnpSvc := newUPnPSvc(cfg, addr.Port)
|
upnpSvc := newUPnPSvc(cfg, addr.Port)
|
||||||
@@ -703,14 +701,6 @@ func syncthingMain() {
|
|||||||
addrList = newAddressLister(nil, cfg)
|
addrList = newAddressLister(nil, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start discovery
|
|
||||||
|
|
||||||
discoverer := discovery(addrList, relaySvc)
|
|
||||||
|
|
||||||
// GUI
|
|
||||||
|
|
||||||
setupGUI(mainSvc, cfg, m, apiSub, discoverer)
|
|
||||||
|
|
||||||
// Start relay management
|
// Start relay management
|
||||||
|
|
||||||
var relaySvc *relay.Svc
|
var relaySvc *relay.Svc
|
||||||
@@ -719,9 +709,51 @@ func syncthingMain() {
|
|||||||
mainSvc.Add(relaySvc)
|
mainSvc.Add(relaySvc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start discovery
|
||||||
|
|
||||||
|
cachedDiscovery := discover.NewCachingMux()
|
||||||
|
mainSvc.Add(cachedDiscovery)
|
||||||
|
|
||||||
|
if cfg.Options().GlobalAnnEnabled {
|
||||||
|
for _, srv := range cfg.GlobalDiscoveryServers() {
|
||||||
|
l.Infoln("Using discovery server", srv)
|
||||||
|
gd, err := discover.NewGlobal(srv, cert, addrList, relaySvc)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Global discovery:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each global discovery server gets its results cached for five
|
||||||
|
// minutes, and is not asked again for a minute when it's returned
|
||||||
|
// unsuccessfully.
|
||||||
|
cachedDiscovery.Add(gd, 5*time.Minute, time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Options().LocalAnnEnabled {
|
||||||
|
// v4 broadcasts
|
||||||
|
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relaySvc)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("IPv4 local discovery:", err)
|
||||||
|
} else {
|
||||||
|
cachedDiscovery.Add(bcd, 0, 0)
|
||||||
|
}
|
||||||
|
// v6 multicasts
|
||||||
|
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relaySvc)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("IPv6 local discovery:", err)
|
||||||
|
} else {
|
||||||
|
cachedDiscovery.Add(mcd, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GUI
|
||||||
|
|
||||||
|
setupGUI(mainSvc, cfg, m, apiSub, cachedDiscovery, relaySvc)
|
||||||
|
|
||||||
// Start connection management
|
// Start connection management
|
||||||
|
|
||||||
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg, discoverer, relaySvc)
|
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg, cachedDiscovery, relaySvc)
|
||||||
mainSvc.Add(connectionSvc)
|
mainSvc.Add(connectionSvc)
|
||||||
|
|
||||||
if cpuProfile {
|
if cpuProfile {
|
||||||
@@ -844,7 +876,7 @@ func startAuditing(mainSvc *suture.Supervisor) {
|
|||||||
l.Infoln("Audit log in", auditFile)
|
l.Infoln("Audit log in", auditFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.Discoverer) {
|
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc) {
|
||||||
opts := cfg.Options()
|
opts := cfg.Options()
|
||||||
guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
|
guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
|
||||||
|
|
||||||
@@ -873,7 +905,7 @@ func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, a
|
|||||||
|
|
||||||
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
|
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
|
||||||
l.Infoln("Starting web GUI on", urlShow)
|
l.Infoln("Starting web GUI on", urlShow)
|
||||||
api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub, discoverer)
|
api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub, discoverer, relaySvc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Cannot start GUI:", err)
|
l.Fatalln("Cannot start GUI:", err)
|
||||||
}
|
}
|
||||||
@@ -944,28 +976,6 @@ func shutdown() {
|
|||||||
stop <- exitSuccess
|
stop <- exitSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
func discovery(addrList *addressLister, relaySvc *relay.Svc) *discover.Discoverer {
|
|
||||||
opts := cfg.Options()
|
|
||||||
disc := discover.NewDiscoverer(myID, opts.ListenAddress, relaySvc)
|
|
||||||
if opts.LocalAnnEnabled {
|
|
||||||
l.Infoln("Starting local discovery announcements")
|
|
||||||
disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.GlobalAnnEnabled {
|
|
||||||
go func() {
|
|
||||||
// Defer starting global announce server, giving time to connect
|
|
||||||
// to relay servers.
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
l.Infoln("Starting global discovery announcements")
|
|
||||||
disc.StartGlobal(opts.GlobalAnnServers, addrList)
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return disc
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureDir(dir string, mode int) {
|
func ensureDir(dir string, mode int) {
|
||||||
fi, err := os.Stat(dir)
|
fi, err := os.Stat(dir)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
)
|
)
|
||||||
@@ -139,6 +140,16 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
|||||||
data := ev.Data.(map[string]string)
|
data := ev.Data.(map[string]string)
|
||||||
device := data["device"]
|
device := data["device"]
|
||||||
return fmt.Sprintf("Device %v was resumed", device)
|
return fmt.Sprintf("Device %v was resumed", device)
|
||||||
|
|
||||||
|
case events.ExternalPortMappingChanged:
|
||||||
|
data := ev.Data.(map[string]int)
|
||||||
|
port := data["port"]
|
||||||
|
return fmt.Sprintf("External port mapping changed; new port is %d.", port)
|
||||||
|
case events.RelayStateChanged:
|
||||||
|
data := ev.Data.(map[string][]string)
|
||||||
|
newRelays := data["new"]
|
||||||
|
return fmt.Sprintf("Relay state changed; connected relay(s) are %s.", strings.Join(newRelays, ", "))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %#v", ev.Type, ev)
|
return fmt.Sprintf("%s %#v", ev.Type, ev)
|
||||||
|
|||||||
@@ -249,3 +249,7 @@ ul.three-columns li, ul.two-columns li {
|
|||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
+13
-13
@@ -379,28 +379,28 @@
|
|||||||
<th><span class="fa fa-fw fa-tachometer"></span> <span translate>CPU Utilization</span></th>
|
<th><span class="fa fa-fw fa-tachometer"></span> <span translate>CPU Utilization</span></th>
|
||||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="system.extAnnounceOK != undefined && announceServersTotal > 0">
|
<tr ng-if="system.discoveryEnabled">
|
||||||
<th><span class="fa fa-fw fa-bullhorn"></span> <span translate>Global Discovery</span></th>
|
<th><span class="fa fa-fw fa-map-signs"></span> <span translate>Discovery</span></th>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span ng-if="announceServersFailed.length == 0" class="data text-success">
|
<span ng-if="discoveryFailed.length == 0" class="data text-success">
|
||||||
<span>OK</span>
|
<span>{{discoveryTotal}}/{{discoveryTotal}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="announceServersFailed.length != 0" class="data" ng-class="{'text-danger': announceServersFailed.length == announceServersTotal}">
|
<span ng-if="discoveryFailed.length != 0" class="data" ng-class="{'text-danger': discoveryFailed.length == discoveryTotal}">
|
||||||
<span popover data-trigger="hover" data-placement="bottom" data-content="{{announceServersFailed.join('\n')}}">
|
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{discoveryFailed.join('<br>\n')}}">
|
||||||
{{announceServersTotal-announceServersFailed.length}}/{{announceServersTotal}}
|
{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="system.relayClientStatus != undefined && relayClientsTotal > 0">
|
<tr ng-if="system.relaysEnabled">
|
||||||
<th><span class="fa fa-fw fa-sitemap"></span> <span translate>Relays</span></th>
|
<th><span class="fa fa-fw fa-sitemap"></span> <span translate>Relays</span></th>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span ng-if="relayClientsFailed.length == 0" class="data text-success">
|
<span ng-if="relaysFailed.length == 0" class="data text-success">
|
||||||
<span>OK</span>
|
<span>{{relaysTotal}}/{{relaysTotal}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="relayClientsFailed.length != 0" class="data" ng-class="{'text-danger': relayClientsFailed.length == relayClientsTotal}">
|
<span ng-if="relaysFailed.length != 0" class="data" ng-class="{'text-danger': relaysFailed.length == relaysTotal}">
|
||||||
<span popover data-trigger="hover" data-placement="bottom" data-content="{{relayClientsFailed.join('\n')}}">
|
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{relaysFailed.join('<br>\n')}}">
|
||||||
{{relayClientsTotal-relayClientsFailed.length}}/{{relayClientsTotal}}
|
{{relaysTotal-relaysFailed.length}}/{{relaysTotal}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -378,24 +378,25 @@ angular.module('syncthing.core')
|
|||||||
$scope.myID = data.myID;
|
$scope.myID = data.myID;
|
||||||
$scope.system = data;
|
$scope.system = data;
|
||||||
|
|
||||||
$scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
|
$scope.discoveryTotal = data.discoveryMethods;
|
||||||
var failedAnnounce = [];
|
var discoveryFailed = [];
|
||||||
for (var server in data.extAnnounceOK) {
|
for (var disco in data.discoveryErrors) {
|
||||||
if (!data.extAnnounceOK[server]) {
|
if (data.discoveryErrors[disco]) {
|
||||||
failedAnnounce.push(server);
|
discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$scope.announceServersFailed = failedAnnounce;
|
$scope.discoveryFailed = discoveryFailed;
|
||||||
|
|
||||||
$scope.relayClientsTotal = data.relayClientStatus ? Object.keys(data.relayClientStatus).length : 0;
|
var relaysFailed = [];
|
||||||
var failedRelays = [];
|
var relaysTotal = 0;
|
||||||
for (var relay in data.relayClientStatus) {
|
for (var relay in data.relayClientStatus) {
|
||||||
if (!data.relayClientStatus[relay]) {
|
if (!data.relayClientStatus[relay]) {
|
||||||
failedRelays.push(relay);
|
relaysFailed.push(relay);
|
||||||
}
|
}
|
||||||
|
relaysTotal++;
|
||||||
}
|
}
|
||||||
$scope.relayClientsFailed = failedRelays;
|
$scope.relaysFailed = relaysFailed;
|
||||||
|
$scope.relaysTotal = relaysTotal;
|
||||||
|
|
||||||
console.log("refreshSystem", data);
|
console.log("refreshSystem", data);
|
||||||
}).error($scope.emitHTTPError);
|
}).error($scope.emitHTTPError);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<label translate for="deviceID">Device ID</label>
|
<label translate for="deviceID">Device ID</label>
|
||||||
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
|
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
|
||||||
<datalist id="discovery-list" ng-if="!editingExisting">
|
<datalist id="discovery-list" ng-if="!editingExisting">
|
||||||
<option ng-repeat="(id,address) in discovery" value="{{ id }}" />
|
<option ng-repeat="(id, data) in discovery" value="{{id}}" />
|
||||||
</datalist>
|
</datalist>
|
||||||
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.deviceID}}</div>
|
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.deviceID}}</div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AssetsBuildDate = "Thu, 10 Sep 2015 13:04:55 GMT"
|
AssetsBuildDate = "Fri, 18 Sep 2015 06:05:31 GMT"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Assets() map[string][]byte {
|
func Assets() map[string][]byte {
|
||||||
var assets = make(map[string][]byte, 116)
|
var assets = make(map[string][]byte, 116)
|
||||||
|
|
||||||
assets["assets/css/overrides.css"], _ = base64.StdEncoding.DecodeString("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")
|
assets["assets/css/overrides.css"], _ = base64.StdEncoding.DecodeString("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")
|
||||||
assets["assets/font/raleway-500.woff"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/font/raleway-500.woff"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/font/raleway.css"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3JIy88r0U1LTE5VqOZSUIDycjNzKq0U1IMSc1LLEyvVrWEyxSWVOalWCnn5RbmJOXDR8tTM9IwSKwVTAwOQWHFRspVCTn5yYo4G3ARNHYXSohyNIghXF6hSrzw/LU0TaADQqBINdRBPXdOaq5YLAAAA//8BAAD//xy2WhaSAAAA")
|
assets["assets/font/raleway.css"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3JIy88r0U1LTE5VqOZSUIDycjNzKq0U1IMSc1LLEyvVrWEyxSWVOalWCnn5RbmJOXDR8tTM9IwSKwVTAwOQWHFRspVCTn5yYo4G3ARNHYXSohyNIghXF6hSrzw/LU0TaADQqBINdRBPXdOaq5YLAAAA//8BAAD//xy2WhaSAAAA")
|
||||||
assets["assets/img/favicon.png"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/img/favicon.png"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
@@ -49,7 +49,7 @@ func Assets() map[string][]byte {
|
|||||||
assets["assets/lang/lang-zh-TW.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-zh-TW.json"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/8x7WXMcR3L/+36K/jOC8QciuFju2rsPfLBCK612GVqKtCB6ww5GOBrTNUCbM92z3T2EsAg4BiRxH0NJAEiAEHEQBIcAcZCUCBAHGeGPYk8f87RfwZmV1dXVPTOQLEu29SAOqrKurDx+mVk9+DMN/jv3vmaxXt0zbzHNKhd7mKPZec3QB1zNsJlr/X9PK+o3meYyy2Vd5y5p5+qHFb8yFb1cDZfu+hvPgvnD+uFm/XDGvzce3hv9j8rtcxeSmfth9L/YjnaLOa5pW/DXgGbZntbDtJxdLMG6PQWm9Zten1Zy2C3TLrsxrctX++9PEu/m2mXtYzbA54SfjbEvGl/sy84eu+xhV+P+crBwIptzHs6BHcFXM/XTZdlhGNjorz/yJ1eVRu1DWD7Hkr7o8dfh6a5K8ZFdMJijULwaCxYW/Y23KhGeOc8J3+OLL+wDsaR8TyF1mMv3Vz+d8b+uZDpYy65bupVjfP/yd6ZP+8C28mZv2dHx/CplpiceVyhoH+qezk81UmvcqdFelX67X3vfsq2BIl7OdVfvZdqnrGQ7nmn1vkfjhqPaC3/6nX9vBiSrfvounMNpotqk//CRv/rirycryXylPr2HeWaOj9y5H+zNNFZH/aOqpLA09rnHHEsvoIwUdcvQ+uB/BeZqXh+LxQMW79Iue9AFzbbmsKINeoAEeRNEKu/YRf6XO8APT1dCcvmTLiBP0YZj/NjtWZWMH9AMLpEubJIuDlYBFdItzbQ8xzbKOdB4ogENgnsEpdINA6hgu16f6cadcIB+ViiQCTg+rp/O+8s1/2TeH3kSVUZIzoOJSlTbgR+wK2rx98eC5TtgMuqn3wopHnsV7Gxk9KLs2UWQqJxWLvU6ukFyG41t+VPz/sxY+M1STPlbUHndYfly4f8hifpnTFHu5aPD2pR/Um1M70VbG3HfB9eua9c9s2D+RQo2NhEDJRFcYy8r2JzJwcNvYNvB/SfR1rQkKDBgHxgMTc97pMrZJklpu0yYlcbChGwmeeHzT4/VjzfUDmZ5F7T+PmZpZRduQfdIPjzd8dAy61rBtPicUe04evr4ryfT4fzr6OQQ7gEuBDgdrU0D+xsLU43VnWDxtjJ3CW2COLf/+KvwcDfptCzGjZ32O8ex+ZkalcfhwVKGgXbJRDFFsWUFl8E+HbJ2bx/540+ixfloYyx6fNqK3nbMXhPUhfOrVXsyZgCa+jzt355pv7r4y1+TuthoQ0D00QJ5jtkDIuO4l8RkmQH1wyl//H70cj2cOQbh/OuJlJ8PWYF5tOPxrcbiRrqdjPrBy2wfV4DLH3Kuc8GNdh7AccP1kyyNAddn5s2clC+VPkP8iV5kCQnqcu1FhmTwPGnf+SGtY/C8Tlb9/FCn1q9bHrcoObq5Li12GzTgvWRibXCQ2oZwkkExyRBMEtx5FW0O0z2D34bfQkVpQ/dnFZNLG3KTaWWH6Yo9EPuChQOYTvbauTKKdMKPra3gwWywAGL/OiHqtwq2bmif6nQ3cH/RyUSjshLOjmWJ5CURUbZbGMdg5zHXBpXkd4bJfXx4UItO9tRWxW1TZ/qInCTx24Ik67eRKrV6Zh1LR5Ry/Zp1jR9gfgVsDv0pKcB0kDMBuFXSwceCjmgdN855udKlX/zCLF1C83/j3AUtbuqzXU80dmp67PRBpYDCGLD0opm7cQ7FpMScvO0UNV1aWgPvDbzUAFoV1DAxnBv4/ytbSXHG7LVsh2mA99DvuhfAjzGcjVtEvm1hBt+dhvNPgtpjsP9gHoO9KphEMIxhbVdBp4mdS1m438VO/SP0z/8gvTi/s40FgDbB1lywNh5OjAfLz4PZp/64lOOPdBhjgLtnRa4oqb9jGpz2Whkc7VVHyFNQvedXF8S0YLlTQKbFNs7YABIDR4omt/NajwlGAvyjYJ1BfqVg2zfRkMItaDnu7NwuABgMccFH739GyMQdcHHbnK0/xbTKjmkuBEYcdHS5XgzeBR6i+R1WKugIkmwELNxcaz0DWjcgJ8ApgLPirf5o87XfowH6gD65WIK/5ewm4Kofffs/yVJNJys5tsdNOPlkcX8QaRn8/mzQyhgmguKB820iUbFiDCQhaPQIRzLYlOvF+p0rlEEMCEj7I6/rxwvC5QwvgQIEc2+i3UmIKAE5+qcvGivv/PF9iRmRgCsAqHb9dBTsrOyqH04i8Nyd9KcXAHhG69vh4kmjMgzD/ep64+Fo/XAHg1a+ImiyP66Gq4mBbzLt1BUjgLi3CQQIsiu6K2Bh/fC43VzXdK8vPdvBnv/2izSV23I3v79+Gdvxn6RFAwzdh+CDoAfM77r9tmMISi3amol2Jvy9UWW/LYaBvjrpISK6qIykAQoS/NGEg1paKtjE9vDh42j4NB12/p5ZzBHuPZxbD+cksvh9we4Be/th7AREEOmvrASz6+E36+3otG7m3BKRtEJePzkKlmf8xVpmXLcnVificKoSjEzFNH9ghVKCTmSrXUR300sQfnOhsTocd13mlo9Px71Nuh1vl/uohIDcUZZM2lSV8vlkY/GepLTAC6NZRWwEHC8CFOn42PztL9xOAdLB4xFSglHgC2SvMoHjgG7LCJBuGpMohl7EsHLALjuxAQEiuFO07xDQOszCNmk4wOzY4AcQyHDd/cnmlnuPQ1TOnjjYTLrjjJCwK73mLZBHWAahGDR3mF2sSzNsni1in4PZMRhnm1+diU4nwm83/d2lYP1rwKJahz/zCgIFMDuIg2e/hABA8vBjxkqxE6aUyruvw/nFYOfAr74Ujnj+MKb+o+70orH7yHRcDjiD5Yq/8RQtz90tf2Q8oeNEBYzrcwy2bsTEb6eD2SfB3Gtp61JDXMashBJMXFh9C4FeY+GrhEwYoLA2AySy2c5lFQ127i/vpxWNyKS+EElaXxQSreMz29MLnK2tmsWIKzyDd53Cey65YzPAFIqvE6LPzWK5qL3fK1auNOYPBK9TB7zCPB0co65dtQpkMO6MoHF/c5JOPF0xLT7hRw5jePCbWndJJ6DfriseyfM04LrsEkrXn8usTLbr6TG4k8bSJASYjdXb4bYE+FfKBc/UCuwWK6ADNHK6Y2gdgHBzfegmsbcEd22YqDA22C9OSmrsbyw1lmb9FxvRvedgbf2d+/7IHRDJ6l6wN+fvA6eWsDP2BJKrn7DYAr59FCxvJc39SkgDDE7HM9ideDvszroYoMhKMJA1SfAnNl/73mbS0Ao3h3fX2mPWT2wv3uerWnBXmsirH/Oh60egorItn2/OplyFg2T2Go1PNu31aknmchvDh41VaUWuljkuQUOEneqfzRQJuOcZ7+lg50k48UyJZYG21z7bXo8dnWWvr8KNOiZgKspDUS7rdDeoPiRcFNMhgIixlbCvHIUxAOI5SkqWygiytD8JNJZzGA/jzLwGm8LsvrCL4Ma7tM9gpAdiyxDZOXoOI66Of+3UcrqFgykZBaZbc/sgyMsBS/J2Gi0hFOO2ovHwOBrZRCjGMQ2mFqaeSDJkHCCn5Zo/gblB//go3J4CmuDVevDtVvh8M1pc4SuD9IMu1E+XRTonngCt4swBTKzkdTg3eDYqwciw0XLB4DjU42FKR4HpoNWsWPIGeJiC3DJYXgfdbAWmTUvhbieh1bsvYfOq4ZfH1DrglOHto/DZEUBTOFxj9V5U25HZ2fj0CEFTqyVqraDRa3qZUof0Q201ZLNM3F+Dk7lYDbFcPAvhbWqDK4aL7mF5HjtT7I3yqYuiisi40ulWDiBUJstMeVd/YibangI18qt3Ggsv6MiUalY3S0v165RggRFpx3MNqzKsnysAMGXzNNORSm8nVKnE9uoL/wsp/H9fNnM3td4y6gnogFsu4Ui44pIKvMB6zu0F1W8Ie6FFeLcNqhdMj/kz9+OpPn3/SjYnHNUeBKOvG9tz6cTwpwBb7CLf3lIteCYB7KesoA9gcGbyAoj6p0ohrAY6qcbxI6WL8+4TvCau6wqLEyIMB4X7UTKTnzIX1FN4U8xTz94JqtV0r/Z+oZDUZc6gu4xJlluUn1XJwO02luYUYkxFJ0T+/Io/NZ/p1j5hTGTqGmCON4fPoE3lzNrQlSlTKn7J9lgZxC/R3q0Tr0hXZSuc0UqtleZCN1xEzhMmgUomKFl9GB3z2DaOsE2vr0t4keCrMTibP/KifgSCtYhlUcVIkdNV1ERZgiZTluDVSyWG/sFLeMhQChxrO4rv7MZlKPE9CnOlmtWiJO9tAgQqmQseBTabAIxofEzE73ybbQYrg1wxCg5UP3qojk0PUfj+XrI5jPfjJZQMNR9i8GXiTW3tZ2cFK6GZIknPHOkERN1Nu1J2eVmZl1z0Ik9s6OA9RcIiloyulN+Dewj318KVHUoIAHiDC/HfjTRWx9HL8cyDP/I0mKgEyxPy0niyYjR8eAgoQr3APrtfJBoaa3vhxlFTlgEpsHoHG9INxCWyPhE7rHi3oF5e2U0AgG6Az/FMlxJLqZwOd+0QR3GQBOgh9ooWMEGU/B4HD99lqh+0Q3lIihHAxMF54GqD3W+DhTHEsvvk6B8HX+5gLobnXxJGnC6D16RXBWCyQeZ5EhR9J6UcMtz5YWcvlwwOfgRiwnMpup6wxkV4VGB5j2DCDzo7YtzndwENACT317b9vW1AA5SQongHziuTViAWxBn0T83nLXsGHLkZ9cY9Gtb14poWkfi708G4TCB0m9jfEpbvr/kLu+2ReTfQwaB2EQ1aLJ6GTCIaG6IximcWdlvHM1vIsIVd1YwRISoID3UkEO4uguY1xdH71SZk322XHbjCD2wKLP3ZFf/plKownt7by9A2tGACZgun5gEenMEH7tF+69j9IjlG7imsVAGjKIkmjHsBSps58uPfvohqyR4hjiwl9cXh5WBHxtvdhF74CTlekR0YcFxzbM/O2YWW2TaKQBqVJULine truncated
|
||||||
assets["assets/lang/prettyprint.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1xSbW7aQBD931Os/MtI9QUqVWrtUtoQEWQg+T3gxd54PWvtBwiiHCcnycUys7aA5Bfz3puZffPwAazQgPXSSu9PvVXoxU/xkmzr5EeSB12DVYDJ92QHRBTgQY/w1wG0xJ264UX6OHI44R7H0lnuGgKVJDCTtovjUjOyUrYMkMAUa61cE2E2y6+MSDeovKzEXGFdmY43S9686gHHCZdNV1dGpFSoaGGviP6rcGzcW4aWLA4oK35fCJEWgFABjzWB6H8BL9crT8R/OnKAz3z0HdBz0knCrcnmJVFzYyXHEH95kea5e+WbQM7iKG6JWRh7lDURIjdt9/6mWeBI/gQfrSF+6lqc0FjHWfXctTRjVL3P8jIS1oc6kBmR5hbOSk8Gdbn+og41RN2arHwgvTRddCfSsYoiR1AG5wbb7sD5HmU1PBxagpvWUsyDfm6yYsF/d6MwPsRFXETK+ulWWYM68gfy+u0DAAD//wEAAP//j0iqQH0CAAA=")
|
assets["assets/lang/prettyprint.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1xSbW7aQBD931Os/MtI9QUqVWrtUtoQEWQg+T3gxd54PWvtBwiiHCcnycUys7aA5Bfz3puZffPwAazQgPXSSu9PvVXoxU/xkmzr5EeSB12DVYDJ92QHRBTgQY/w1wG0xJ264UX6OHI44R7H0lnuGgKVJDCTtovjUjOyUrYMkMAUa61cE2E2y6+MSDeovKzEXGFdmY43S9686gHHCZdNV1dGpFSoaGGviP6rcGzcW4aWLA4oK35fCJEWgFABjzWB6H8BL9crT8R/OnKAz3z0HdBz0knCrcnmJVFzYyXHEH95kea5e+WbQM7iKG6JWRh7lDURIjdt9/6mWeBI/gQfrSF+6lqc0FjHWfXctTRjVL3P8jIS1oc6kBmR5hbOSk8Gdbn+og41RN2arHwgvTRddCfSsYoiR1AG5wbb7sD5HmU1PBxagpvWUsyDfm6yYsF/d6MwPsRFXETK+ulWWYM68gfy+u0DAAD//wEAAP//j0iqQH0CAAA=")
|
||||||
assets["assets/lang/valid-langs.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/yTMvQ6CMBTF8d2naDp7H8FEIcZBo6aSOBiHAhUqTSEtdPDpvacsv38/kpN0EEk7216076LYiZesO7mVjc7s+c/4xuZbZFrDGAd8hk4FGjN0fHA/FoQMlQduvzB2Zr5YGkY6K67Di68BFj0WJ5ymmQq19l5xw0jqhmInJmYZmF9P5XVt9ZTvzR8AAP//AQAA//8eEnv9zQAAAA==")
|
assets["assets/lang/valid-langs.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/yTMvQ6CMBTF8d2naDp7H8FEIcZBo6aSOBiHAhUqTSEtdPDpvacsv38/kpN0EEk7216076LYiZesO7mVjc7s+c/4xuZbZFrDGAd8hk4FGjN0fHA/FoQMlQduvzB2Zr5YGkY6K67Di68BFj0WJ5ymmQq19l5xw0jqhmInJmYZmF9P5XVt9ZTvzR8AAP//AQAA//8eEnv9zQAAAA==")
|
||||||
assets["index.html"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["index.html"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
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["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/app.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/5xXbXfTthd/n08hzuFgp02clD/8t7WUM9bS0a2lHSkM1tMXii07amXLk+S0hZPvvqsnPyTugPlAY0n33t99vvJkgg54eS9otlAoPBiip9OdZ+hiQdDsvojVghYZelWpBRcyGkwm8A8OqUQzXomYAG9C0BEXOYI9Wc2vSayQ4kiBAEVELhFPzeKUf6aMYXRezRmNtZgTGpNCkhFaRuhpNI3QcYowikGZmuf8BN1iiQquUEKlEnReKZKgW6oWQACIKWVkpIV94hWKcYH4XGEKPwVBWKGFUuXuZJJb7IiLbAIyJ4A2iQaDwWTrWjJaKDQX/FYSsYuUqEChmBeKFhXx65JVUv+3a7QFXtjKGJ9jhh7vohQzbQUusophUa9BiOSM1OslZjQ5ASrptrScwRILJGtH73spUc6TipEwqM+CEbocIHgCR/JeUSajhArwOF0S83qOM1pgRXkRjCxxiWWMWQlECxUpgQvJsCJwao9r8VHMBfFMzW5ClhClzf2Us4SIzX1JFHguk5snBjvt46kkzoggJRcqGFwN96xTKsHmGJy0jwJBpApgu61skdIsTCvYAGNR+FhH+lzwJQW1RuhxbWmzd8LBEWRGhLbIbw/RF6NOhz+ClCCQ3KXSSV9WctEg3b0hGEjkOyJLCDA5bkhDL0w/2gTrvOMEbCgqxvYG9akgqhJFi9xuWpGQHh6NFx4n9KfDNS4PtrB6AZanjNxWCC7t4/B0H4AGwPqJvA0zaACqZYF/oPRcPkK556V2OdRhDMUrSZHo0vWabbDSNDwz/SK6IfcydGTDiJEig/re399H0z5rWw70JmzqvtpUlaYofOTteUhwK2ZOocvgblxn3pgmwdUmmhf/NeltBONRgPHrCLqnbnFFFk5H6HlP0PzTTdaEpLhiSkZ3UqQ2O9/i3BTOx/HB7N3R+ILfkGIcoO31aP4HgAPObyjxAN8vfvUtkfrX6DYSVvZgNXR5CbnIdDJCO4GkTBDTFU+l6YeWYrMvQPchMwUk8RGMEnnCtf/CJnrQOVN6twtdV0JvkxMNYP6MXSPTj6xSSxRdS2i9Xa16+45ulK+WmDI8Z8RSyLCZEC74D7Ie2pDY8zAgRaDRDCQ44eeLs8OzXUTuwFqYxm5SMbIkrG4tEkGT41CkJRa6ZqUFCeVwMKjbj43nAc+BiIR4hOY+sXWyq/uSwKDGUaGz4RGUa1AVkCy0IEmAnjxBjmDeS9AuES3NiXnhyIeDnoQY7+ytt1DH9tKxuYwYNFJdTh4ayX7RBl+TbXlr6TX7yxb73mDV8pKdhQ95CUc0Mdg0+XZUYHlpWPqQTnEZMi9L9+gcSvGLKwYGo1m8hgbcGo6iDZxfwnxLrvScaMqnBZ73QZ7A5SvM25gMBFy6TgiIKNSboDbcvfI2HLPzM78EzGHbVhZJaBJhx3ldTVhXk4TENId7ky6TEQzUjjoJzaiSI00kXd1p5wPp5hxx4qdeG/NjBYBRp1gtopRxmOfmlfHMvuC5gR4O0QTVJzvToVNaI3v2HN/pHg46orGT3DXNqtm2DvpSwvOZ7f8wAdvG0ZFuhVDvuuXCPTBeYDPog+nO0/89e/7/H378aYrnMVRWtqDXNywvePm3kKpa3t7df371y8Hh66Nf3xz/9vvJ6duz8z/ezS7ef/jz46e/xkErfhQETvcQhVwFeHjZ3u46zSiwvW/RL42dgkM9W+9Y/eEKtIVCQ+HH+BjtDIdXPVluJXa9QOXrvFT39loQ8vl12w1NfRuFTd3rq35D1ZJubtg9qPoCv55XczADWp/eGcH3BlVtVEVzwisF93uRSftpAI11ZPalwnnpgwMUGXx6uOwzNQKNVYBbm0pcvyEyLHVQD4EwKvgtnI8bwXud/mhIX3TU849TEQTBaLiwi9CAW3NAqOZu3ShWiOgPkIfE2Ntq+8x0Mm1e383GHGg7G5+3nzp3tR8iXJbsPqzdqL3ac9Vx5/qDCAh6VWrdA2yYned9AvQ6vZGrPx8bgQ4FfqqcFKp1UoejE6e9ThhhCrO3/BYoHjkndmPnd783cGuO+V5HfsWJvUngY2nrpPHwen356nWeXw3+AQAA//8BAAD//02cuXRIEAAA")
|
||||||
assets["syncthing/core/aboutModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2ENqWktOglwHhFDvTgpSgT/+/yw/F8fMTmxIQ2asYX5ErxPF0fokCE1lSuxAgl7pVgB3TlkPaXAMrbnkQlOKpm7M842uEdQs8af4lLMKed0Z2EP7NymGKzvFo3BZN4NuNei+/06EdztpYNiul75amr56AQAA//8BAAD//xLf4CHFAAAA")
|
assets["syncthing/core/aboutModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2ENqWktOglwHhFDvTgpSgT/+/yw/F8fMTmxIQ2asYX5ErxPF0fokCE1lSuxAgl7pVgB3TlkPaXAMrbnkQlOKpm7M842uEdQs8af4lLMKed0Z2EP7NymGKzvFo3BZN4NuNei+/06EdztpYNiul75amr56AQAA//8BAAD//xLf4CHFAAAA")
|
||||||
@@ -78,7 +78,7 @@ func Assets() map[string][]byte {
|
|||||||
assets["syncthing/core/selectOnClickDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3SQQWvEIBCF7/srPBRiIEjPXXooe2+hPZYeRKeprJkpo2YpJf+9atLdJWzeSZ/z+Z5q7JPXrAayyYNswg+a+OWwV4YYmnYnspR1DCa6sQyAz8sXPHhnjk0nPlMGHKGQdyeHlk6t+K1QEUNMjFdGkXd4fLjigqFv6ES+dwCMndAxcmhXUNEyoQhlY1bx8hZQNGoWc+cS9iiWlqqH+PZvy3a/yXL+IcicJZNqumHQEV6LvcVVRs2pz2ThQBgzGuTygvf7jw3y3FQxDDTCk/c1KWxFXQBt7Vyqpt8Yn1bedN5N+105/AMAAP//AQAA//+SF+4JDAIAAA==")
|
assets["syncthing/core/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/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/shutdownDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0TOTaqDMBAH8PXzFMNsXL16gZhNj+AJ0jjWQJwJzgQp1ru3lkK3P/h/uEXGkCGNPepcbZSNEdSCVX1LjZFUEVIU7rHIRuu/TBOCJcvU4763wzcFV1lKJqMWnmBrYM3B6DjQN3+u/MQPD442J77DHBRuRAznMpwlF9cV37juc8o3LwAAAP//AQAA//99X8KxnQAAAA==")
|
||||||
assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
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/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/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==")
|
assets["syncthing/core/upgradingDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1zOTarDMAwE4PXLKYQ2WSW5gO0zPCg9gHDcVODaxlIIJc3dm5b+QLfDN8yYSx4pAo8W5zJVGjlNCKKks1jkdMoI7HOySLXmpfNcfQzdXBCUNQaL69oe380WbqCVkkTSsG3omj9TnJFC6Zu7wzV5Pe8cWOCz2pvh4Rz88v8YSAIsxPoifb/j4hozPN+75g4AAP//AQAA///kaeW6xgAAAA==")
|
||||||
|
|||||||
+23
-22
@@ -6,7 +6,12 @@
|
|||||||
|
|
||||||
package beacon
|
package beacon
|
||||||
|
|
||||||
import "net"
|
import (
|
||||||
|
"net"
|
||||||
|
stdsync "sync"
|
||||||
|
|
||||||
|
"github.com/thejerf/suture"
|
||||||
|
)
|
||||||
|
|
||||||
type recv struct {
|
type recv struct {
|
||||||
data []byte
|
data []byte
|
||||||
@@ -14,34 +19,30 @@ type recv struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
|
suture.Service
|
||||||
Send(data []byte)
|
Send(data []byte)
|
||||||
Recv() ([]byte, net.Addr)
|
Recv() ([]byte, net.Addr)
|
||||||
|
Error() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type readerFrom interface {
|
type readerFrom interface {
|
||||||
ReadFrom([]byte) (int, net.Addr, error)
|
ReadFrom([]byte) (int, net.Addr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func genericReader(conn readerFrom, outbox chan<- recv) {
|
type errorHolder struct {
|
||||||
bs := make([]byte, 65536)
|
err error
|
||||||
for {
|
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||||
n, addr, err := conn.ReadFrom(bs)
|
}
|
||||||
if err != nil {
|
|
||||||
l.Warnln("multicast read:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if debug {
|
|
||||||
l.Debugf("recv %d bytes from %s", n, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := make([]byte, n)
|
func (e *errorHolder) setError(err error) {
|
||||||
copy(c, bs)
|
e.mut.Lock()
|
||||||
select {
|
e.err = err
|
||||||
case outbox <- recv{c, addr}:
|
e.mut.Unlock()
|
||||||
default:
|
}
|
||||||
if debug {
|
|
||||||
l.Debugln("dropping message")
|
func (e *errorHolder) Error() error {
|
||||||
}
|
e.mut.Lock()
|
||||||
}
|
err := e.err
|
||||||
}
|
e.mut.Unlock()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
+43
-27
@@ -19,6 +19,8 @@ type Broadcast struct {
|
|||||||
port int
|
port int
|
||||||
inbox chan []byte
|
inbox chan []byte
|
||||||
outbox chan recv
|
outbox chan recv
|
||||||
|
br *broadcastReader
|
||||||
|
bw *broadcastWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBroadcast(port int) *Broadcast {
|
func NewBroadcast(port int) *Broadcast {
|
||||||
@@ -41,14 +43,16 @@ func NewBroadcast(port int) *Broadcast {
|
|||||||
outbox: make(chan recv, 16),
|
outbox: make(chan recv, 16),
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Add(&broadcastReader{
|
b.br = &broadcastReader{
|
||||||
port: port,
|
port: port,
|
||||||
outbox: b.outbox,
|
outbox: b.outbox,
|
||||||
})
|
}
|
||||||
b.Add(&broadcastWriter{
|
b.Add(b.br)
|
||||||
|
b.bw = &broadcastWriter{
|
||||||
port: port,
|
port: port,
|
||||||
inbox: b.inbox,
|
inbox: b.inbox,
|
||||||
})
|
}
|
||||||
|
b.Add(b.bw)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@@ -62,11 +66,18 @@ func (b *Broadcast) Recv() ([]byte, net.Addr) {
|
|||||||
return recv.data, recv.src
|
return recv.data, recv.src
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Broadcast) Error() error {
|
||||||
|
if err := b.br.Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.bw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
type broadcastWriter struct {
|
type broadcastWriter struct {
|
||||||
port int
|
port int
|
||||||
inbox chan []byte
|
inbox chan []byte
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
failed bool // Have we already logged a failure reason?
|
errorHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *broadcastWriter) Serve() {
|
func (w *broadcastWriter) Serve() {
|
||||||
@@ -78,22 +89,21 @@ func (w *broadcastWriter) Serve() {
|
|||||||
var err error
|
var err error
|
||||||
w.conn, err = net.ListenUDP("udp4", nil)
|
w.conn, err = net.ListenUDP("udp4", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !w.failed {
|
if debug {
|
||||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
l.Debugln(err)
|
||||||
w.failed = true
|
|
||||||
}
|
}
|
||||||
|
w.setError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer w.conn.Close()
|
defer w.conn.Close()
|
||||||
|
|
||||||
w.failed = false
|
|
||||||
|
|
||||||
for bs := range w.inbox {
|
for bs := range w.inbox {
|
||||||
addrs, err := net.InterfaceAddrs()
|
addrs, err := net.InterfaceAddrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugln("Local discovery (broadcast writer):", err)
|
l.Debugln(err)
|
||||||
}
|
}
|
||||||
|
w.setError(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,13 +127,16 @@ func (w *broadcastWriter) Serve() {
|
|||||||
for _, ip := range dsts {
|
for _, ip := range dsts {
|
||||||
dst := &net.UDPAddr{IP: ip, Port: w.port}
|
dst := &net.UDPAddr{IP: ip, Port: w.port}
|
||||||
|
|
||||||
w.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
w.conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||||
_, err := w.conn.WriteTo(bs, dst)
|
_, err := w.conn.WriteTo(bs, dst)
|
||||||
|
w.conn.SetWriteDeadline(time.Time{})
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||||
// Write timeouts should not happen. We treat it as a fatal
|
// Write timeouts should not happen. We treat it as a fatal
|
||||||
// error on the socket.
|
// error on the socket.
|
||||||
l.Infoln("Local discovery (broadcast writer):", err)
|
if debug {
|
||||||
w.failed = true
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
w.setError(err)
|
||||||
return
|
return
|
||||||
} else if err, ok := err.(net.Error); ok && err.Temporary() {
|
} else if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||||
// A transient error. Lets hope for better luck in the future.
|
// A transient error. Lets hope for better luck in the future.
|
||||||
@@ -133,11 +146,14 @@ func (w *broadcastWriter) Serve() {
|
|||||||
continue
|
continue
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
// Some other error that we don't expect. Bail and retry.
|
// Some other error that we don't expect. Bail and retry.
|
||||||
l.Infoln("Local discovery (broadcast writer):", err)
|
if debug {
|
||||||
w.failed = true
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
w.setError(err)
|
||||||
return
|
return
|
||||||
} else if debug {
|
} else if debug {
|
||||||
l.Debugf("sent %d bytes to %s", len(bs), dst)
|
l.Debugf("sent %d bytes to %s", len(bs), dst)
|
||||||
|
w.setError(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +171,7 @@ type broadcastReader struct {
|
|||||||
port int
|
port int
|
||||||
outbox chan recv
|
outbox chan recv
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
failed bool
|
errorHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *broadcastReader) Serve() {
|
func (r *broadcastReader) Serve() {
|
||||||
@@ -167,10 +183,10 @@ func (r *broadcastReader) Serve() {
|
|||||||
var err error
|
var err error
|
||||||
r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !r.failed {
|
if debug {
|
||||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
l.Debugln(err)
|
||||||
r.failed = true
|
|
||||||
}
|
}
|
||||||
|
r.setError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.conn.Close()
|
defer r.conn.Close()
|
||||||
@@ -179,14 +195,14 @@ func (r *broadcastReader) Serve() {
|
|||||||
for {
|
for {
|
||||||
n, addr, err := r.conn.ReadFrom(bs)
|
n, addr, err := r.conn.ReadFrom(bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !r.failed {
|
if debug {
|
||||||
l.Infoln("Local discovery (broadcast reader):", err)
|
l.Debugln(err)
|
||||||
r.failed = true
|
|
||||||
}
|
}
|
||||||
|
r.setError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r.failed = false
|
r.setError(nil)
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("recv %d bytes from %s", n, addr)
|
l.Debugf("recv %d bytes from %s", n, addr)
|
||||||
|
|||||||
+201
-54
@@ -8,39 +8,200 @@ package beacon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/thejerf/suture"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Multicast struct {
|
type Multicast struct {
|
||||||
conn *ipv6.PacketConn
|
*suture.Supervisor
|
||||||
addr *net.UDPAddr
|
addr *net.UDPAddr
|
||||||
inbox chan []byte
|
inbox chan []byte
|
||||||
outbox chan recv
|
outbox chan recv
|
||||||
intfs []net.Interface
|
mr *multicastReader
|
||||||
|
mw *multicastWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMulticast(addr string) (*Multicast, error) {
|
func NewMulticast(addr string) *Multicast {
|
||||||
gaddr, err := net.ResolveUDPAddr("udp6", addr)
|
m := &Multicast{
|
||||||
if err != nil {
|
Supervisor: suture.New("multicastBeacon", suture.Spec{
|
||||||
return nil, err
|
// Don't retry too frenetically: an error to open a socket or
|
||||||
|
// whatever is usually something that is either permanent or takes
|
||||||
|
// a while to get solved...
|
||||||
|
FailureThreshold: 2,
|
||||||
|
FailureBackoff: 60 * time.Second,
|
||||||
|
// Only log restarts in debug mode.
|
||||||
|
Log: func(line string) {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(line)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inbox: make(chan []byte),
|
||||||
|
outbox: make(chan recv, 16),
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.ListenPacket("udp6", addr)
|
m.mr = &multicastReader{
|
||||||
|
addr: addr,
|
||||||
|
outbox: m.outbox,
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
m.Add(m.mr)
|
||||||
|
|
||||||
|
m.mw = &multicastWriter{
|
||||||
|
addr: addr,
|
||||||
|
inbox: m.inbox,
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
m.Add(m.mw)
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multicast) Send(data []byte) {
|
||||||
|
m.inbox <- data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multicast) Recv() ([]byte, net.Addr) {
|
||||||
|
recv := <-m.outbox
|
||||||
|
return recv.data, recv.src
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multicast) Error() error {
|
||||||
|
if err := m.mr.Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.mw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
type multicastWriter struct {
|
||||||
|
addr string
|
||||||
|
inbox <-chan []byte
|
||||||
|
errorHolder
|
||||||
|
stop chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multicastWriter) Serve() {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(w, "starting")
|
||||||
|
defer l.Debugln(w, "stopping")
|
||||||
|
}
|
||||||
|
|
||||||
|
gaddr, err := net.ResolveUDPAddr("udp6", w.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if debug {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
w.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.ListenPacket("udp6", ":0")
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
w.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pconn := ipv6.NewPacketConn(conn)
|
||||||
|
|
||||||
|
wcm := &ipv6.ControlMessage{
|
||||||
|
HopLimit: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
for bs := range w.inbox {
|
||||||
|
intfs, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
w.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var success int
|
||||||
|
|
||||||
|
for _, intf := range intfs {
|
||||||
|
wcm.IfIndex = intf.Index
|
||||||
|
pconn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||||
|
_, err = pconn.WriteTo(bs, wcm, gaddr)
|
||||||
|
pconn.SetWriteDeadline(time.Time{})
|
||||||
|
if err != nil && debug {
|
||||||
|
l.Debugln(err, "on write to", gaddr, intf.Name)
|
||||||
|
} else if debug {
|
||||||
|
l.Debugf("sent %d bytes to %v on %s", len(bs), gaddr, intf.Name)
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if success > 0 {
|
||||||
|
w.setError(nil)
|
||||||
|
} else {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
w.setError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multicastWriter) Stop() {
|
||||||
|
close(w.stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multicastWriter) String() string {
|
||||||
|
return fmt.Sprintf("multicastWriter@%p", w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type multicastReader struct {
|
||||||
|
addr string
|
||||||
|
outbox chan<- recv
|
||||||
|
errorHolder
|
||||||
|
stop chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *multicastReader) Serve() {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(r, "starting")
|
||||||
|
defer l.Debugln(r, "stopping")
|
||||||
|
}
|
||||||
|
|
||||||
|
gaddr, err := net.ResolveUDPAddr("udp6", r.addr)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
r.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.ListenPacket("udp6", r.addr)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
r.setError(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
intfs, err := net.Interfaces()
|
intfs, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if debug {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
r.setError(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p := ipv6.NewPacketConn(conn)
|
pconn := ipv6.NewPacketConn(conn)
|
||||||
joined := 0
|
joined := 0
|
||||||
for _, intf := range intfs {
|
for _, intf := range intfs {
|
||||||
err := p.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
err := pconn.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
||||||
if debug {
|
if debug {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugln("IPv6 join", intf.Name, "failed:", err)
|
l.Debugln("IPv6 join", intf.Name, "failed:", err)
|
||||||
@@ -52,57 +213,43 @@ func NewMulticast(addr string) (*Multicast, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if joined == 0 {
|
if joined == 0 {
|
||||||
return nil, errors.New("no multicast interfaces available")
|
if debug {
|
||||||
|
l.Debugln("no multicast interfaces available")
|
||||||
|
}
|
||||||
|
r.setError(errors.New("no multicast interfaces available"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b := &Multicast{
|
bs := make([]byte, 65536)
|
||||||
conn: p,
|
for {
|
||||||
addr: gaddr,
|
n, _, addr, err := pconn.ReadFrom(bs)
|
||||||
inbox: make(chan []byte),
|
if err != nil {
|
||||||
outbox: make(chan recv, 16),
|
if debug {
|
||||||
intfs: intfs,
|
l.Debugln(err)
|
||||||
}
|
}
|
||||||
|
r.setError(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
l.Debugf("recv %d bytes from %s", n, addr)
|
||||||
|
}
|
||||||
|
|
||||||
go genericReader(ipv6ReaderAdapter{b.conn}, b.outbox)
|
c := make([]byte, n)
|
||||||
go b.writer()
|
copy(c, bs)
|
||||||
|
select {
|
||||||
return b, nil
|
case r.outbox <- recv{c, addr}:
|
||||||
}
|
default:
|
||||||
|
if debug {
|
||||||
func (b *Multicast) Send(data []byte) {
|
l.Debugln("dropping message")
|
||||||
b.inbox <- data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Multicast) Recv() ([]byte, net.Addr) {
|
|
||||||
recv := <-b.outbox
|
|
||||||
return recv.data, recv.src
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Multicast) writer() {
|
|
||||||
wcm := &ipv6.ControlMessage{
|
|
||||||
HopLimit: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
for bs := range b.inbox {
|
|
||||||
for _, intf := range b.intfs {
|
|
||||||
wcm.IfIndex = intf.Index
|
|
||||||
_, err := b.conn.WriteTo(bs, wcm, b.addr)
|
|
||||||
if err != nil && debug {
|
|
||||||
l.Debugln(err, "on write to", b.addr)
|
|
||||||
} else if debug {
|
|
||||||
l.Debugf("sent %d bytes to %v on %s", len(bs), b.addr, intf.Name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This makes ReadFrom on an *ipv6.PacketConn behave like ReadFrom on a
|
func (r *multicastReader) Stop() {
|
||||||
// net.PacketConn.
|
close(r.stop)
|
||||||
type ipv6ReaderAdapter struct {
|
|
||||||
c *ipv6.PacketConn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i ipv6ReaderAdapter) ReadFrom(bs []byte) (int, net.Addr, error) {
|
func (r *multicastReader) String() string {
|
||||||
n, _, src, err := i.c.ReadFrom(bs)
|
return fmt.Sprintf("multicastReader@%p", r)
|
||||||
return n, src, err
|
|
||||||
}
|
}
|
||||||
+27
-8
@@ -31,6 +31,21 @@ const (
|
|||||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultDiscoveryServers should be substituted when the configuration
|
||||||
|
// contains <globalAnnounceServer>default</globalAnnounceServer>. This is
|
||||||
|
// done by the "consumer" of the configuration, as we don't want these
|
||||||
|
// saved to the config.
|
||||||
|
DefaultDiscoveryServers = []string{
|
||||||
|
"https://v4-1.discover.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
|
||||||
|
"https://v4-2.discover.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 45.55.230.38, USA
|
||||||
|
"https://v4-3.discover.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 128.199.95.124, Singapore
|
||||||
|
"https://v6-1.discover.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
|
||||||
|
"https://v6-2.discover.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 2604:a880:800:10::182:a001, USA
|
||||||
|
"https://v6-3.discover.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 2400:6180:0:d0::d9:d001, Singapore
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Version int `xml:"version,attr" json:"version"`
|
Version int `xml:"version,attr" json:"version"`
|
||||||
Folders []FolderConfiguration `xml:"folder" json:"folders"`
|
Folders []FolderConfiguration `xml:"folder" json:"folders"`
|
||||||
@@ -215,7 +230,7 @@ type FolderDeviceConfiguration struct {
|
|||||||
|
|
||||||
type OptionsConfiguration struct {
|
type OptionsConfiguration struct {
|
||||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
||||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22027, udp6://announce-v6.syncthing.net:22027"`
|
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
|
||||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
||||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
||||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
||||||
@@ -498,17 +513,21 @@ func convertV11V12(cfg *Configuration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use new discovery server
|
// Use new discovery server
|
||||||
for i, addr := range cfg.Options.GlobalAnnServers {
|
var newDiscoServers []string
|
||||||
|
var useDefault bool
|
||||||
|
for _, addr := range cfg.Options.GlobalAnnServers {
|
||||||
if addr == "udp4://announce.syncthing.net:22026" {
|
if addr == "udp4://announce.syncthing.net:22026" {
|
||||||
cfg.Options.GlobalAnnServers[i] = "udp4://announce.syncthing.net:22027"
|
useDefault = true
|
||||||
} else if addr == "udp6://announce-v6.syncthing.net:22026" {
|
} else if addr == "udp6://announce-v6.syncthing.net:22026" {
|
||||||
cfg.Options.GlobalAnnServers[i] = "udp6://announce-v6.syncthing.net:22027"
|
useDefault = true
|
||||||
} else if addr == "udp4://194.126.249.5:22026" {
|
} else {
|
||||||
cfg.Options.GlobalAnnServers[i] = "udp4://194.126.249.5:22027"
|
newDiscoServers = append(newDiscoServers, addr)
|
||||||
} else if addr == "udp6://[2001:470:28:4d6::5]:22026" {
|
|
||||||
cfg.Options.GlobalAnnServers[i] = "udp6://[2001:470:28:4d6::5]:22027"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if useDefault {
|
||||||
|
newDiscoServers = append(newDiscoServers, "default")
|
||||||
|
}
|
||||||
|
cfg.Options.GlobalAnnServers = newDiscoServers
|
||||||
|
|
||||||
// Use new multicast group
|
// Use new multicast group
|
||||||
if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" {
|
if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func init() {
|
|||||||
func TestDefaultValues(t *testing.T) {
|
func TestDefaultValues(t *testing.T) {
|
||||||
expected := OptionsConfiguration{
|
expected := OptionsConfiguration{
|
||||||
ListenAddress: []string{"tcp://0.0.0.0:22000"},
|
ListenAddress: []string{"tcp://0.0.0.0:22000"},
|
||||||
GlobalAnnServers: []string{"udp4://announce.syncthing.net:22027", "udp6://announce-v6.syncthing.net:22027"},
|
GlobalAnnServers: []string{"default"},
|
||||||
GlobalAnnEnabled: true,
|
GlobalAnnEnabled: true,
|
||||||
LocalAnnEnabled: true,
|
LocalAnnEnabled: true,
|
||||||
LocalAnnPort: 21027,
|
LocalAnnPort: 21027,
|
||||||
|
|||||||
@@ -317,3 +317,15 @@ func (w *Wrapper) Save() error {
|
|||||||
events.Default.Log(events.ConfigSaved, w.cfg)
|
events.Default.Log(events.ConfigSaved, w.cfg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Wrapper) GlobalDiscoveryServers() []string {
|
||||||
|
var servers []string
|
||||||
|
for _, srv := range w.cfg.Options.GlobalAnnServers {
|
||||||
|
if srv == "default" {
|
||||||
|
servers = append(servers, DefaultDiscoveryServers...)
|
||||||
|
} else {
|
||||||
|
servers = append(servers, srv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueStrings(servers)
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package discover
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdsync "sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
|
"github.com/thejerf/suture"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The CachingMux aggregates results from multiple Finders. Each Finder has
|
||||||
|
// an associated cache time and negative cache time. The cache time sets how
|
||||||
|
// long we cache and return successfull lookup results, the negative cache
|
||||||
|
// time sets how long we refrain from asking about the same device ID after
|
||||||
|
// receiving a negative answer. The value of zero disables caching (positive
|
||||||
|
// or negative).
|
||||||
|
type CachingMux struct {
|
||||||
|
*suture.Supervisor
|
||||||
|
finders []cachedFinder
|
||||||
|
caches []*cache
|
||||||
|
mut sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// A cachedFinder is a Finder with associated cache timeouts.
|
||||||
|
type cachedFinder struct {
|
||||||
|
Finder
|
||||||
|
cacheTime time.Duration
|
||||||
|
negCacheTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCachingMux() *CachingMux {
|
||||||
|
return &CachingMux{
|
||||||
|
Supervisor: suture.NewSimple("discover.cachingMux"),
|
||||||
|
mut: sync.NewMutex(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add registers a new Finder, with associated cache timeouts.
|
||||||
|
func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration) {
|
||||||
|
m.mut.Lock()
|
||||||
|
m.finders = append(m.finders, cachedFinder{finder, cacheTime, negCacheTime})
|
||||||
|
m.caches = append(m.caches, newCache())
|
||||||
|
m.mut.Unlock()
|
||||||
|
|
||||||
|
if svc, ok := finder.(suture.Service); ok {
|
||||||
|
m.Supervisor.Add(svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup attempts to resolve the device ID using any of the added Finders,
|
||||||
|
// while obeying the cache settings.
|
||||||
|
func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||||
|
m.mut.Lock()
|
||||||
|
for i, finder := range m.finders {
|
||||||
|
if cacheEntry, ok := m.caches[i].Get(deviceID); ok {
|
||||||
|
// We have a cache entry. Lets see what it says.
|
||||||
|
|
||||||
|
if cacheEntry.found && time.Since(cacheEntry.when) < finder.cacheTime {
|
||||||
|
// It's a positive, valid entry. Use it.
|
||||||
|
if debug {
|
||||||
|
l.Debugln("cached discovery entry for", deviceID, "at", finder.String())
|
||||||
|
l.Debugln(" ", cacheEntry)
|
||||||
|
}
|
||||||
|
direct = append(direct, cacheEntry.Direct...)
|
||||||
|
relays = append(relays, cacheEntry.Relays...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cacheEntry.found && time.Since(cacheEntry.when) < finder.negCacheTime {
|
||||||
|
// It's a negative, valid entry. We should not make another
|
||||||
|
// attempt right now.
|
||||||
|
if debug {
|
||||||
|
l.Debugln("negative cache entry for", deviceID, "at", finder.String())
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's expired. Ignore and continue.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the actual lookup and cache the result.
|
||||||
|
if td, tr, err := finder.Lookup(deviceID); err == nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("lookup for", deviceID, "at", finder.String())
|
||||||
|
l.Debugln(" ", td)
|
||||||
|
l.Debugln(" ", tr)
|
||||||
|
}
|
||||||
|
direct = append(direct, td...)
|
||||||
|
relays = append(relays, tr...)
|
||||||
|
m.caches[i].Set(deviceID, CacheEntry{
|
||||||
|
Direct: td,
|
||||||
|
Relays: tr,
|
||||||
|
when: time.Now(),
|
||||||
|
found: len(td)+len(tr) > 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mut.Unlock()
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
l.Debugln("lookup results for", deviceID)
|
||||||
|
l.Debugln(" ", direct)
|
||||||
|
l.Debugln(" ", relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
return direct, relays, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CachingMux) String() string {
|
||||||
|
return "discovery cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CachingMux) Error() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CachingMux) ChildErrors() map[string]error {
|
||||||
|
m.mut.Lock()
|
||||||
|
children := make(map[string]error, len(m.finders))
|
||||||
|
for _, f := range m.finders {
|
||||||
|
children[f.String()] = f.Error()
|
||||||
|
}
|
||||||
|
m.mut.Unlock()
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CachingMux) Cache() map[protocol.DeviceID]CacheEntry {
|
||||||
|
// Res will be the "total" cache, i.e. the union of our cache and all our
|
||||||
|
// children's caches.
|
||||||
|
res := make(map[protocol.DeviceID]CacheEntry)
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
for i := range m.finders {
|
||||||
|
// Each finder[i] has a corresponding cache at cache[i]. Go through it
|
||||||
|
// and populate the total, if it's newer than what's already in there.
|
||||||
|
// We skip any negative cache entries.
|
||||||
|
for k, v := range m.caches[i].Cache() {
|
||||||
|
if v.found && v.when.After(res[k].when) {
|
||||||
|
res[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then ask the finder itself for it's cache and do the same. If this
|
||||||
|
// finder is a global discovery client, it will have no cache. If it's
|
||||||
|
// a local discovery client, this will be it's current state.
|
||||||
|
for k, v := range m.finders[i].Cache() {
|
||||||
|
if v.found && v.when.After(res[k].when) {
|
||||||
|
res[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mut.Unlock()
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// A cache can be embedded wherever useful
|
||||||
|
|
||||||
|
type cache struct {
|
||||||
|
entries map[protocol.DeviceID]CacheEntry
|
||||||
|
mut stdsync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCache() *cache {
|
||||||
|
return &cache{
|
||||||
|
entries: make(map[protocol.DeviceID]CacheEntry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cache) Set(id protocol.DeviceID, ce CacheEntry) {
|
||||||
|
c.mut.Lock()
|
||||||
|
c.entries[id] = ce
|
||||||
|
c.mut.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cache) Get(id protocol.DeviceID) (CacheEntry, bool) {
|
||||||
|
c.mut.Lock()
|
||||||
|
ce, ok := c.entries[id]
|
||||||
|
c.mut.Unlock()
|
||||||
|
return ce, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cache) Cache() map[protocol.DeviceID]CacheEntry {
|
||||||
|
c.mut.Lock()
|
||||||
|
m := make(map[protocol.DeviceID]CacheEntry, len(c.entries))
|
||||||
|
for k, v := range c.entries {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
c.mut.Unlock()
|
||||||
|
return m
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (C) 2014 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Announcer interface {
|
|
||||||
Announcement() Announce
|
|
||||||
}
|
|
||||||
|
|
||||||
type Factory func(*url.URL, Announcer) (Client, error)
|
|
||||||
|
|
||||||
var (
|
|
||||||
factories = make(map[string]Factory)
|
|
||||||
DefaultErrorRetryInternval = 60 * time.Second
|
|
||||||
DefaultGlobalBroadcastInterval = 1800 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func Register(proto string, factory Factory) {
|
|
||||||
factories[proto] = factory
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(addr string, announcer Announcer) (Client, error) {
|
|
||||||
uri, err := url.Parse(addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
factory, ok := factories[uri.Scheme]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
|
|
||||||
}
|
|
||||||
client, err := factory(uri, announcer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
Lookup(device protocol.DeviceID) (Announce, error)
|
|
||||||
StatusOK() bool
|
|
||||||
Address() string
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
// Copyright (C) 2014 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var device protocol.DeviceID
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
|
|
||||||
}
|
|
||||||
|
|
||||||
type FakeAnnouncer struct {
|
|
||||||
pkt Announce
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FakeAnnouncer) Announcement() Announce {
|
|
||||||
return f.pkt
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDP4Success(t *testing.T) {
|
|
||||||
conn, err := net.ListenUDP("udp4", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
|
||||||
|
|
||||||
address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
|
|
||||||
pkt := Announce{
|
|
||||||
Magic: AnnouncementMagic,
|
|
||||||
This: Device{
|
|
||||||
device[:],
|
|
||||||
[]string{"tcp://123.123.123.123:1234"},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ann := &FakeAnnouncer{
|
|
||||||
pkt: pkt,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := New(address, ann)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
udpclient := client.(*UDPClient)
|
|
||||||
if udpclient.errorRetryInterval != DefaultErrorRetryInternval {
|
|
||||||
t.Fatal("Incorrect retry interval")
|
|
||||||
}
|
|
||||||
|
|
||||||
if udpclient.listenAddress.IP != nil || udpclient.listenAddress.Port != 0 {
|
|
||||||
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.Address() != address {
|
|
||||||
t.Fatal("Incorrect address")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 2048)
|
|
||||||
|
|
||||||
// First announcement
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
|
||||||
_, err = conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Announcement verification
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
|
|
||||||
_, addr, err := conn.ReadFromUDP(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply to it.
|
|
||||||
_, err = conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should get nothing else
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
|
||||||
_, err = conn.Read(buf)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status should be ok
|
|
||||||
if !client.StatusOK() {
|
|
||||||
t.Fatal("Wrong status")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a lookup in a separate routine
|
|
||||||
addrs := []string{}
|
|
||||||
wg := sync.NewWaitGroup()
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
pkt, err := client.Lookup(device)
|
|
||||||
if err == nil {
|
|
||||||
for _, addr := range pkt.This.Addresses {
|
|
||||||
addrs = append(addrs, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Receive the lookup and reply
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
|
||||||
_, addr, err = conn.ReadFromUDP(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
|
|
||||||
|
|
||||||
// Wait for the lookup to arrive, verify that the number of answers is correct
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if len(addrs) != 1 || addrs[0] != "tcp://123.123.123.123:1234" {
|
|
||||||
t.Fatal("Wrong number of answers")
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDP4Failure(t *testing.T) {
|
|
||||||
conn, err := net.ListenUDP("udp4", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
|
||||||
|
|
||||||
address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
|
|
||||||
|
|
||||||
pkt := Announce{
|
|
||||||
Magic: AnnouncementMagic,
|
|
||||||
This: Device{
|
|
||||||
device[:],
|
|
||||||
[]string{"tcp://123.123.123.123:1234"},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ann := &FakeAnnouncer{
|
|
||||||
pkt: pkt,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := New(address, ann)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
udpclient := client.(*UDPClient)
|
|
||||||
if udpclient.errorRetryInterval != time.Second*5 {
|
|
||||||
t.Fatal("Incorrect retry interval")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !udpclient.listenAddress.IP.Equal(net.IPv4(127, 0, 0, 1)) || udpclient.listenAddress.Port != 0 {
|
|
||||||
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.Address() != address {
|
|
||||||
t.Fatal("Incorrect address")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 2048)
|
|
||||||
|
|
||||||
// First announcement
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
|
||||||
_, err = conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Announcement verification
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
|
|
||||||
_, _, err = conn.ReadFromUDP(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't reply
|
|
||||||
// We should get nothing else
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
|
||||||
_, err = conn.Read(buf)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status should be failure
|
|
||||||
if client.StatusOK() {
|
|
||||||
t.Fatal("Wrong status")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a lookup in a separate routine
|
|
||||||
addrs := []string{}
|
|
||||||
wg := sync.NewWaitGroup()
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
pkt, err := client.Lookup(device)
|
|
||||||
if err == nil {
|
|
||||||
for _, addr := range pkt.This.Addresses {
|
|
||||||
addrs = append(addrs, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Receive the lookup and don't reply
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
|
||||||
_, _, err = conn.ReadFromUDP(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the lookup to timeout, verify that the number of answers is none
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if len(addrs) != 0 {
|
|
||||||
t.Fatal("Wrong number of answers")
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Stop()
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
// Copyright (C) 2014 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for _, proto := range []string{"udp", "udp4", "udp6"} {
|
|
||||||
Register(proto, func(uri *url.URL, announcer Announcer) (Client, error) {
|
|
||||||
c := &UDPClient{
|
|
||||||
announcer: announcer,
|
|
||||||
wg: sync.NewWaitGroup(),
|
|
||||||
mut: sync.NewRWMutex(),
|
|
||||||
}
|
|
||||||
err := c.Start(uri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type UDPClient struct {
|
|
||||||
url *url.URL
|
|
||||||
|
|
||||||
stop chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
listenAddress *net.UDPAddr
|
|
||||||
|
|
||||||
globalBroadcastInterval time.Duration
|
|
||||||
errorRetryInterval time.Duration
|
|
||||||
announcer Announcer
|
|
||||||
|
|
||||||
status bool
|
|
||||||
mut sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *UDPClient) Start(uri *url.URL) error {
|
|
||||||
d.url = uri
|
|
||||||
d.stop = make(chan struct{})
|
|
||||||
|
|
||||||
params := uri.Query()
|
|
||||||
// The address must not have a port, as otherwise both announce and lookup
|
|
||||||
// sockets would try to bind to the same port.
|
|
||||||
addr, err := net.ResolveUDPAddr(d.url.Scheme, params.Get("listenaddress")+":0")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.listenAddress = addr
|
|
||||||
|
|
||||||
broadcastSeconds, err := strconv.ParseUint(params.Get("broadcast"), 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
d.globalBroadcastInterval = DefaultGlobalBroadcastInterval
|
|
||||||
} else {
|
|
||||||
d.globalBroadcastInterval = time.Duration(broadcastSeconds) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
retrySeconds, err := strconv.ParseUint(params.Get("retry"), 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
d.errorRetryInterval = DefaultErrorRetryInternval
|
|
||||||
} else {
|
|
||||||
d.errorRetryInterval = time.Duration(retrySeconds) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
d.wg.Add(1)
|
|
||||||
go d.broadcast()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *UDPClient) broadcast() {
|
|
||||||
defer d.wg.Done()
|
|
||||||
|
|
||||||
conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress)
|
|
||||||
for err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: broadcast listen: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-d.stop:
|
|
||||||
return
|
|
||||||
case <-time.After(d.errorRetryInterval):
|
|
||||||
}
|
|
||||||
conn, err = net.ListenUDP(d.url.Scheme, d.listenAddress)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
remote, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
|
||||||
for err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: broadcast resolve: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-d.stop:
|
|
||||||
return
|
|
||||||
case <-time.After(d.errorRetryInterval):
|
|
||||||
}
|
|
||||||
remote, err = net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
timer := time.NewTimer(0)
|
|
||||||
|
|
||||||
eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged)
|
|
||||||
defer events.Default.Unsubscribe(eventSub)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-d.stop:
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-eventSub.C():
|
|
||||||
ok := d.sendAnnouncement(remote, conn)
|
|
||||||
|
|
||||||
d.mut.Lock()
|
|
||||||
d.status = ok
|
|
||||||
d.mut.Unlock()
|
|
||||||
|
|
||||||
case <-timer.C:
|
|
||||||
ok := d.sendAnnouncement(remote, conn)
|
|
||||||
|
|
||||||
d.mut.Lock()
|
|
||||||
d.status = ok
|
|
||||||
d.mut.Unlock()
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
timer.Reset(d.globalBroadcastInterval)
|
|
||||||
} else {
|
|
||||||
timer.Reset(d.errorRetryInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *UDPClient) sendAnnouncement(remote net.Addr, conn *net.UDPConn) bool {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
ann := d.announcer.Announcement()
|
|
||||||
pkt, err := ann.MarshalXDR()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
myID := protocol.DeviceIDFromBytes(ann.This.ID)
|
|
||||||
|
|
||||||
_, err = conn.WriteTo(pkt, remote)
|
|
||||||
if err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the announce server responds positively for our device ID
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
ann, err = d.Lookup(myID)
|
|
||||||
if err != nil && debug {
|
|
||||||
l.Debugf("discover %s: broadcast: Self-lookup failed: %v", d.url, err)
|
|
||||||
} else if debug {
|
|
||||||
l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, ann.This.Addresses)
|
|
||||||
}
|
|
||||||
return len(ann.This.Addresses) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *UDPClient) Lookup(device protocol.DeviceID) (Announce, error) {
|
|
||||||
extIP, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
|
||||||
if err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
|
||||||
}
|
|
||||||
return Announce{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP)
|
|
||||||
if err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
|
||||||
}
|
|
||||||
return Announce{}, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
err = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
|
||||||
if err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
|
||||||
}
|
|
||||||
return Announce{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
|
|
||||||
_, err = conn.Write(buf)
|
|
||||||
if err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
|
||||||
}
|
|
||||||
return Announce{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = make([]byte, 2048)
|
|
||||||
n, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
|
||||||
// Expected if the server doesn't know about requested device ID
|
|
||||||
return Announce{}, err
|
|
||||||
}
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
|
||||||
}
|
|
||||||
return Announce{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkt Announce
|
|
||||||
err = pkt.UnmarshalXDR(buf[:n])
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n]))
|
|
||||||
}
|
|
||||||
return Announce{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover %s: Lookup(%s) result: %v relays: %v", d.url, device, pkt.This.Addresses, pkt.This.Relays)
|
|
||||||
}
|
|
||||||
return pkt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *UDPClient) Stop() {
|
|
||||||
if d.stop != nil {
|
|
||||||
close(d.stop)
|
|
||||||
d.wg.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *UDPClient) StatusOK() bool {
|
|
||||||
d.mut.RLock()
|
|
||||||
defer d.mut.RUnlock()
|
|
||||||
return d.status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *UDPClient) Address() string {
|
|
||||||
return d.url.String()
|
|
||||||
}
|
|
||||||
+29
-520
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2014 The Syncthing Authors.
|
// Copyright (C) 2015 The Syncthing Authors.
|
||||||
//
|
//
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
@@ -7,539 +7,48 @@
|
|||||||
package discover
|
package discover
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/beacon"
|
"github.com/thejerf/suture"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Discoverer struct {
|
// A Finder provides lookup services of some kind.
|
||||||
myID protocol.DeviceID
|
type Finder interface {
|
||||||
listenAddrs []string
|
Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error)
|
||||||
relayStatusProvider relayStatusProvider
|
Error() error
|
||||||
localBcastIntv time.Duration
|
String() string
|
||||||
localBcastStart time.Time
|
Cache() map[protocol.DeviceID]CacheEntry
|
||||||
cacheLifetime time.Duration
|
|
||||||
negCacheCutoff time.Duration
|
|
||||||
beacons []beacon.Interface
|
|
||||||
extAddr externalAddr
|
|
||||||
localBcastTick <-chan time.Time
|
|
||||||
forcedBcastTick chan time.Time
|
|
||||||
|
|
||||||
registryLock sync.RWMutex
|
|
||||||
addressRegistry map[protocol.DeviceID][]CacheEntry
|
|
||||||
relayRegistry map[protocol.DeviceID][]CacheEntry
|
|
||||||
lastLookup map[protocol.DeviceID]time.Time
|
|
||||||
|
|
||||||
clients []Client
|
|
||||||
mut sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
type relayStatusProvider interface {
|
|
||||||
ClientStatus() map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type externalAddr interface {
|
|
||||||
ExternalAddresses() []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheEntry struct {
|
type CacheEntry struct {
|
||||||
Address string
|
Direct []string `json:"direct"`
|
||||||
Seen time.Time
|
Relays []Relay `json:"relays"`
|
||||||
|
when time.Time // When did we get the result
|
||||||
|
found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// A FinderService is a Finder that has background activity and must be run as
|
||||||
ErrIncorrectMagic = errors.New("incorrect magic number")
|
// a suture.Service.
|
||||||
)
|
type FinderService interface {
|
||||||
|
Finder
|
||||||
func NewDiscoverer(id protocol.DeviceID, addresses []string, relayStatusProvider relayStatusProvider) *Discoverer {
|
suture.Service
|
||||||
return &Discoverer{
|
|
||||||
myID: id,
|
|
||||||
listenAddrs: addresses,
|
|
||||||
relayStatusProvider: relayStatusProvider,
|
|
||||||
localBcastIntv: 30 * time.Second,
|
|
||||||
cacheLifetime: 5 * time.Minute,
|
|
||||||
negCacheCutoff: 3 * time.Minute,
|
|
||||||
addressRegistry: make(map[protocol.DeviceID][]CacheEntry),
|
|
||||||
relayRegistry: make(map[protocol.DeviceID][]CacheEntry),
|
|
||||||
lastLookup: make(map[protocol.DeviceID]time.Time),
|
|
||||||
registryLock: sync.NewRWMutex(),
|
|
||||||
mut: sync.NewRWMutex(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
type FinderMux interface {
|
||||||
if localPort > 0 {
|
Finder
|
||||||
d.startLocalIPv4Broadcasts(localPort)
|
ChildStatus() map[string]error
|
||||||
}
|
|
||||||
|
|
||||||
if len(localMCAddr) > 0 {
|
|
||||||
d.startLocalIPv6Multicasts(localMCAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.beacons) == 0 {
|
|
||||||
l.Warnln("Local discovery unavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d.localBcastTick = time.Tick(d.localBcastIntv)
|
|
||||||
d.forcedBcastTick = make(chan time.Time)
|
|
||||||
d.localBcastStart = time.Now()
|
|
||||||
go d.sendLocalAnnouncements()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) startLocalIPv4Broadcasts(localPort int) {
|
// The RelayStatusProvider answers questions about current relay status.
|
||||||
bb := beacon.NewBroadcast(localPort)
|
type RelayStatusProvider interface {
|
||||||
d.beacons = append(d.beacons, bb)
|
Relays() []string
|
||||||
go d.recvAnnouncements(bb)
|
RelayStatus(uri string) (time.Duration, bool)
|
||||||
bb.ServeBackground()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) startLocalIPv6Multicasts(localMCAddr string) {
|
// The AddressLister answers questions about what addresses we are listening
|
||||||
mb, err := beacon.NewMulticast(localMCAddr)
|
// on.
|
||||||
if err != nil {
|
type AddressLister interface {
|
||||||
if debug {
|
ExternalAddresses() []string
|
||||||
l.Debugln("beacon.NewMulticast:", err)
|
AllAddresses() []string
|
||||||
}
|
|
||||||
l.Infoln("Local discovery over IPv6 unavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.beacons = append(d.beacons, mb)
|
|
||||||
go d.recvAnnouncements(mb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) StartGlobal(servers []string, extAddr externalAddr) {
|
|
||||||
d.mut.Lock()
|
|
||||||
defer d.mut.Unlock()
|
|
||||||
|
|
||||||
if len(d.clients) > 0 {
|
|
||||||
d.stopGlobal()
|
|
||||||
}
|
|
||||||
|
|
||||||
d.extAddr = extAddr
|
|
||||||
wg := sync.NewWaitGroup()
|
|
||||||
clients := make(chan Client, len(servers))
|
|
||||||
for _, address := range servers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(addr string) {
|
|
||||||
defer wg.Done()
|
|
||||||
client, err := New(addr, d)
|
|
||||||
if err != nil {
|
|
||||||
l.Infoln("Error creating discovery client", addr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clients <- client
|
|
||||||
}(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(clients)
|
|
||||||
|
|
||||||
for client := range clients {
|
|
||||||
d.clients = append(d.clients, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) StopGlobal() {
|
|
||||||
d.mut.Lock()
|
|
||||||
defer d.mut.Unlock()
|
|
||||||
d.stopGlobal()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) stopGlobal() {
|
|
||||||
for _, client := range d.clients {
|
|
||||||
client.Stop()
|
|
||||||
}
|
|
||||||
d.clients = []Client{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) ExtAnnounceOK() map[string]bool {
|
|
||||||
d.mut.RLock()
|
|
||||||
defer d.mut.RUnlock()
|
|
||||||
|
|
||||||
ret := make(map[string]bool)
|
|
||||||
for _, client := range d.clients {
|
|
||||||
ret[client.Address()] = client.StatusOK()
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup returns a list of addresses the device is available at, as well as
|
|
||||||
// a list of relays the device is supposed to be available on sorted by the
|
|
||||||
// sum of latencies between this device, and the device in question.
|
|
||||||
func (d *Discoverer) Lookup(device protocol.DeviceID) ([]string, []string) {
|
|
||||||
d.registryLock.RLock()
|
|
||||||
cachedAddresses := d.filterCached(d.addressRegistry[device])
|
|
||||||
cachedRelays := d.filterCached(d.relayRegistry[device])
|
|
||||||
lastLookup := d.lastLookup[device]
|
|
||||||
d.registryLock.RUnlock()
|
|
||||||
|
|
||||||
d.mut.RLock()
|
|
||||||
defer d.mut.RUnlock()
|
|
||||||
|
|
||||||
relays := make([]string, len(cachedRelays))
|
|
||||||
for i := range cachedRelays {
|
|
||||||
relays[i] = cachedRelays[i].Address
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cachedAddresses) > 0 {
|
|
||||||
// There are cached address entries.
|
|
||||||
addrs := make([]string, len(cachedAddresses))
|
|
||||||
for i := range cachedAddresses {
|
|
||||||
addrs[i] = cachedAddresses[i].Address
|
|
||||||
}
|
|
||||||
return addrs, relays
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Since(lastLookup) < d.negCacheCutoff {
|
|
||||||
// We have recently tried to lookup this address and failed. Lets
|
|
||||||
// chill for a while.
|
|
||||||
return nil, relays
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
|
|
||||||
// Only perform external lookups if we have at least one external
|
|
||||||
// server client and one local announcement interval has passed. This is
|
|
||||||
// to avoid finding local peers on their remote address at startup.
|
|
||||||
results := make(chan Announce, len(d.clients))
|
|
||||||
wg := sync.NewWaitGroup()
|
|
||||||
for _, client := range d.clients {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(c Client) {
|
|
||||||
defer wg.Done()
|
|
||||||
ann, err := c.Lookup(device)
|
|
||||||
if err == nil {
|
|
||||||
results <- ann
|
|
||||||
}
|
|
||||||
|
|
||||||
}(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(results)
|
|
||||||
|
|
||||||
cachedAddresses := []CacheEntry{}
|
|
||||||
availableRelays := []Relay{}
|
|
||||||
seenAddresses := make(map[string]struct{})
|
|
||||||
seenRelays := make(map[string]struct{})
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
var addrs []string
|
|
||||||
for result := range results {
|
|
||||||
for _, addr := range result.This.Addresses {
|
|
||||||
_, ok := seenAddresses[addr]
|
|
||||||
if !ok {
|
|
||||||
cachedAddresses = append(cachedAddresses, CacheEntry{
|
|
||||||
Address: addr,
|
|
||||||
Seen: now,
|
|
||||||
})
|
|
||||||
seenAddresses[addr] = struct{}{}
|
|
||||||
addrs = append(addrs, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, relay := range result.This.Relays {
|
|
||||||
_, ok := seenRelays[relay.Address]
|
|
||||||
if !ok {
|
|
||||||
availableRelays = append(availableRelays, relay)
|
|
||||||
seenRelays[relay.Address] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relays = RelayAddressesSortedByLatency(availableRelays)
|
|
||||||
cachedRelays := make([]CacheEntry, len(relays))
|
|
||||||
for i := range relays {
|
|
||||||
cachedRelays[i] = CacheEntry{
|
|
||||||
Address: relays[i],
|
|
||||||
Seen: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d.registryLock.Lock()
|
|
||||||
d.addressRegistry[device] = cachedAddresses
|
|
||||||
d.relayRegistry[device] = cachedRelays
|
|
||||||
d.lastLookup[device] = time.Now()
|
|
||||||
d.registryLock.Unlock()
|
|
||||||
|
|
||||||
return addrs, relays
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, relays
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) Hint(device string, addrs []string) {
|
|
||||||
resAddrs := resolveAddrs(addrs)
|
|
||||||
var id protocol.DeviceID
|
|
||||||
id.UnmarshalText([]byte(device))
|
|
||||||
d.registerDevice(nil, Device{
|
|
||||||
Addresses: resAddrs,
|
|
||||||
ID: id[:],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
|
|
||||||
d.registryLock.RLock()
|
|
||||||
devices := make(map[protocol.DeviceID][]CacheEntry, len(d.addressRegistry))
|
|
||||||
for device, addrs := range d.addressRegistry {
|
|
||||||
addrsCopy := make([]CacheEntry, len(addrs))
|
|
||||||
copy(addrsCopy, addrs)
|
|
||||||
devices[device] = addrsCopy
|
|
||||||
}
|
|
||||||
d.registryLock.RUnlock()
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) Announcement() Announce {
|
|
||||||
return d.announcementPkt(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) announcementPkt(allowExternal bool) Announce {
|
|
||||||
var addrs []string
|
|
||||||
if allowExternal && d.extAddr != nil {
|
|
||||||
addrs = d.extAddr.ExternalAddresses()
|
|
||||||
} else {
|
|
||||||
addrs = resolveAddrs(d.listenAddrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
var relayAddrs []string
|
|
||||||
if d.relayStatusProvider != nil {
|
|
||||||
status := d.relayStatusProvider.ClientStatus()
|
|
||||||
for uri, ok := range status {
|
|
||||||
if ok {
|
|
||||||
relayAddrs = append(relayAddrs, uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Announce{
|
|
||||||
Magic: AnnouncementMagic,
|
|
||||||
This: Device{d.myID[:], addrs, measureLatency(relayAddrs)},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) sendLocalAnnouncements() {
|
|
||||||
var pkt = d.announcementPkt(false)
|
|
||||||
msg := pkt.MustMarshalXDR()
|
|
||||||
|
|
||||||
for {
|
|
||||||
for _, b := range d.beacons {
|
|
||||||
b.Send(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-d.localBcastTick:
|
|
||||||
case <-d.forcedBcastTick:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) recvAnnouncements(b beacon.Interface) {
|
|
||||||
for {
|
|
||||||
buf, addr := b.Recv()
|
|
||||||
|
|
||||||
var pkt Announce
|
|
||||||
err := pkt.UnmarshalXDR(buf)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
var newDevice bool
|
|
||||||
if bytes.Compare(pkt.This.ID, d.myID[:]) != 0 {
|
|
||||||
newDevice = d.registerDevice(addr, pkt.This)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newDevice {
|
|
||||||
select {
|
|
||||||
case d.forcedBcastTick <- time.Now():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
|
|
||||||
var id protocol.DeviceID
|
|
||||||
copy(id[:], device.ID)
|
|
||||||
|
|
||||||
d.registryLock.Lock()
|
|
||||||
defer d.registryLock.Unlock()
|
|
||||||
|
|
||||||
current := d.filterCached(d.addressRegistry[id])
|
|
||||||
|
|
||||||
orig := current
|
|
||||||
|
|
||||||
for _, deviceAddr := range device.Addresses {
|
|
||||||
uri, err := url.Parse(deviceAddr)
|
|
||||||
if err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover: Failed to parse address %s: %s", deviceAddr, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
host, port, err := net.SplitHostPort(uri.Host)
|
|
||||||
if err != nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover: Failed to split address host %s: %s", deviceAddr, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if host == "" {
|
|
||||||
uri.Host = net.JoinHostPort(addr.(*net.UDPAddr).IP.String(), port)
|
|
||||||
deviceAddr = uri.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range current {
|
|
||||||
if current[i].Address == deviceAddr {
|
|
||||||
current[i].Seen = time.Now()
|
|
||||||
goto done
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = append(current, CacheEntry{
|
|
||||||
Address: deviceAddr,
|
|
||||||
Seen: time.Now(),
|
|
||||||
})
|
|
||||||
done:
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover: Caching %s addresses: %v", id, current)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.addressRegistry[id] = current
|
|
||||||
|
|
||||||
if len(current) > len(orig) {
|
|
||||||
addrs := make([]string, len(current))
|
|
||||||
for i := range current {
|
|
||||||
addrs[i] = current[i].Address
|
|
||||||
}
|
|
||||||
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
|
|
||||||
"device": id.String(),
|
|
||||||
"addrs": addrs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(current) > len(orig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
|
|
||||||
for i := 0; i < len(c); {
|
|
||||||
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("discover: Removing cached entry %s - seen %v ago", c[i].Address, ago)
|
|
||||||
}
|
|
||||||
c[i] = c[len(c)-1]
|
|
||||||
c = c[:len(c)-1]
|
|
||||||
} else {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func addrToAddr(addr *net.TCPAddr) string {
|
|
||||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
|
||||||
return fmt.Sprintf(":%d", addr.Port)
|
|
||||||
} else if bs := addr.IP.To4(); bs != nil {
|
|
||||||
return fmt.Sprintf("%s:%d", bs.String(), addr.Port)
|
|
||||||
} else if bs := addr.IP.To16(); bs != nil {
|
|
||||||
return fmt.Sprintf("[%s]:%d", bs.String(), addr.Port)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveAddrs(addrs []string) []string {
|
|
||||||
var raddrs []string
|
|
||||||
for _, addrStr := range addrs {
|
|
||||||
uri, err := url.Parse(addrStr)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addr := addrToAddr(addrRes)
|
|
||||||
if len(addr) > 0 {
|
|
||||||
uri.Host = addr
|
|
||||||
raddrs = append(raddrs, uri.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return raddrs
|
|
||||||
}
|
|
||||||
|
|
||||||
func measureLatency(relayAdresses []string) []Relay {
|
|
||||||
relays := make([]Relay, 0, len(relayAdresses))
|
|
||||||
for i, addr := range relayAdresses {
|
|
||||||
relay := Relay{
|
|
||||||
Address: addr,
|
|
||||||
Latency: int32(time.Hour / time.Millisecond),
|
|
||||||
}
|
|
||||||
relays = append(relays, relay)
|
|
||||||
|
|
||||||
if latency, err := osutil.GetLatencyForURL(addr); err == nil {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("Relay %s latency %s", addr, latency)
|
|
||||||
}
|
|
||||||
relays[i].Latency = int32(latency / time.Millisecond)
|
|
||||||
} else {
|
|
||||||
l.Debugf("Failed to get relay %s latency %s", addr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return relays
|
|
||||||
}
|
|
||||||
|
|
||||||
// RelayAddressesSortedByLatency adds local latency to the relay, and sorts them
|
|
||||||
// by sum latency, and returns the addresses.
|
|
||||||
func RelayAddressesSortedByLatency(input []Relay) []string {
|
|
||||||
relays := make([]Relay, len(input))
|
|
||||||
copy(relays, input)
|
|
||||||
for i, relay := range relays {
|
|
||||||
if latency, err := osutil.GetLatencyForURL(relay.Address); err == nil {
|
|
||||||
relays[i].Latency += int32(latency / time.Millisecond)
|
|
||||||
} else {
|
|
||||||
relays[i].Latency += int32(time.Hour / time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(relayList(relays))
|
|
||||||
|
|
||||||
addresses := make([]string, 0, len(relays))
|
|
||||||
for _, relay := range relays {
|
|
||||||
addresses = append(addresses, relay.Address)
|
|
||||||
}
|
|
||||||
return addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
type relayList []Relay
|
|
||||||
|
|
||||||
func (l relayList) Len() int {
|
|
||||||
return len(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l relayList) Less(a, b int) bool {
|
|
||||||
return l[a].Latency < l[b].Latency
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l relayList) Swap(a, b int) {
|
|
||||||
l[a], l[b] = l[b], l[a]
|
|
||||||
}
|
}
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
// Copyright (C) 2014 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DummyClient struct {
|
|
||||||
url *url.URL
|
|
||||||
lookups []protocol.DeviceID
|
|
||||||
lookupRet Announce
|
|
||||||
stops int
|
|
||||||
statusRet bool
|
|
||||||
statusChecks int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DummyClient) Lookup(device protocol.DeviceID) (Announce, error) {
|
|
||||||
c.lookups = append(c.lookups, device)
|
|
||||||
return c.lookupRet, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DummyClient) StatusOK() bool {
|
|
||||||
c.statusChecks++
|
|
||||||
return c.statusRet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DummyClient) Stop() {
|
|
||||||
c.stops++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DummyClient) Address() string {
|
|
||||||
return c.url.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGlobalDiscovery(t *testing.T) {
|
|
||||||
c1 := &DummyClient{
|
|
||||||
statusRet: false,
|
|
||||||
lookupRet: Announce{
|
|
||||||
Magic: AnnouncementMagic,
|
|
||||||
This: Device{
|
|
||||||
ID: protocol.LocalDeviceID[:],
|
|
||||||
Addresses: []string{"test.com:1234"},
|
|
||||||
Relays: nil,
|
|
||||||
},
|
|
||||||
Extra: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
c2 := &DummyClient{
|
|
||||||
statusRet: true,
|
|
||||||
lookupRet: Announce{
|
|
||||||
Magic: AnnouncementMagic,
|
|
||||||
This: Device{
|
|
||||||
ID: protocol.LocalDeviceID[:],
|
|
||||||
Addresses: nil,
|
|
||||||
Relays: nil,
|
|
||||||
},
|
|
||||||
Extra: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
c3 := &DummyClient{
|
|
||||||
statusRet: true,
|
|
||||||
lookupRet: Announce{
|
|
||||||
Magic: AnnouncementMagic,
|
|
||||||
This: Device{
|
|
||||||
ID: protocol.LocalDeviceID[:],
|
|
||||||
Addresses: []string{"best.com:2345"},
|
|
||||||
Relays: nil,
|
|
||||||
},
|
|
||||||
Extra: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
clients := []*DummyClient{c1, c2}
|
|
||||||
|
|
||||||
Register("test1", func(uri *url.URL, ann Announcer) (Client, error) {
|
|
||||||
c := clients[0]
|
|
||||||
clients = clients[1:]
|
|
||||||
c.url = uri
|
|
||||||
return c, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
Register("test2", func(uri *url.URL, ann Announcer) (Client, error) {
|
|
||||||
c3.url = uri
|
|
||||||
return c3, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
d := NewDiscoverer(device, []string{}, nil)
|
|
||||||
d.localBcastStart = time.Time{}
|
|
||||||
servers := []string{
|
|
||||||
"test1://123.123.123.123:1234",
|
|
||||||
"test1://23.23.23.23:234",
|
|
||||||
"test2://234.234.234.234.2345",
|
|
||||||
}
|
|
||||||
d.StartGlobal(servers, nil)
|
|
||||||
|
|
||||||
if len(d.clients) != 3 {
|
|
||||||
t.Fatal("Wrong number of clients")
|
|
||||||
}
|
|
||||||
|
|
||||||
status := d.ExtAnnounceOK()
|
|
||||||
|
|
||||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
|
||||||
if status[c.url.String()] != c.statusRet || c.statusChecks != 1 {
|
|
||||||
t.Fatal("Wrong status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, _ := d.Lookup(device)
|
|
||||||
if len(addrs) != 2 {
|
|
||||||
t.Fatal("Wrong number of addresses", addrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range []string{"test.com:1234", "best.com:2345"} {
|
|
||||||
found := false
|
|
||||||
for _, laddr := range addrs {
|
|
||||||
if laddr == addr {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Fatal("Couldn't find", addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
|
||||||
if len(c.lookups) != 1 || c.lookups[0] != device {
|
|
||||||
t.Fatal("Wrong lookups")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, _ = d.Lookup(device)
|
|
||||||
if len(addrs) != 2 {
|
|
||||||
t.Fatal("Wrong number of addresses", addrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Answer should be cached, so number of lookups should have not increased
|
|
||||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
|
||||||
if len(c.lookups) != 1 || c.lookups[0] != device {
|
|
||||||
t.Fatal("Wrong lookups")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d.StopGlobal()
|
|
||||||
|
|
||||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
|
||||||
if c.stops != 1 {
|
|
||||||
t.Fatal("Wrong number of stops")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+69
-2
@@ -1,8 +1,75 @@
|
|||||||
// Copyright (C) 2014 The Syncthing Authors.
|
// Copyright (C) 2015 The Syncthing Authors.
|
||||||
//
|
//
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
// Package discover implements the device discovery protocol.
|
/*
|
||||||
|
Package discover implements the local and global device discovery protocols.
|
||||||
|
|
||||||
|
Global Discovery
|
||||||
|
================
|
||||||
|
|
||||||
|
Announcements
|
||||||
|
-------------
|
||||||
|
|
||||||
|
A device should announce itself at startup. It does this by an HTTPS POST to
|
||||||
|
the announce server URL (with the path usually being "/", but this is of
|
||||||
|
course up to the discovery server). The POST has a JSON payload listing direct
|
||||||
|
connection addresses (if any) and relay addresses (if any).
|
||||||
|
|
||||||
|
{
|
||||||
|
direct: ["tcp://192.0.2.45:22000", "tcp://:22202"],
|
||||||
|
relays: [{"url": "relay://192.0.2.99:22028", "latency": 142}]
|
||||||
|
}
|
||||||
|
|
||||||
|
It's OK for either of the "direct" or "relays" fields to be either the empty
|
||||||
|
list ([]), null, or missing entirely. An announcment with both fields missing
|
||||||
|
or empty is however not useful...
|
||||||
|
|
||||||
|
Any empty or unspecified IP addresses (i.e. addresses like tcp://:22000,
|
||||||
|
tcp://0.0.0.0:22000, tcp://[::]:22000) are interpreted as referring to the
|
||||||
|
source IP address of the announcement.
|
||||||
|
|
||||||
|
The device ID of the announcing device is not part of the announcement.
|
||||||
|
Instead, the server requires that the client perform certificate
|
||||||
|
authentication. The device ID is deduced from the presented certificate.
|
||||||
|
|
||||||
|
The server response is empty, with code 200 (OK) on success. If no certificate
|
||||||
|
was presented, status 403 (Forbidden) is returned. If the posted data doesn't
|
||||||
|
conform to the expected format, 400 (Bad Request) is returned.
|
||||||
|
|
||||||
|
In successfull responses, the server may return a "Reannounce-After" header
|
||||||
|
containing the number of seconds after which the client should perform a new
|
||||||
|
announcement.
|
||||||
|
|
||||||
|
In error responses, the server may return a "Retry-After" header containing
|
||||||
|
the number of seconds after which the client should retry.
|
||||||
|
|
||||||
|
Performing announcements significantly more often than indicated by the
|
||||||
|
Reannounce-After or Retry-After headers may result in the client being
|
||||||
|
throttled. In such cases the server may respond with status code 429 (Too Many
|
||||||
|
Requests).
|
||||||
|
|
||||||
|
Queries
|
||||||
|
=======
|
||||||
|
|
||||||
|
Queries are performed as HTTPS GET requests to the announce server URL. The
|
||||||
|
requested device ID is passed as the query parameter "device", in canonical
|
||||||
|
string form, i.e. https://announce.syncthing.net/?device=ABC12345-....
|
||||||
|
|
||||||
|
Successfull responses will have status code 200 (OK) and carry a JSON payload
|
||||||
|
of the same format as the announcement above. The response will not contain
|
||||||
|
empty or unspecified addresses.
|
||||||
|
|
||||||
|
If the "device" query parameter is missing or malformed, the status code 400
|
||||||
|
(Bad Request) is returned.
|
||||||
|
|
||||||
|
If the device ID is of a valid format but not found in the registry, 404 (Not
|
||||||
|
Found) is returned.
|
||||||
|
|
||||||
|
If the client has exceeded a rate limit, the server may respond with 429 (Too
|
||||||
|
Many Requests).
|
||||||
|
|
||||||
|
*/
|
||||||
package discover
|
package discover
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
package discover
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
stdsync "sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type globalClient struct {
|
||||||
|
server string
|
||||||
|
addrList AddressLister
|
||||||
|
relayStat RelayStatusProvider
|
||||||
|
announceClient httpClient
|
||||||
|
queryClient httpClient
|
||||||
|
noAnnounce bool
|
||||||
|
stop chan struct{}
|
||||||
|
errorHolder
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpClient interface {
|
||||||
|
Get(url string) (*http.Response, error)
|
||||||
|
Post(url, ctype string, data io.Reader) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultReannounceInterval = 30 * time.Minute
|
||||||
|
announceErrorRetryInterval = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type announcement struct {
|
||||||
|
Direct []string `json:"direct"`
|
||||||
|
Relays []Relay `json:"relays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverOptions struct {
|
||||||
|
insecure bool // don't check certificate
|
||||||
|
noAnnounce bool // don't announce
|
||||||
|
id string // expected server device ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
|
||||||
|
server, opts, err := parseOptions(server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var devID protocol.DeviceID
|
||||||
|
if opts.id != "" {
|
||||||
|
devID, err = protocol.DeviceIDFromString(opts.id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The http.Client used for announcements. It needs to have our
|
||||||
|
// certificate to prove our identity, and may or may not verify the server
|
||||||
|
// certificate depending on the insecure setting.
|
||||||
|
var announceClient httpClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: opts.insecure,
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if opts.id != "" {
|
||||||
|
announceClient = newIDCheckingHTTPClient(announceClient, devID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The http.Client used for queries. We don't need to present our
|
||||||
|
// certificate here, so lets not include it. May be insecure if requested.
|
||||||
|
var queryClient httpClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: opts.insecure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if opts.id != "" {
|
||||||
|
queryClient = newIDCheckingHTTPClient(queryClient, devID)
|
||||||
|
}
|
||||||
|
|
||||||
|
cl := &globalClient{
|
||||||
|
server: server,
|
||||||
|
addrList: addrList,
|
||||||
|
relayStat: relayStat,
|
||||||
|
announceClient: announceClient,
|
||||||
|
queryClient: queryClient,
|
||||||
|
noAnnounce: opts.noAnnounce,
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
cl.setError(errors.New("not announced"))
|
||||||
|
|
||||||
|
return cl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup returns the list of addresses where the given device is available;
|
||||||
|
// direct, and via relays.
|
||||||
|
func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||||
|
qURL, err := url.Parse(c.server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := qURL.Query()
|
||||||
|
q.Set("device", device.String())
|
||||||
|
qURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := c.queryClient.Get(qURL.String())
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("globalClient.Lookup", qURL.String(), err)
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
resp.Body.Close()
|
||||||
|
if debug {
|
||||||
|
l.Debugln("globalClient.Lookup", qURL.String(), resp.Status)
|
||||||
|
}
|
||||||
|
return nil, nil, errors.New(resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle 429 and Retry-After?
|
||||||
|
|
||||||
|
var ann announcement
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&ann)
|
||||||
|
resp.Body.Close()
|
||||||
|
return ann.Direct, ann.Relays, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *globalClient) String() string {
|
||||||
|
return "global@" + c.server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *globalClient) Serve() {
|
||||||
|
if c.noAnnounce {
|
||||||
|
// We're configured to not do announcements, only lookups. To maintain
|
||||||
|
// the same interface, we just pause here if Serve() is run.
|
||||||
|
<-c.stop
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := time.NewTimer(0)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged | events.RelayStateChanged)
|
||||||
|
defer events.Default.Unsubscribe(eventSub)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-eventSub.C():
|
||||||
|
c.sendAnnouncement(timer)
|
||||||
|
|
||||||
|
case <-timer.C:
|
||||||
|
c.sendAnnouncement(timer)
|
||||||
|
|
||||||
|
case <-c.stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *globalClient) sendAnnouncement(timer *time.Timer) {
|
||||||
|
|
||||||
|
var ann announcement
|
||||||
|
if c.addrList != nil {
|
||||||
|
ann.Direct = c.addrList.ExternalAddresses()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.relayStat != nil {
|
||||||
|
for _, relay := range c.relayStat.Relays() {
|
||||||
|
latency, ok := c.relayStat.RelayStatus(relay)
|
||||||
|
if ok {
|
||||||
|
ann.Relays = append(ann.Relays, Relay{
|
||||||
|
URL: relay,
|
||||||
|
Latency: int32(latency / time.Millisecond),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ann.Direct)+len(ann.Relays) == 0 {
|
||||||
|
c.setError(errors.New("nothing to announce"))
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Nothing to announce")
|
||||||
|
}
|
||||||
|
timer.Reset(announceErrorRetryInterval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The marshal doesn't fail, I promise.
|
||||||
|
postData, _ := json.Marshal(ann)
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
l.Debugf("Announcement: %s", postData)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.announceClient.Post(c.server, "application/json", bytes.NewReader(postData))
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("announce POST:", err)
|
||||||
|
}
|
||||||
|
c.setError(err)
|
||||||
|
timer.Reset(announceErrorRetryInterval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
l.Debugln("announce POST:", resp.Status)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("announce POST:", resp.Status)
|
||||||
|
}
|
||||||
|
c.setError(errors.New(resp.Status))
|
||||||
|
|
||||||
|
if h := resp.Header.Get("Retry-After"); h != "" {
|
||||||
|
// The server has a recommendation on when we should
|
||||||
|
// retry. Follow it.
|
||||||
|
if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("announce Retry-After:", secs, err)
|
||||||
|
}
|
||||||
|
timer.Reset(time.Duration(secs) * time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.Reset(announceErrorRetryInterval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setError(nil)
|
||||||
|
|
||||||
|
if h := resp.Header.Get("Reannounce-After"); h != "" {
|
||||||
|
// The server has a recommendation on when we should
|
||||||
|
// reannounce. Follow it.
|
||||||
|
if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("announce Reannounce-After:", secs, err)
|
||||||
|
}
|
||||||
|
timer.Reset(time.Duration(secs) * time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.Reset(defaultReannounceInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *globalClient) Stop() {
|
||||||
|
close(c.stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *globalClient) Cache() map[protocol.DeviceID]CacheEntry {
|
||||||
|
// The globalClient doesn't do caching
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOptions parses and strips away any ?query=val options, setting the
|
||||||
|
// corresponding field in the serverOptions struct. Unknown query options are
|
||||||
|
// ignored and removed.
|
||||||
|
func parseOptions(dsn string) (server string, opts serverOptions, err error) {
|
||||||
|
p, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return "", serverOptions{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab known options from the query string
|
||||||
|
q := p.Query()
|
||||||
|
opts.id = q.Get("id")
|
||||||
|
opts.insecure = opts.id != "" || queryBool(q, "insecure")
|
||||||
|
opts.noAnnounce = queryBool(q, "noannounce")
|
||||||
|
|
||||||
|
// Check for disallowed combinations
|
||||||
|
if p.Scheme == "http" {
|
||||||
|
if !opts.insecure {
|
||||||
|
return "", serverOptions{}, errors.New("http without insecure not supported")
|
||||||
|
}
|
||||||
|
if !opts.noAnnounce {
|
||||||
|
return "", serverOptions{}, errors.New("http without noannounce not supported")
|
||||||
|
}
|
||||||
|
} else if p.Scheme != "https" {
|
||||||
|
return "", serverOptions{}, errors.New("unsupported scheme " + p.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the query string
|
||||||
|
p.RawQuery = ""
|
||||||
|
server = p.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryBool returns the query parameter parsed as a boolean. An empty value
|
||||||
|
// ("?foo") is considered true, as is any value string except false
|
||||||
|
// ("?foo=false").
|
||||||
|
func queryBool(q url.Values, key string) bool {
|
||||||
|
if _, ok := q[key]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Get(key) != "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
type idCheckingHTTPClient struct {
|
||||||
|
httpClient
|
||||||
|
id protocol.DeviceID
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIDCheckingHTTPClient(client httpClient, id protocol.DeviceID) *idCheckingHTTPClient {
|
||||||
|
return &idCheckingHTTPClient{
|
||||||
|
httpClient: client,
|
||||||
|
id: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *idCheckingHTTPClient) check(resp *http.Response) error {
|
||||||
|
if resp.TLS == nil {
|
||||||
|
return errors.New("security: not TLS")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.TLS.PeerCertificates) == 0 {
|
||||||
|
return errors.New("security: no certificates")
|
||||||
|
}
|
||||||
|
|
||||||
|
id := protocol.NewDeviceID(resp.TLS.PeerCertificates[0].Raw)
|
||||||
|
if !id.Equals(c.id) {
|
||||||
|
return errors.New("security: incorrect device id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *idCheckingHTTPClient) Get(url string) (*http.Response, error) {
|
||||||
|
resp, err := c.httpClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := c.check(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *idCheckingHTTPClient) Post(url, ctype string, data io.Reader) (*http.Response, error) {
|
||||||
|
resp, err := c.httpClient.Post(url, ctype, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := c.check(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorHolder struct {
|
||||||
|
err error
|
||||||
|
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errorHolder) setError(err error) {
|
||||||
|
e.mut.Lock()
|
||||||
|
e.err = err
|
||||||
|
e.mut.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errorHolder) Error() error {
|
||||||
|
e.mut.Lock()
|
||||||
|
err := e.err
|
||||||
|
e.mut.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package discover
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseOptions(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
opts serverOptions
|
||||||
|
}{
|
||||||
|
{"https://example.com/", "https://example.com/", serverOptions{}},
|
||||||
|
{"https://example.com/?insecure", "https://example.com/", serverOptions{insecure: true}},
|
||||||
|
{"https://example.com/?insecure=true", "https://example.com/", serverOptions{insecure: true}},
|
||||||
|
{"https://example.com/?insecure=yes", "https://example.com/", serverOptions{insecure: true}},
|
||||||
|
{"https://example.com/?insecure=false&noannounce", "https://example.com/", serverOptions{noAnnounce: true}},
|
||||||
|
{"https://example.com/?id=abc", "https://example.com/", serverOptions{id: "abc", insecure: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
res, opts, err := parseOptions(tc.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected err %v for %v", err, tc.in)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res != tc.out {
|
||||||
|
t.Errorf("Incorrect server, %v!= %v for %v", res, tc.out, tc.in)
|
||||||
|
}
|
||||||
|
if opts != tc.opts {
|
||||||
|
t.Errorf("Incorrect options, %v!= %v for %v", opts, tc.opts, tc.in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalOverHTTP(t *testing.T) {
|
||||||
|
// HTTP works for queries, but is obviously insecure and we can't do
|
||||||
|
// announces over it (as we don't present a certificate). As such, http://
|
||||||
|
// is only allowed in combination with the "insecure" and "noannounce"
|
||||||
|
// parameters.
|
||||||
|
|
||||||
|
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, nil); err == nil {
|
||||||
|
t.Fatal("http is not allowed without insecure and noannounce")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil, nil); err == nil {
|
||||||
|
t.Fatal("http is not allowed without noannounce")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, nil); err == nil {
|
||||||
|
t.Fatal("http is not allowed without insecure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now lets check that lookups work over HTTP, given the correct options.
|
||||||
|
|
||||||
|
list, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer list.Close()
|
||||||
|
|
||||||
|
s := new(fakeDiscoveryServer)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", s.handler)
|
||||||
|
go http.Serve(list, mux)
|
||||||
|
|
||||||
|
direct, relays, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||||
|
t.Errorf("incorrect direct list: %+v", direct)
|
||||||
|
}
|
||||||
|
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||||
|
t.Errorf("incorrect relays list: %+v", direct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalOverHTTPS(t *testing.T) {
|
||||||
|
dir, err := ioutil.TempDir("", "syncthing")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||||
|
// process along a bit.
|
||||||
|
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer list.Close()
|
||||||
|
|
||||||
|
s := new(fakeDiscoveryServer)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", s.handler)
|
||||||
|
go http.Serve(list, mux)
|
||||||
|
|
||||||
|
// With default options the lookup code expects the server certificate to
|
||||||
|
// check out according to the usual CA chains etc. That won't be the case
|
||||||
|
// here so we expect the lookup to fail.
|
||||||
|
|
||||||
|
url := "https://" + list.Addr().String()
|
||||||
|
if _, _, err := testLookup(url); err == nil {
|
||||||
|
t.Fatalf("unexpected nil error when we should have got a certificate error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// With "insecure" set, whatever certificate is on the other side should
|
||||||
|
// be accepted.
|
||||||
|
|
||||||
|
url = "https://" + list.Addr().String() + "?insecure"
|
||||||
|
if direct, relays, err := testLookup(url); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
} else {
|
||||||
|
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||||
|
t.Errorf("incorrect direct list: %+v", direct)
|
||||||
|
}
|
||||||
|
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||||
|
t.Errorf("incorrect relays list: %+v", direct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With "id" set to something incorrect, the checks should fail again.
|
||||||
|
|
||||||
|
url = "https://" + list.Addr().String() + "?id=" + protocol.LocalDeviceID.String()
|
||||||
|
if _, _, err := testLookup(url); err == nil {
|
||||||
|
t.Fatalf("unexpected nil error for incorrect discovery server ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the correct device ID, the check should pass and we should get a
|
||||||
|
// lookup response.
|
||||||
|
|
||||||
|
id := protocol.NewDeviceID(cert.Certificate[0])
|
||||||
|
url = "https://" + list.Addr().String() + "?id=" + id.String()
|
||||||
|
if direct, relays, err := testLookup(url); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
} else {
|
||||||
|
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||||
|
t.Errorf("incorrect direct list: %+v", direct)
|
||||||
|
}
|
||||||
|
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||||
|
t.Errorf("incorrect relays list: %+v", direct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalAnnounce(t *testing.T) {
|
||||||
|
dir, err := ioutil.TempDir("", "syncthing")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||||
|
// process along a bit.
|
||||||
|
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer list.Close()
|
||||||
|
|
||||||
|
s := new(fakeDiscoveryServer)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", s.handler)
|
||||||
|
go http.Serve(list, mux)
|
||||||
|
|
||||||
|
url := "https://" + list.Addr().String() + "?insecure"
|
||||||
|
disco, err := NewGlobal(url, cert, new(fakeAddressLister), new(fakeRelayStatus))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go disco.Serve()
|
||||||
|
defer disco.Stop()
|
||||||
|
|
||||||
|
// The discovery thing should attempt an announcement immediately. We wait
|
||||||
|
// for it to succeed, a while.
|
||||||
|
t0 := time.Now()
|
||||||
|
for err := disco.Error(); err != nil; err = disco.Error() {
|
||||||
|
if time.Since(t0) > 10*time.Second {
|
||||||
|
t.Fatal("announce failed:", err)
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(string(s.announce), "tcp://0.0.0.0:22000") {
|
||||||
|
t.Errorf("announce missing direct address: %s", s.announce)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(s.announce), "relay://192.0.2.42:443") {
|
||||||
|
t.Errorf("announce missing relay address: %s", s.announce)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLookup(url string) ([]string, []Relay, error) {
|
||||||
|
disco, err := NewGlobal(url, tls.Certificate{}, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
go disco.Serve()
|
||||||
|
defer disco.Stop()
|
||||||
|
|
||||||
|
return disco.Lookup(protocol.LocalDeviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeDiscoveryServer struct {
|
||||||
|
announce []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeDiscoveryServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "POST" {
|
||||||
|
s.announce, _ = ioutil.ReadAll(r.Body)
|
||||||
|
w.WriteHeader(204)
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(`{"direct":["tcp://192.0.2.42::22000"], "relays":[{"url": "relay://192.0.2.43:443", "latency": 42}]}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeAddressLister struct{}
|
||||||
|
|
||||||
|
func (f *fakeAddressLister) ExternalAddresses() []string {
|
||||||
|
return []string{"tcp://0.0.0.0:22000"}
|
||||||
|
}
|
||||||
|
func (f *fakeAddressLister) AllAddresses() []string {
|
||||||
|
return []string{"tcp://0.0.0.0:22000", "tcp://192.168.0.1:22000"}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRelayStatus struct{}
|
||||||
|
|
||||||
|
func (f *fakeRelayStatus) Relays() []string {
|
||||||
|
return []string{"relay://192.0.2.42:443"}
|
||||||
|
}
|
||||||
|
func (f *fakeRelayStatus) RelayStatus(uri string) (time.Duration, bool) {
|
||||||
|
return 42 * time.Millisecond, true
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
// Copyright (C) 2014 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package discover
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/beacon"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/thejerf/suture"
|
||||||
|
)
|
||||||
|
|
||||||
|
type localClient struct {
|
||||||
|
*suture.Supervisor
|
||||||
|
myID protocol.DeviceID
|
||||||
|
addrList AddressLister
|
||||||
|
relayStat RelayStatusProvider
|
||||||
|
name string
|
||||||
|
|
||||||
|
beacon beacon.Interface
|
||||||
|
localBcastStart time.Time
|
||||||
|
localBcastTick <-chan time.Time
|
||||||
|
forcedBcastTick chan time.Time
|
||||||
|
|
||||||
|
*cache
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
BroadcastInterval = 30 * time.Second
|
||||||
|
CacheLifeTime = 3 * BroadcastInterval
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrIncorrectMagic = errors.New("incorrect magic number")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
|
||||||
|
c := &localClient{
|
||||||
|
Supervisor: suture.NewSimple("local"),
|
||||||
|
myID: id,
|
||||||
|
addrList: addrList,
|
||||||
|
relayStat: relayStat,
|
||||||
|
localBcastTick: time.Tick(BroadcastInterval),
|
||||||
|
forcedBcastTick: make(chan time.Time),
|
||||||
|
localBcastStart: time.Now(),
|
||||||
|
cache: newCache(),
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(host) == 0 {
|
||||||
|
// A broadcast client
|
||||||
|
c.name = "IPv4 local"
|
||||||
|
bcPort, err := strconv.Atoi(port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.startLocalIPv4Broadcasts(bcPort)
|
||||||
|
} else {
|
||||||
|
// A multicast client
|
||||||
|
c.name = "IPv6 local"
|
||||||
|
c.startLocalIPv6Multicasts(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
go c.sendLocalAnnouncements()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) startLocalIPv4Broadcasts(localPort int) {
|
||||||
|
c.beacon = beacon.NewBroadcast(localPort)
|
||||||
|
c.Add(c.beacon)
|
||||||
|
go c.recvAnnouncements(c.beacon)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) startLocalIPv6Multicasts(localMCAddr string) {
|
||||||
|
c.beacon = beacon.NewMulticast(localMCAddr)
|
||||||
|
c.Add(c.beacon)
|
||||||
|
go c.recvAnnouncements(c.beacon)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup returns a list of addresses the device is available at. Local
|
||||||
|
// discovery never returns relays.
|
||||||
|
func (c *localClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||||
|
if cache, ok := c.Get(device); ok {
|
||||||
|
if time.Since(cache.when) < CacheLifeTime {
|
||||||
|
direct = cache.Direct
|
||||||
|
relays = cache.Relays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) String() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) Error() error {
|
||||||
|
return c.beacon.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) announcementPkt() Announce {
|
||||||
|
addrs := c.addrList.AllAddresses()
|
||||||
|
|
||||||
|
var relays []Relay
|
||||||
|
for _, relay := range c.relayStat.Relays() {
|
||||||
|
latency, ok := c.relayStat.RelayStatus(relay)
|
||||||
|
if ok {
|
||||||
|
relays = append(relays, Relay{
|
||||||
|
URL: relay,
|
||||||
|
Latency: int32(latency / time.Millisecond),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Announce{
|
||||||
|
Magic: AnnouncementMagic,
|
||||||
|
This: Device{
|
||||||
|
ID: c.myID[:],
|
||||||
|
Addresses: addrs,
|
||||||
|
Relays: relays,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) sendLocalAnnouncements() {
|
||||||
|
var pkt = c.announcementPkt()
|
||||||
|
msg := pkt.MustMarshalXDR()
|
||||||
|
|
||||||
|
for {
|
||||||
|
c.beacon.Send(msg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-c.localBcastTick:
|
||||||
|
case <-c.forcedBcastTick:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) recvAnnouncements(b beacon.Interface) {
|
||||||
|
for {
|
||||||
|
buf, addr := b.Recv()
|
||||||
|
|
||||||
|
var pkt Announce
|
||||||
|
err := pkt.UnmarshalXDR(buf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
if debug {
|
||||||
|
l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
var newDevice bool
|
||||||
|
if bytes.Compare(pkt.This.ID, c.myID[:]) != 0 {
|
||||||
|
newDevice = c.registerDevice(addr, pkt.This)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newDevice {
|
||||||
|
select {
|
||||||
|
case c.forcedBcastTick <- time.Now():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *localClient) registerDevice(src net.Addr, device Device) bool {
|
||||||
|
var id protocol.DeviceID
|
||||||
|
copy(id[:], device.ID)
|
||||||
|
|
||||||
|
// Remember whether we already had a valid cache entry for this device.
|
||||||
|
|
||||||
|
ce, existsAlready := c.Get(id)
|
||||||
|
isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime
|
||||||
|
|
||||||
|
// Any empty or unspecified addresses should be set to the source address
|
||||||
|
// of the announcement. We also skip any addresses we can't parse.
|
||||||
|
|
||||||
|
var validAddresses []string
|
||||||
|
for _, addr := range device.Addresses {
|
||||||
|
u, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp", u.Host)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tcpAddr.IP) == 0 || tcpAddr.IP.IsUnspecified() {
|
||||||
|
host, _, err := net.SplitHostPort(src.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.Host = fmt.Sprintf("%s:%d", host, tcpAddr.Port)
|
||||||
|
validAddresses = append(validAddresses, u.String())
|
||||||
|
} else {
|
||||||
|
validAddresses = append(validAddresses, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set(id, CacheEntry{
|
||||||
|
Direct: validAddresses,
|
||||||
|
Relays: device.Relays,
|
||||||
|
when: time.Now(),
|
||||||
|
found: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if isNewDevice {
|
||||||
|
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
|
||||||
|
"device": id.String(),
|
||||||
|
"addrs": device.Addresses,
|
||||||
|
"relays": device.Relays,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNewDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
func addrToAddr(addr *net.TCPAddr) string {
|
||||||
|
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||||
|
return fmt.Sprintf(":%c", addr.Port)
|
||||||
|
} else if bs := addr.IP.To4(); bs != nil {
|
||||||
|
return fmt.Sprintf("%s:%c", bs.String(), addr.Port)
|
||||||
|
} else if bs := addr.IP.To16(); bs != nil {
|
||||||
|
return fmt.Sprintf("[%s]:%c", bs.String(), addr.Port)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveAddrs(addrs []string) []string {
|
||||||
|
var raddrs []string
|
||||||
|
for _, addrStr := range addrs {
|
||||||
|
uri, err := url.Parse(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr := addrToAddr(addrRes)
|
||||||
|
if len(addr) > 0 {
|
||||||
|
uri.Host = addr
|
||||||
|
raddrs = append(raddrs, uri.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raddrs
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||||
//go:generate genxdr -o packets_xdr.go packets.go
|
//go:generate genxdr -o localpackets_xdr.go localpackets.go
|
||||||
|
|
||||||
package discover
|
package discover
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ type Announce struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Relay struct {
|
type Relay struct {
|
||||||
Address string // max:256
|
URL string `json:"url"` // max:2083
|
||||||
Latency int32
|
Latency int32 `json:"latency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
@@ -192,10 +192,10 @@ Relay Structure:
|
|||||||
0 1 2 3
|
0 1 2 3
|
||||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| Length of Address |
|
| Length of URL |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
/ /
|
/ /
|
||||||
\ Address (variable length) \
|
\ URL (variable length) \
|
||||||
/ /
|
/ /
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| Latency |
|
| Latency |
|
||||||
@@ -203,7 +203,7 @@ Relay Structure:
|
|||||||
|
|
||||||
|
|
||||||
struct Relay {
|
struct Relay {
|
||||||
string Address<256>;
|
string URL<256>;
|
||||||
int Latency;
|
int Latency;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,10 +234,10 @@ func (o Relay) AppendXDR(bs []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
if l := len(o.Address); l > 256 {
|
if l := len(o.URL); l > 256 {
|
||||||
return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 256)
|
return xw.Tot(), xdr.ElementSizeExceeded("URL", l, 256)
|
||||||
}
|
}
|
||||||
xw.WriteString(o.Address)
|
xw.WriteString(o.URL)
|
||||||
xw.WriteUint32(uint32(o.Latency))
|
xw.WriteUint32(uint32(o.Latency))
|
||||||
return xw.Tot(), xw.Error()
|
return xw.Tot(), xw.Error()
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ func (o *Relay) UnmarshalXDR(bs []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error {
|
func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
o.Address = xr.ReadStringMax(256)
|
o.URL = xr.ReadStringMax(256)
|
||||||
o.Latency = int32(xr.ReadUint32())
|
o.Latency = int32(xr.ReadUint32())
|
||||||
return xr.Error()
|
return xr.Error()
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,7 @@ const (
|
|||||||
FolderErrors
|
FolderErrors
|
||||||
FolderScanProgress
|
FolderScanProgress
|
||||||
ExternalPortMappingChanged
|
ExternalPortMappingChanged
|
||||||
|
RelayStateChanged
|
||||||
|
|
||||||
AllEvents = (1 << iota) - 1
|
AllEvents = (1 << iota) - 1
|
||||||
)
|
)
|
||||||
@@ -90,6 +91,8 @@ func (t EventType) String() string {
|
|||||||
return "FolderScanProgress"
|
return "FolderScanProgress"
|
||||||
case ExternalPortMappingChanged:
|
case ExternalPortMappingChanged:
|
||||||
return "ExternalPortMappingChanged"
|
return "ExternalPortMappingChanged"
|
||||||
|
case RelayStateChanged:
|
||||||
|
return "RelayStateChanged"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -236,6 +237,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
|
|||||||
includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
|
includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
|
||||||
includes, err := loadIgnoreFile(includeFile, seen)
|
includes, err := loadIgnoreFile(includeFile, seen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
patterns = append(patterns, includes...)
|
patterns = append(patterns, includes...)
|
||||||
|
|||||||
+137
-11
@@ -12,18 +12,23 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/relaysrv/client"
|
"github.com/syncthing/relaysrv/client"
|
||||||
"github.com/syncthing/relaysrv/protocol"
|
"github.com/syncthing/relaysrv/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
|
|
||||||
"github.com/thejerf/suture"
|
"github.com/thejerf/suture"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventBroadcasterCheckInterval = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
type Svc struct {
|
type Svc struct {
|
||||||
*suture.Supervisor
|
*suture.Supervisor
|
||||||
cfg *config.Wrapper
|
cfg *config.Wrapper
|
||||||
@@ -71,7 +76,12 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
|
|||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventBc := &eventBroadcaster{
|
||||||
|
svc: svc,
|
||||||
|
}
|
||||||
|
|
||||||
svc.Add(receiver)
|
svc.Add(receiver)
|
||||||
|
svc.Add(eventBc)
|
||||||
|
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
@@ -132,7 +142,7 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
dynRelays := make([]discover.Relay, 0, len(ann.Relays))
|
var dynRelayAddrs []string
|
||||||
for _, relayAnn := range ann.Relays {
|
for _, relayAnn := range ann.Relays {
|
||||||
ruri, err := url.Parse(relayAnn.URL)
|
ruri, err := url.Parse(relayAnn.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -144,13 +154,11 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
if debug {
|
if debug {
|
||||||
l.Debugln("Found", ruri, "via", uri)
|
l.Debugln("Found", ruri, "via", uri)
|
||||||
}
|
}
|
||||||
dynRelays = append(dynRelays, discover.Relay{
|
dynRelayAddrs = append(dynRelayAddrs, ruri.String())
|
||||||
Address: ruri.String(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dynRelayAddrs := discover.RelayAddressesSortedByLatency(dynRelays)
|
|
||||||
if len(dynRelayAddrs) > 0 {
|
if len(dynRelayAddrs) > 0 {
|
||||||
|
dynRelayAddrs = relayAddressesSortedByLatency(dynRelayAddrs)
|
||||||
closestRelay := dynRelayAddrs[0]
|
closestRelay := dynRelayAddrs[0]
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugln("Picking", closestRelay, "as closest dynamic relay from", uri)
|
l.Debugln("Picking", closestRelay, "as closest dynamic relay from", uri)
|
||||||
@@ -193,7 +201,14 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Svc) ClientStatus() map[string]bool {
|
type Status struct {
|
||||||
|
URL string
|
||||||
|
OK bool
|
||||||
|
Latency int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relays return the list of relays that currently have an OK status.
|
||||||
|
func (s *Svc) Relays() []string {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
// A nil client does not have a status, really. Yet we may be called
|
// A nil client does not have a status, really. Yet we may be called
|
||||||
// this way, for raisins...
|
// this way, for raisins...
|
||||||
@@ -201,12 +216,34 @@ func (s *Svc) ClientStatus() map[string]bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.mut.RLock()
|
s.mut.RLock()
|
||||||
status := make(map[string]bool, len(s.clients))
|
relays := make([]string, 0, len(s.clients))
|
||||||
for uri, client := range s.clients {
|
for uri := range s.clients {
|
||||||
status[uri] = client.StatusOK()
|
relays = append(relays, uri)
|
||||||
}
|
}
|
||||||
s.mut.RUnlock()
|
s.mut.RUnlock()
|
||||||
return status
|
|
||||||
|
sort.Strings(relays)
|
||||||
|
|
||||||
|
return relays
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelayStatus returns the latency and OK status for a given relay.
|
||||||
|
func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
|
||||||
|
if s == nil {
|
||||||
|
// A nil client does not have a status, really. Yet we may be called
|
||||||
|
// this way, for raisins...
|
||||||
|
return time.Hour, false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mut.RLock()
|
||||||
|
client, ok := s.clients[uri]
|
||||||
|
s.mut.RUnlock()
|
||||||
|
|
||||||
|
if !ok || !client.StatusOK() {
|
||||||
|
return time.Hour, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Latency(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept returns a new *tls.Conn. The connection is already handshaken.
|
// Accept returns a new *tls.Conn. The connection is already handshaken.
|
||||||
@@ -266,6 +303,55 @@ func (r *invitationReceiver) Stop() {
|
|||||||
close(r.stop)
|
close(r.stop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The eventBroadcaster sends a RelayStateChanged event when the relay status
|
||||||
|
// changes. We need this somewhat ugly polling mechanism as there's currently
|
||||||
|
// no way to get the event feed directly from the relay lib. This may be
|
||||||
|
// somethign to revisit later, possibly.
|
||||||
|
type eventBroadcaster struct {
|
||||||
|
svc *Svc
|
||||||
|
stop chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventBroadcaster) Serve() {
|
||||||
|
timer := time.NewTicker(eventBroadcasterCheckInterval)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
var prevOKRelays []string
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
curOKRelays := e.svc.Relays()
|
||||||
|
|
||||||
|
changed := len(curOKRelays) != len(prevOKRelays)
|
||||||
|
if !changed {
|
||||||
|
for i := range curOKRelays {
|
||||||
|
if curOKRelays[i] != prevOKRelays[i] {
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
events.Default.Log(events.RelayStateChanged, map[string][]string{
|
||||||
|
"old": prevOKRelays,
|
||||||
|
"new": curOKRelays,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
prevOKRelays = curOKRelays
|
||||||
|
|
||||||
|
case <-e.stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventBroadcaster) Stop() {
|
||||||
|
close(e.stop)
|
||||||
|
}
|
||||||
|
|
||||||
// This is the announcement recieved from the relay server;
|
// This is the announcement recieved from the relay server;
|
||||||
// {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]}
|
// {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]}
|
||||||
type dynamicAnnouncement struct {
|
type dynamicAnnouncement struct {
|
||||||
@@ -273,3 +359,43 @@ type dynamicAnnouncement struct {
|
|||||||
URL string
|
URL string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// relayAddressesSortedByLatency adds local latency to the relay, and sorts them
|
||||||
|
// by sum latency, and returns the addresses.
|
||||||
|
func relayAddressesSortedByLatency(input []string) []string {
|
||||||
|
relays := make(relayList, len(input))
|
||||||
|
for i, relay := range input {
|
||||||
|
if latency, err := osutil.GetLatencyForURL(relay); err == nil {
|
||||||
|
relays[i] = relayWithLatency{relay, int(latency / time.Millisecond)}
|
||||||
|
} else {
|
||||||
|
relays[i] = relayWithLatency{relay, int(time.Hour / time.Millisecond)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(relays)
|
||||||
|
|
||||||
|
addresses := make([]string, len(relays))
|
||||||
|
for i, relay := range relays {
|
||||||
|
addresses[i] = relay.relay
|
||||||
|
}
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
type relayWithLatency struct {
|
||||||
|
relay string
|
||||||
|
latency int
|
||||||
|
}
|
||||||
|
|
||||||
|
type relayList []relayWithLatency
|
||||||
|
|
||||||
|
func (l relayList) Len() int {
|
||||||
|
return len(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l relayList) Less(a, b int) bool {
|
||||||
|
return l[a].latency < l[b].latency
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l relayList) Swap(a, b int) {
|
||||||
|
l[a], l[b] = l[b], l[a]
|
||||||
|
}
|
||||||
+1
-2
@@ -46,8 +46,7 @@
|
|||||||
</gui>
|
</gui>
|
||||||
<options>
|
<options>
|
||||||
<listenAddress>tcp://127.0.0.1:22001</listenAddress>
|
<listenAddress>tcp://127.0.0.1:22001</listenAddress>
|
||||||
<globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
|
<globalAnnounceServer>default</globalAnnounceServer>
|
||||||
<globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
|
|
||||||
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
||||||
<localAnnounceEnabled>true</localAnnounceEnabled>
|
<localAnnounceEnabled>true</localAnnounceEnabled>
|
||||||
<localAnnouncePort>21025</localAnnouncePort>
|
<localAnnouncePort>21025</localAnnouncePort>
|
||||||
|
|||||||
+1
-2
@@ -53,8 +53,7 @@
|
|||||||
</gui>
|
</gui>
|
||||||
<options>
|
<options>
|
||||||
<listenAddress>tcp://127.0.0.1:22002</listenAddress>
|
<listenAddress>tcp://127.0.0.1:22002</listenAddress>
|
||||||
<globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
|
<globalAnnounceServer>default</globalAnnounceServer>
|
||||||
<globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
|
|
||||||
<globalAnnounceEnabled>true</globalAnnounceEnabled>
|
<globalAnnounceEnabled>true</globalAnnounceEnabled>
|
||||||
<localAnnounceEnabled>true</localAnnounceEnabled>
|
<localAnnounceEnabled>true</localAnnounceEnabled>
|
||||||
<localAnnouncePort>21025</localAnnouncePort>
|
<localAnnouncePort>21025</localAnnouncePort>
|
||||||
|
|||||||
+1
-2
@@ -39,8 +39,7 @@
|
|||||||
</gui>
|
</gui>
|
||||||
<options>
|
<options>
|
||||||
<listenAddress>tcp://127.0.0.1:22003</listenAddress>
|
<listenAddress>tcp://127.0.0.1:22003</listenAddress>
|
||||||
<globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
|
<globalAnnounceServer>default</globalAnnounceServer>
|
||||||
<globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
|
|
||||||
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
||||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
<localAnnounceEnabled>false</localAnnounceEnabled>
|
||||||
<localAnnouncePort>21025</localAnnouncePort>
|
<localAnnouncePort>21025</localAnnouncePort>
|
||||||
|
|||||||
+1
-2
@@ -18,8 +18,7 @@
|
|||||||
</gui>
|
</gui>
|
||||||
<options>
|
<options>
|
||||||
<listenAddress>tcp://127.0.0.1:22004</listenAddress>
|
<listenAddress>tcp://127.0.0.1:22004</listenAddress>
|
||||||
<globalAnnounceServer>udp4://announce.syncthing.net:22027</globalAnnounceServer>
|
<globalAnnounceServer>default</globalAnnounceServer>
|
||||||
<globalAnnounceServer>udp6://announce-v6.syncthing.net:22027</globalAnnounceServer>
|
|
||||||
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
||||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
<localAnnounceEnabled>false</localAnnounceEnabled>
|
||||||
<localAnnouncePort>21025</localAnnouncePort>
|
<localAnnouncePort>21025</localAnnouncePort>
|
||||||
|
|||||||
Reference in new issue
Block a user