lib/connections: Refactor

1. Removes separate relay lists and relay clients/services, just makes it a listen address
2. Easier plugging-in of other transports
3. Allows "hot" disabling and enabling NAT services
4. Allows "hot" listen address changes
5. Changes listen address list with a preferable "default" value just like for discovery
6. Debounces global discovery announcements as external addresses change (which it might alot upon starting)
7. Stops this whole "pick other peers relay by latency". This information is no longer available,
   but I don't think it matters as most of the time other peer only has one relay.
8. Rename ListenAddress to ListenAddresses, as well as in javascript land.
9. Stop serializing deprecated values to JSON

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2982
This commit is contained in:
Audrius Butkevicius 2016-05-04 19:38:12 +00:00 committed by Jakob Borg
parent 09832abe50
commit 674fc566bb
45 changed files with 1683 additions and 1707 deletions

View File

@ -50,8 +50,7 @@ func main() {
type checkResult struct { type checkResult struct {
server string server string
direct []string addresses []string
relays []discover.Relay
error error
} }
@ -76,17 +75,14 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
if res.error != nil { if res.error != nil {
fmt.Println(" " + res.error.Error()) fmt.Println(" " + res.error.Error())
} }
for _, addr := range res.direct { for _, addr := range res.addresses {
fmt.Println(" address:", addr) 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 { func checkServer(deviceID protocol.DeviceID, server string) checkResult {
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, nil) disco, err := discover.NewGlobal(server, tls.Certificate{}, nil)
if err != nil { if err != nil {
return checkResult{error: err} return checkResult{error: err}
} }
@ -98,8 +94,8 @@ func checkServer(deviceID protocol.DeviceID, server string) checkResult {
}) })
go func() { go func() {
direct, relays, err := disco.Lookup(deviceID) addresses, err := disco.Lookup(deviceID)
res <- checkResult{direct: direct, relays: relays, error: err} res <- checkResult{addresses: addresses, error: err}
}() }()
return <-res return <-res

View File

@ -35,7 +35,6 @@ import (
"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/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/stats" "github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/tlsutil"
@ -60,7 +59,7 @@ type apiService struct {
model modelIntf model modelIntf
eventSub events.BufferedSubscription eventSub events.BufferedSubscription
discoverer discover.CachingMux discoverer discover.CachingMux
relayService relay.Service connectionsService connectionsIntf
fss *folderSummaryService fss *folderSummaryService
systemConfigMut sync.Mutex // serializes posts to /rest/system/config systemConfigMut sync.Mutex // serializes posts to /rest/system/config
stop chan struct{} // signals intentional stop stop chan struct{} // signals intentional stop
@ -113,9 +112,14 @@ type configIntf interface {
Folders() map[string]config.FolderConfiguration Folders() map[string]config.FolderConfiguration
Devices() map[protocol.DeviceID]config.DeviceConfiguration Devices() map[protocol.DeviceID]config.DeviceConfiguration
Save() error Save() error
ListenAddresses() []string
} }
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder) (*apiService, error) { type connectionsIntf interface {
Status() map[string]interface{}
}
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) (*apiService, error) {
service := &apiService{ service := &apiService{
id: id, id: id,
cfg: cfg, cfg: cfg,
@ -125,7 +129,7 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
model: m, model: m,
eventSub: eventSub, eventSub: eventSub,
discoverer: discoverer, discoverer: discoverer,
relayService: relayService, connectionsService: connectionsService,
systemConfigMut: sync.NewMutex(), systemConfigMut: sync.NewMutex(),
stop: make(chan struct{}), stop: make(chan struct{}),
configChanged: make(chan struct{}), configChanged: make(chan struct{}),
@ -825,18 +829,9 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["discoveryMethods"] = discoMethods res["discoveryMethods"] = discoMethods
res["discoveryErrors"] = discoErrors res["discoveryErrors"] = discoErrors
} }
if s.relayService != nil {
res["relaysEnabled"] = true res["connectionServiceStatus"] = s.connectionsService.Status()
relayClientStatus := make(map[string]bool)
relayClientLatency := make(map[string]int)
for _, relay := range s.relayService.Relays() {
latency, ok := s.relayService.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
for _, p := range cpuUsagePercent { for _, p := range cpuUsagePercent {

View File

@ -462,13 +462,13 @@ func startHTTP(cfg *mockedConfig) (string, error) {
assetDir := "../../gui" assetDir := "../../gui"
eventSub := new(mockedEventSub) eventSub := new(mockedEventSub)
discoverer := new(mockedCachingMux) discoverer := new(mockedCachingMux)
relayService := new(mockedRelayService) connections := new(mockedConnections)
errorLog := new(mockedLoggerRecorder) errorLog := new(mockedLoggerRecorder)
systemLog := new(mockedLoggerRecorder) systemLog := new(mockedLoggerRecorder)
// Instantiate the API service // Instantiate the API service
svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model, svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
eventSub, discoverer, relayService, errorLog, systemLog) eventSub, discoverer, connections, errorLog, systemLog)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -17,7 +17,6 @@ import (
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"net/url"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@ -38,19 +37,13 @@ import (
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/logger" "github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/symlinks" "github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util" "github.com/syncthing/syncthing/lib/util"
// Registers NAT service providers
_ "github.com/syncthing/syncthing/lib/pmp"
_ "github.com/syncthing/syncthing/lib/upnp"
"github.com/thejerf/suture" "github.com/thejerf/suture"
) )
@ -696,40 +689,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
mainService.Add(m) mainService.Add(m)
// Start NAT service
var natService *nat.Service
var mappings []*nat.Mapping
if opts.NATEnabled {
natService = nat.NewService(myID, cfg)
for _, addrStr := range opts.ListenAddress {
uri, err := url.Parse(addrStr)
if err != nil {
l.Fatalf("Failed to parse listen address %s: %v", addrStr, err)
}
if uri.Scheme == "tcp" || uri.Scheme == "tcp4" {
addr, err := net.ResolveTCPAddr(uri.Scheme, uri.Host)
if err != nil {
l.Fatalln("Bad listen address:", err)
}
if addr.Port == 0 {
l.Fatalf("Listen address %s: invalid port", uri)
}
mappings = append(mappings, natService.NewMapping(nat.TCP, addr.IP, addr.Port))
}
}
mainService.Add(natService)
}
// Start relay management
var relayService relay.Service
if opts.RelaysEnabled {
relayService = relay.NewService(cfg, tlsCfg)
mainService.Add(relayService)
}
// Start discovery // Start discovery
cachedDiscovery := discover.NewCachingMux() cachedDiscovery := discover.NewCachingMux()
@ -737,13 +696,13 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Start connection management // Start connection management
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, mappings, relayService, bepProtocolName, tlsDefaultCommonName, lans) connectionsService := connections.NewService(cfg, myID, m, tlsCfg, cachedDiscovery, bepProtocolName, tlsDefaultCommonName, lans)
mainService.Add(connectionService) mainService.Add(connectionsService)
if cfg.Options().GlobalAnnEnabled { if cfg.Options().GlobalAnnEnabled {
for _, srv := range cfg.GlobalDiscoveryServers() { for _, srv := range cfg.GlobalDiscoveryServers() {
l.Infoln("Using discovery server", srv) l.Infoln("Using discovery server", srv)
gd, err := discover.NewGlobal(srv, cert, connectionService, relayService) gd, err := discover.NewGlobal(srv, cert, connectionsService)
if err != nil { if err != nil {
l.Warnln("Global discovery:", err) l.Warnln("Global discovery:", err)
continue continue
@ -758,14 +717,14 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
if cfg.Options().LocalAnnEnabled { if cfg.Options().LocalAnnEnabled {
// v4 broadcasts // v4 broadcasts
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), connectionService, relayService) bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), connectionsService)
if err != nil { if err != nil {
l.Warnln("IPv4 local discovery:", err) l.Warnln("IPv4 local discovery:", err)
} else { } else {
cachedDiscovery.Add(bcd, 0, 0, ipv4LocalDiscoveryPriority) cachedDiscovery.Add(bcd, 0, 0, ipv4LocalDiscoveryPriority)
} }
// v6 multicasts // v6 multicasts
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, connectionService, relayService) mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, connectionsService)
if err != nil { if err != nil {
l.Warnln("IPv6 local discovery:", err) l.Warnln("IPv6 local discovery:", err)
} else { } else {
@ -775,7 +734,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// GUI // GUI
setupGUI(mainService, cfg, m, apiSub, cachedDiscovery, relayService, errors, systemLog, runtimeOptions) setupGUI(mainService, cfg, m, apiSub, cachedDiscovery, connectionsService, errors, systemLog, runtimeOptions)
if runtimeOptions.cpuProfile { if runtimeOptions.cpuProfile {
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid())) f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
@ -957,7 +916,7 @@ func startAuditing(mainService *suture.Supervisor) {
l.Infoln("Audit log in", auditFile) l.Infoln("Audit log in", auditFile)
} }
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) { func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService *connections.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
guiCfg := cfg.GUI() guiCfg := cfg.GUI()
if !guiCfg.Enabled { if !guiCfg.Enabled {
@ -968,7 +927,7 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
l.Warnln("Insecure admin access is enabled.") l.Warnln("Insecure admin access is enabled.")
} }
api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, relayService, errors, systemLog) api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
if err != nil { if err != nil {
l.Fatalln("Cannot start GUI:", err) l.Fatalln("Cannot start GUI:", err)
} }
@ -1017,7 +976,15 @@ func defaultConfig(myName string) config.Configuration {
if err != nil { if err != nil {
l.Fatalln("get free port (BEP):", err) l.Fatalln("get free port (BEP):", err)
} }
newCfg.Options.ListenAddress = []string{fmt.Sprintf("tcp://0.0.0.0:%d", port)} if port == 22000 {
newCfg.Options.ListenAddresses = []string{"default"}
} else {
newCfg.Options.ListenAddresses = []string{
fmt.Sprintf("tcp://%s", net.JoinHostPort("0.0.0.0", strconv.Itoa(port))),
"dynamic+https://relays.syncthing.net/endpoint",
}
}
return newCfg return newCfg
} }

View File

@ -19,6 +19,10 @@ func (c *mockedConfig) GUI() config.GUIConfiguration {
return c.gui return c.gui
} }
func (c *mockedConfig) ListenAddresses() []string {
return nil
}
func (c *mockedConfig) Raw() config.Configuration { func (c *mockedConfig) Raw() config.Configuration {
return config.Configuration{} return config.Configuration{}
} }

View File

@ -0,0 +1,13 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package main
type mockedConnections struct{}
func (m *mockedConnections) Status() map[string]interface{} {
return nil
}

View File

@ -26,8 +26,8 @@ func (m *mockedCachingMux) Stop() {
// from events.Finder // from events.Finder
func (m *mockedCachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []discover.Relay, err error) { func (m *mockedCachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, err error) {
return nil, nil, nil return nil, nil
} }
func (m *mockedCachingMux) Error() error { func (m *mockedCachingMux) Error() error {

View File

@ -1,37 +0,0 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"crypto/tls"
"time"
)
type mockedRelayService struct{}
// from suture.Service
func (s *mockedRelayService) Serve() {
select {}
}
func (s *mockedRelayService) Stop() {
}
// from relay.Service
func (s *mockedRelayService) Accept() *tls.Conn {
return nil
}
func (s *mockedRelayService) Relays() []string {
return nil
}
func (s *mockedRelayService) RelayStatus(uri string) (time.Duration, bool) {
return 0, false
}

View File

@ -16,6 +16,7 @@ import (
"net/http" "net/http"
"runtime" "runtime"
"sort" "sort"
"strings"
"time" "time"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
@ -203,16 +204,16 @@ func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
} }
defaultRelayServers, otherRelayServers := 0, 0 defaultRelayServers, otherRelayServers := 0, 0
for _, addr := range cfg.Options().RelayServers { for _, addr := range cfg.ListenAddresses() {
switch addr { switch {
case "dynamic+https://relays.syncthing.net/endpoint": case addr == "dynamic+https://relays.syncthing.net/endpoint":
defaultRelayServers++ defaultRelayServers++
default: case strings.HasPrefix(addr, "relay://") || strings.HasPrefix(addr, "dynamic+http"):
otherRelayServers++ otherRelayServers++
} }
} }
res["relays"] = map[string]interface{}{ res["relays"] = map[string]interface{}{
"enabled": cfg.Options().RelaysEnabled, "enabled": defaultRelayServers+otherAnnounceServers > 0,
"defaultServers": defaultRelayServers, "defaultServers": defaultRelayServers,
"otherServers": otherRelayServers, "otherServers": otherRelayServers,
} }

View File

@ -8,7 +8,6 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
) )
@ -147,17 +146,12 @@ func (s *verboseService) 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: case events.ListenAddressesChanged:
data := ev.Data.(map[string]interface{}) data := ev.Data.(map[string]interface{})
protocol := data["protocol"] address := data["address"]
local := data["local"] lan := data["lan"]
added := data["added"] wan := data["wan"]
removed := data["removed"] return fmt.Sprintf("Listen address %s resolution has changed: lan addresses: %s wan addresses: %s", address, lan, wan)
return fmt.Sprintf("External port mapping changed; protocol: %s, local: %s, added: %s, removed: %s", protocol, local, added, removed)
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, ", "))
case events.LoginAttempt: case events.LoginAttempt:
data := ev.Data.(map[string]interface{}) data := ev.Data.(map[string]interface{})
username := data["username"].(string) username := data["username"].(string)

View File

@ -430,6 +430,19 @@
<th><span class="fa fa-fw fa-tachometer"></span>&nbsp;<span translate>CPU Utilization</span></th> <th><span class="fa fa-fw fa-tachometer"></span>&nbsp;<span translate>CPU Utilization</span></th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td> <td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr> </tr>
<tr>
<th><span class="fa fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
<td class="text-right">
<span ng-if="listenersFailed.length == 0" class="data text-success">
<span>{{listenersTotal}}/{{listenersTotal}}</span>
</span>
<span ng-if="listenersFailed.length != 0" class="data" ng-class="{'text-danger': listenersFailed.length == listenersTotal}">
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{listenersFailed.join('<br>\n')}}">
{{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
</span>
</span>
</td>
</tr>
<tr ng-if="system.discoveryEnabled"> <tr ng-if="system.discoveryEnabled">
<th><span class="fa fa-fw fa-map-signs"></span>&nbsp;<span translate>Discovery</span></th> <th><span class="fa fa-fw fa-map-signs"></span>&nbsp;<span translate>Discovery</span></th>
<td class="text-right"> <td class="text-right">
@ -443,19 +456,6 @@
</span> </span>
</td> </td>
</tr> </tr>
<tr ng-if="system.relaysEnabled">
<th><span class="fa fa-fw fa-sitemap"></span>&nbsp;<span translate>Relays</span></th>
<td class="text-right">
<span ng-if="relaysFailed.length == 0" class="data text-success">
<span>{{relaysTotal}}/{{relaysTotal}}</span>
</span>
<span ng-if="relaysFailed.length != 0" class="data" ng-class="{'text-danger': relaysFailed.length == relaysTotal}">
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{relaysFailed.join('<br>\n')}}">
{{relaysTotal-relaysFailed.length}}/{{relaysTotal}}
</span>
</span>
</td>
</tr>
<tr> <tr>
<th><span class="fa fa-fw fa-clock-o"></span>&nbsp;<span translate>Uptime</span></th> <th><span class="fa fa-fw fa-clock-o"></span>&nbsp;<span translate>Uptime</span></th>
<td class="text-right">{{system.uptime | duration:"m"}}</td> <td class="text-right">{{system.uptime | duration:"m"}}</td>
@ -503,13 +503,17 @@
<th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th> <th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td> <td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
</tr> </tr>
<tr> <tr ng-if="connections[deviceCfg.deviceID].connected">
<th> <th><span class="fa fa-fw fa-link"></span>&nbsp<span translate>Address</span></th>
<span class="fa fa-fw fa-link"></span> <td class="text-right">
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('direct') == 0" >Address</span> <span tooltip data-original-title="{{ connections[deviceCfg.deviceID].type.indexOf('Relay') > -1 ? '' : connections[deviceCfg.deviceID].type }}">
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span> {{deviceAddr(deviceCfg)}}
</th> </span>
<td class="text-right">{{deviceAddr(deviceCfg)}}</td> </td>
</tr>
<tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay">
<th><span class="fa fa-fw fa-warning text-danger"></span>&nbsp<span translate>Connection Type</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].type}}</td>
</tr> </tr>
<tr ng-if="deviceCfg.compression != 'metadata'"> <tr ng-if="deviceCfg.compression != 'metadata'">
<th><span class="fa fa-fw fa-compress"></span>&nbsp;<span translate>Compression</span></th> <th><span class="fa fa-fw fa-compress"></span>&nbsp;<span translate>Compression</span></th>

View File

@ -352,9 +352,8 @@ angular.module('syncthing.core')
var hasConfig = !isEmptyObject($scope.config); var hasConfig = !isEmptyObject($scope.config);
$scope.config = config; $scope.config = config;
$scope.config.options._listenAddressStr = $scope.config.options.listenAddress.join(', '); $scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', ');
$scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', '); $scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
$scope.config.options._relayServersStr = $scope.config.options.relayServers.join(', ');
$scope.devices = $scope.config.devices; $scope.devices = $scope.config.devices;
$scope.devices.forEach(function (deviceCfg) { $scope.devices.forEach(function (deviceCfg) {
@ -390,6 +389,15 @@ angular.module('syncthing.core')
$scope.myID = data.myID; $scope.myID = data.myID;
$scope.system = data; $scope.system = data;
var listenersFailed = [];
for (var address in data.connectionServiceStatus) {
if (data.connectionServiceStatus[address].error) {
listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
}
}
$scope.listenersFailed = listenersFailed;
$scope.listenersTotal = Object.keys(data.connectionServiceStatus).length;
$scope.discoveryTotal = data.discoveryMethods; $scope.discoveryTotal = data.discoveryMethods;
var discoveryFailed = []; var discoveryFailed = [];
for (var disco in data.discoveryErrors) { for (var disco in data.discoveryErrors) {
@ -398,18 +406,6 @@ angular.module('syncthing.core')
} }
} }
$scope.discoveryFailed = discoveryFailed; $scope.discoveryFailed = discoveryFailed;
var relaysFailed = [];
var relaysTotal = 0;
for (var relay in data.relayClientStatus) {
if (!data.relayClientStatus[relay]) {
relaysFailed.push(relay);
}
relaysTotal++;
}
$scope.relaysFailed = relaysFailed;
$scope.relaysTotal = relaysTotal;
console.log("refreshSystem", data); console.log("refreshSystem", data);
}).error($scope.emitHTTPError); }).error($scope.emitHTTPError);
} }
@ -892,7 +888,7 @@ angular.module('syncthing.core')
$scope.config.options = angular.copy($scope.tmpOptions); $scope.config.options = angular.copy($scope.tmpOptions);
$scope.config.gui = angular.copy($scope.tmpGUI); $scope.config.gui = angular.copy($scope.tmpGUI);
['listenAddress', 'globalAnnounceServers', 'relayServers'].forEach(function (key) { ['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
$scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) { $scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
return x.trim(); return x.trim();
}); });

View File

@ -15,8 +15,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label translate for="ListenAddressStr">Sync Protocol Listen Addresses</label> <label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label>
<input id="ListenAddressStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressStr"> <input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr">
</div> </div>
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}"> <div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
@ -56,20 +56,6 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="RelaysEnabled" type="checkbox" ng-model="tmpOptions.relaysEnabled"> <span translate>Enable Relaying</span>
</label>
</div>
</div>
<div class="clearfix"></div>
<div class="form-group">
<label translate for="RelayServersStr">Relay Servers</label>
<input ng-disabled="!tmpOptions.relaysEnabled" id="RelayServersStr" class="form-control" type="text" ng-model="tmpOptions._relayServersStr">
</div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
<label> <label>

View File

@ -11,6 +11,7 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"io" "io"
"net/url"
"os" "os"
"sort" "sort"
"strings" "strings"
@ -26,21 +27,27 @@ const (
) )
var ( var (
// DefaultDiscoveryServersV4 should be substituted when the configuration // DefaultListenAddresses should be substituted when the configuration
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>. This is // contains <listenAddress>default</listenAddress>. This is
// done by the "consumer" of the configuration, as we don't want these // done by the "consumer" of the configuration, as we don't want these
// saved to the config. // saved to the config.
DefaultListenAddresses = []string{
"tcp://0.0.0.0:22000",
"dynamic+https://relays.syncthing.net/endpoint",
}
// DefaultDiscoveryServersV4 should be substituted when the configuration
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>.
DefaultDiscoveryServersV4 = []string{ DefaultDiscoveryServersV4 = []string{
"https://discovery-v4-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden "https://discovery-v4-1.syncthing.net/v2/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
"https://discovery-v4-2.syncthing.net/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 45.55.230.38, USA "https://discovery-v4-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 45.55.230.38, USA
"https://discovery-v4-3.syncthing.net/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 128.199.95.124, Singapore "https://discovery-v4-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 128.199.95.124, Singapore
} }
// DefaultDiscoveryServersV6 should be substituted when the configuration // DefaultDiscoveryServersV6 should be substituted when the configuration
// contains <globalAnnounceServer>default-v6</globalAnnounceServer>. // contains <globalAnnounceServer>default-v6</globalAnnounceServer>.
DefaultDiscoveryServersV6 = []string{ DefaultDiscoveryServersV6 = []string{
"https://discovery-v6-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden "https://discovery-v6-1.syncthing.net/v2/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
"https://discovery-v6-2.syncthing.net/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 2604:a880:800:10::182:a001, USA "https://discovery-v6-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 2604:a880:800:10::182:a001, USA
"https://discovery-v6-3.syncthing.net/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 2400:6180:0:d0::d9:d001, Singapore "https://discovery-v6-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 2400:6180:0:d0::d9:d001, Singapore
} }
// DefaultDiscoveryServers should be substituted when the configuration // DefaultDiscoveryServers should be substituted when the configuration
// contains <globalAnnounceServer>default</globalAnnounceServer>. // contains <globalAnnounceServer>default</globalAnnounceServer>.
@ -168,7 +175,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
} }
} }
cfg.Options.ListenAddress = util.UniqueStrings(cfg.Options.ListenAddress) cfg.Options.ListenAddresses = util.UniqueStrings(cfg.Options.ListenAddresses)
cfg.Options.GlobalAnnServers = util.UniqueStrings(cfg.Options.GlobalAnnServers) cfg.Options.GlobalAnnServers = util.UniqueStrings(cfg.Options.GlobalAnnServers)
if cfg.Version > 0 && cfg.Version < OldestHandledVersion { if cfg.Version > 0 && cfg.Version < OldestHandledVersion {
@ -246,6 +253,38 @@ func convertV12V13(cfg *Configuration) {
cfg.Options.NATLeaseM = cfg.Options.DeprecatedUPnPLeaseM cfg.Options.NATLeaseM = cfg.Options.DeprecatedUPnPLeaseM
cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM
cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS
if cfg.Options.DeprecatedRelaysEnabled {
cfg.Options.ListenAddresses = append(cfg.Options.ListenAddresses, cfg.Options.DeprecatedRelayServers...)
// Replace our two fairly long addresses with 'default' if both exist.
var newAddresses []string
for _, addr := range cfg.Options.ListenAddresses {
if addr != "tcp://0.0.0.0:22000" && addr != "dynamic+https://relays.syncthing.net/endpoint" {
newAddresses = append(newAddresses, addr)
}
}
if len(newAddresses)+2 == len(cfg.Options.ListenAddresses) {
cfg.Options.ListenAddresses = append([]string{"default"}, newAddresses...)
}
}
cfg.Options.DeprecatedRelaysEnabled = false
cfg.Options.DeprecatedRelayServers = nil
var newAddrs []string
for _, addr := range cfg.Options.GlobalAnnServers {
if addr != "default" {
uri, err := url.Parse(addr)
if err != nil {
panic(err)
}
uri.Path += "v2/"
addr = uri.String()
}
newAddrs = append(newAddrs, addr)
}
cfg.Options.GlobalAnnServers = newAddrs
cfg.Version = 13 cfg.Version = 13
for i, fcfg := range cfg.Folders { for i, fcfg := range cfg.Folders {
@ -260,9 +299,9 @@ func convertV12V13(cfg *Configuration) {
func convertV11V12(cfg *Configuration) { func convertV11V12(cfg *Configuration) {
// Change listen address schema // Change listen address schema
for i, addr := range cfg.Options.ListenAddress { for i, addr := range cfg.Options.ListenAddresses {
if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") { if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") {
cfg.Options.ListenAddress[i] = util.Address("tcp", addr) cfg.Options.ListenAddresses[i] = util.Address("tcp", addr)
} }
} }

View File

@ -31,17 +31,15 @@ func init() {
func TestDefaultValues(t *testing.T) { func TestDefaultValues(t *testing.T) {
expected := OptionsConfiguration{ expected := OptionsConfiguration{
ListenAddress: []string{"tcp://0.0.0.0:22000"}, ListenAddresses: []string{"default"},
GlobalAnnServers: []string{"default"}, GlobalAnnServers: []string{"default"},
GlobalAnnEnabled: true, GlobalAnnEnabled: true,
LocalAnnEnabled: true, LocalAnnEnabled: true,
LocalAnnPort: 21027, LocalAnnPort: 21027,
LocalAnnMCAddr: "[ff12::8384]:21027", LocalAnnMCAddr: "[ff12::8384]:21027",
RelayServers: []string{"dynamic+https://relays.syncthing.net/endpoint"},
MaxSendKbps: 0, MaxSendKbps: 0,
MaxRecvKbps: 0, MaxRecvKbps: 0,
ReconnectIntervalS: 60, ReconnectIntervalS: 60,
RelaysEnabled: true,
RelayReconnectIntervalM: 10, RelayReconnectIntervalM: 10,
StartBrowser: true, StartBrowser: true,
NATEnabled: true, NATEnabled: true,
@ -147,32 +145,30 @@ func TestDeviceConfig(t *testing.T) {
} }
} }
func TestNoListenAddress(t *testing.T) { func TestNoListenAddresses(t *testing.T) {
cfg, err := Load("testdata/nolistenaddress.xml", device1) cfg, err := Load("testdata/nolistenaddress.xml", device1)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
expected := []string{""} expected := []string{""}
actual := cfg.Options().ListenAddress actual := cfg.Options().ListenAddresses
if diff, equal := messagediff.PrettyDiff(expected, actual); !equal { if diff, equal := messagediff.PrettyDiff(expected, actual); !equal {
t.Errorf("Unexpected ListenAddress. Diff:\n%s", diff) t.Errorf("Unexpected ListenAddresses. Diff:\n%s", diff)
} }
} }
func TestOverriddenValues(t *testing.T) { func TestOverriddenValues(t *testing.T) {
expected := OptionsConfiguration{ expected := OptionsConfiguration{
ListenAddress: []string{"tcp://:23000"}, ListenAddresses: []string{"tcp://:23000"},
GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"}, GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"},
GlobalAnnEnabled: false, GlobalAnnEnabled: false,
LocalAnnEnabled: false, LocalAnnEnabled: false,
LocalAnnPort: 42123, LocalAnnPort: 42123,
LocalAnnMCAddr: "quux:3232", LocalAnnMCAddr: "quux:3232",
RelayServers: []string{"relay://123.123.123.123:1234", "relay://125.125.125.125:1255"},
MaxSendKbps: 1234, MaxSendKbps: 1234,
MaxRecvKbps: 2341, MaxRecvKbps: 2341,
ReconnectIntervalS: 6000, ReconnectIntervalS: 6000,
RelaysEnabled: false,
RelayReconnectIntervalM: 20, RelayReconnectIntervalM: 20,
StartBrowser: false, StartBrowser: false,
NATEnabled: false, NATEnabled: false,
@ -357,20 +353,20 @@ func TestIssue1750(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if cfg.Options().ListenAddress[0] != "tcp://:23000" { if cfg.Options().ListenAddresses[0] != "tcp://:23000" {
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], "tcp://:23000") t.Errorf("%q != %q", cfg.Options().ListenAddresses[0], "tcp://:23000")
} }
if cfg.Options().ListenAddress[1] != "tcp://:23001" { if cfg.Options().ListenAddresses[1] != "tcp://:23001" {
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], "tcp://:23001") t.Errorf("%q != %q", cfg.Options().ListenAddresses[1], "tcp://:23001")
} }
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" { if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026/v2/" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[0], "udp4://syncthing.nym.se:22026") t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[0], "udp4://syncthing.nym.se:22026/v2/")
} }
if cfg.Options().GlobalAnnServers[1] != "udp4://syncthing.nym.se:22027" { if cfg.Options().GlobalAnnServers[1] != "udp4://syncthing.nym.se:22027/v2/" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[1], "udp4://syncthing.nym.se:22027") t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[1], "udp4://syncthing.nym.se:22027/v2/")
} }
} }
@ -461,13 +457,13 @@ func TestNewSaveLoad(t *testing.T) {
func TestPrepare(t *testing.T) { func TestPrepare(t *testing.T) {
var cfg Configuration var cfg Configuration
if cfg.Folders != nil || cfg.Devices != nil || cfg.Options.ListenAddress != nil { if cfg.Folders != nil || cfg.Devices != nil || cfg.Options.ListenAddresses != nil {
t.Error("Expected nil") t.Error("Expected nil")
} }
cfg.prepare(device1) cfg.prepare(device1)
if cfg.Folders == nil || cfg.Devices == nil || cfg.Options.ListenAddress == nil { if cfg.Folders == nil || cfg.Devices == nil || cfg.Options.ListenAddresses == nil {
t.Error("Unexpected nil") t.Error("Unexpected nil")
} }
} }
@ -488,7 +484,7 @@ func TestCopy(t *testing.T) {
cfg.Devices[0].Addresses[0] = "wrong" cfg.Devices[0].Addresses[0] = "wrong"
cfg.Folders[0].Devices[0].DeviceID = protocol.DeviceID{0, 1, 2, 3} cfg.Folders[0].Devices[0].DeviceID = protocol.DeviceID{0, 1, 2, 3}
cfg.Options.ListenAddress[0] = "wrong" cfg.Options.ListenAddresses[0] = "wrong"
cfg.GUI.APIKey = "wrong" cfg.GUI.APIKey = "wrong"
bsChanged, err := json.MarshalIndent(cfg, "", " ") bsChanged, err := json.MarshalIndent(cfg, "", " ")

View File

@ -7,17 +7,15 @@
package config package config
type OptionsConfiguration struct { type OptionsConfiguration struct {
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"` ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"` 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"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"` LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net/endpoint"`
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"` MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"` MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"` ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"` RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"` StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"` NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"`
@ -42,20 +40,20 @@ type OptionsConfiguration struct {
OverwriteNames bool `xml:"overwriteNames" json:"overwriteNames" default:"false"` OverwriteNames bool `xml:"overwriteNames" json:"overwriteNames" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"` TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled"` DeprecatedUPnPEnabled bool `xml:"upnpEnabled" json:"-"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes"` DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes" json:"-"`
DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes"` DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes" json:"-"`
DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds"` DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"-"`
DeprecatedRelaysEnabled bool `xml:"relaysEnabled" json:"-"`
DeprecatedRelayServers []string `xml:"relayServer" json:"-"`
} }
func (orig OptionsConfiguration) Copy() OptionsConfiguration { func (orig OptionsConfiguration) Copy() OptionsConfiguration {
c := orig c := orig
c.ListenAddress = make([]string, len(orig.ListenAddress)) c.ListenAddresses = make([]string, len(orig.ListenAddresses))
copy(c.ListenAddress, orig.ListenAddress) copy(c.ListenAddresses, orig.ListenAddresses)
c.GlobalAnnServers = make([]string, len(orig.GlobalAnnServers)) c.GlobalAnnServers = make([]string, len(orig.GlobalAnnServers))
copy(c.GlobalAnnServers, orig.GlobalAnnServers) copy(c.GlobalAnnServers, orig.GlobalAnnServers)
c.RelayServers = make([]string, len(orig.RelayServers))
copy(c.RelayServers, orig.RelayServers)
c.AlwaysLocalNets = make([]string, len(orig.AlwaysLocalNets)) c.AlwaysLocalNets = make([]string, len(orig.AlwaysLocalNets))
copy(c.AlwaysLocalNets, orig.AlwaysLocalNets) copy(c.AlwaysLocalNets, orig.AlwaysLocalNets)
return c return c

View File

@ -7,8 +7,6 @@
<localAnnounceEnabled>false</localAnnounceEnabled> <localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>42123</localAnnouncePort> <localAnnouncePort>42123</localAnnouncePort>
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr> <localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
<relayServer>relay://123.123.123.123:1234</relayServer>
<relayServer>relay://125.125.125.125:1255</relayServer>
<parallelRequests>32</parallelRequests> <parallelRequests>32</parallelRequests>
<maxSendKbps>1234</maxSendKbps> <maxSendKbps>1234</maxSendKbps>
<maxRecvKbps>2341</maxRecvKbps> <maxRecvKbps>2341</maxRecvKbps>

View File

@ -319,3 +319,16 @@ func (w *Wrapper) GlobalDiscoveryServers() []string {
} }
return util.UniqueStrings(servers) return util.UniqueStrings(servers)
} }
func (w *Wrapper) ListenAddresses() []string {
var addresses []string
for _, addr := range w.cfg.Options.ListenAddresses {
switch addr {
case "default":
addresses = append(addresses, DefaultListenAddresses...)
default:
addresses = append(addresses, addr)
}
}
return util.UniqueStrings(addresses)
}

View File

@ -1,638 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"sync"
"time"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
)
type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error)
type ListenerFactory func(*url.URL, *tls.Config, chan<- model.IntermediateConnection)
var (
dialers = make(map[string]DialerFactory, 0)
listeners = make(map[string]ListenerFactory, 0)
)
type Model interface {
protocol.Model
AddConnection(conn model.Connection, hello protocol.HelloMessage)
ConnectedTo(remoteID protocol.DeviceID) bool
IsPaused(remoteID protocol.DeviceID) bool
OnHello(protocol.DeviceID, net.Addr, protocol.HelloMessage)
GetHello(protocol.DeviceID) protocol.HelloMessage
}
// Service listens on TLS and dials configured unconnected devices. Successful
// connections are handed to the model.
type Service struct {
*suture.Supervisor
cfg *config.Wrapper
myID protocol.DeviceID
model Model
tlsCfg *tls.Config
discoverer discover.Finder
conns chan model.IntermediateConnection
mappings []*nat.Mapping
relayService relay.Service
bepProtocolName string
tlsDefaultCommonName string
lans []*net.IPNet
writeRateLimit *ratelimit.Bucket
readRateLimit *ratelimit.Bucket
mut sync.RWMutex
connType map[protocol.DeviceID]model.ConnectionType
}
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, mappings []*nat.Mapping,
relayService relay.Service, bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
service := &Service{
Supervisor: suture.NewSimple("connections.Service"),
cfg: cfg,
myID: myID,
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
mappings: mappings,
relayService: relayService,
conns: make(chan model.IntermediateConnection),
bepProtocolName: bepProtocolName,
tlsDefaultCommonName: tlsDefaultCommonName,
lans: lans,
connType: make(map[protocol.DeviceID]model.ConnectionType),
}
cfg.Subscribe(service)
// The rate variables are in KiB/s in the UI (despite the camel casing
// of the name). We multiply by 1024 here to get B/s.
if service.cfg.Options().MaxSendKbps > 0 {
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxSendKbps), int64(5*1024*service.cfg.Options().MaxSendKbps))
}
if service.cfg.Options().MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxRecvKbps), int64(5*1024*service.cfg.Options().MaxRecvKbps))
}
// There are several moving parts here; one routine per listening address
// to handle incoming connections, one routine to periodically attempt
// outgoing connections, one routine to the the common handling
// regardless of whether the connection was incoming or outgoing.
// Furthermore, a relay service which handles incoming requests to connect
// via the relays.
//
// TODO: Clean shutdown, and/or handling config changes on the fly. We
// partly do this now - new devices and addresses will be picked up, but
// not new listen addresses and we don't support disconnecting devices
// that are removed and so on...
service.Add(serviceFunc(service.connect))
for _, addr := range service.cfg.Options().ListenAddress {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse listen address:", addr, err)
continue
}
listener, ok := listeners[uri.Scheme]
if !ok {
l.Infoln("Unknown listen address scheme:", uri.String())
continue
}
l.Debugln("listening on", uri)
service.Add(serviceFunc(func() {
listener(uri, service.tlsCfg, service.conns)
}))
}
service.Add(serviceFunc(service.handle))
if service.relayService != nil {
service.Add(serviceFunc(service.acceptRelayConns))
}
for _, mapping := range mappings {
mapping.OnChanged(func(m *nat.Mapping, added, removed []nat.Address) {
events.Default.Log(events.ExternalPortMappingChanged, map[string]interface{}{
"protocol": m.Protocol(),
"local": m.Address().String(),
"added": added,
"removed": removed,
})
})
}
return service
}
func (s *Service) handle() {
next:
for c := range s.conns {
cs := c.ConnectionState()
// We should have negotiated the next level protocol "bep/1.0" as part
// of the TLS handshake. Unfortunately this can't be a hard error,
// because there are implementations out there that don't support
// protocol negotiation (iOS for one...).
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != s.bepProtocolName {
l.Infof("Peer %s did not negotiate bep/1.0", c.RemoteAddr())
}
// We should have received exactly one certificate from the other
// side. If we didn't, they don't have a device ID and we drop the
// connection.
certs := cs.PeerCertificates
if cl := len(certs); cl != 1 {
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.RemoteAddr())
c.Close()
continue
}
remoteCert := certs[0]
remoteID := protocol.NewDeviceID(remoteCert.Raw)
// The device ID should not be that of ourselves. It can happen
// though, especially in the presence of NAT hairpinning, multiple
// clients between the same NAT gateway, and global discovery.
if remoteID == s.myID {
l.Infof("Connected to myself (%s) - should not happen", remoteID)
c.Close()
continue
}
hello, err := exchangeHello(c, s.model.GetHello(remoteID))
if err != nil {
l.Infof("Failed to exchange Hello messages with %s (%s): %s", remoteID, c.RemoteAddr(), err)
c.Close()
continue next
}
s.model.OnHello(remoteID, c.RemoteAddr(), hello)
// If we have a relay connection, and the new incoming connection is
// not a relay connection, we should drop that, and prefer the this one.
s.mut.RLock()
skip := false
ct, ok := s.connType[remoteID]
if ok && !ct.IsDirect() && c.Type.IsDirect() {
l.Debugln("Switching connections", remoteID)
s.model.Close(remoteID, protocol.ErrSwitchingConnections)
} else if s.model.ConnectedTo(remoteID) {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
l.Infof("Connected to already connected device (%s)", remoteID)
c.Close()
skip = true
} else if s.model.IsPaused(remoteID) {
l.Infof("Connection from paused device (%s)", remoteID)
c.Close()
skip = true
}
s.mut.RUnlock()
if skip {
continue
}
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == remoteID {
// Verify the name on the certificate. By default we set it to
// "syncthing" when generating, but the user may have replaced
// the certificate and used another name.
certName := deviceCfg.CertName
if certName == "" {
certName = s.tlsDefaultCommonName
}
err := remoteCert.VerifyHostname(certName)
if err != nil {
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.RemoteAddr(), err)
c.Close()
continue next
}
// If rate limiting is set, and based on the address we should
// limit the connection, then we wrap it in a limiter.
limit := s.shouldLimit(c.RemoteAddr())
wr := io.Writer(c.Conn)
if limit && s.writeRateLimit != nil {
wr = NewWriteLimiter(c.Conn, s.writeRateLimit)
}
rd := io.Reader(c.Conn)
if limit && s.readRateLimit != nil {
rd = NewReadLimiter(c.Conn, s.readRateLimit)
}
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
l.Infof("Established secure connection to %s at %s", remoteID, name)
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
s.mut.Lock()
s.model.AddConnection(model.Connection{
Conn: c,
Connection: protoConn,
Type: c.Type,
}, hello)
s.connType[remoteID] = c.Type
s.mut.Unlock()
continue next
}
}
l.Infof("Connection from %s (%s) with ignored device ID %s", c.RemoteAddr(), c.Type, remoteID)
c.Close()
}
}
func (s *Service) connect() {
lastRelayCheck := make(map[protocol.DeviceID]time.Time)
delay := time.Second
for {
l.Debugln("Reconnect loop")
relaysEnabled := s.cfg.Options().RelaysEnabled
nextDevice:
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == s.myID {
continue
}
l.Debugln("Reconnect loop for", deviceID)
s.mut.RLock()
paused := s.model.IsPaused(deviceID)
connected := s.model.ConnectedTo(deviceID)
ct, ok := s.connType[deviceID]
s.mut.RUnlock()
if paused {
continue
}
if connected && ok && ct.IsDirect() {
l.Debugln("Already connected to", deviceID, "via", ct.String())
continue
}
addrs, relays := s.resolveAddresses(deviceID, deviceCfg.Addresses)
for _, addr := range addrs {
if conn := s.connectDirect(deviceID, addr); conn != nil {
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
if connected {
s.model.Close(deviceID, protocol.ErrSwitchingConnections)
}
s.conns <- model.IntermediateConnection{
Conn: conn,
Type: model.ConnectionTypeDirectDial,
}
continue nextDevice
}
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
}
// Only connect via relays if not already connected
// Also, do not set lastRelayCheck time if we have no relays,
// as otherwise when we do discover relays, we might have to
// wait up to RelayReconnectIntervalM to connect again.
// Also, do not try relays if we are explicitly told not to.
if connected || len(relays) == 0 || !relaysEnabled {
l.Debugln("Not connecting via relay", connected, len(relays) == 0, !relaysEnabled)
continue nextDevice
}
reconIntv := time.Duration(s.cfg.Options().RelayReconnectIntervalM) * time.Minute
if last, ok := lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv {
l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last)
continue nextDevice
}
l.Debugln("Trying relay connections to", deviceID, relays)
lastRelayCheck[deviceID] = time.Now()
for _, addr := range relays {
if conn := s.connectViaRelay(deviceID, addr); conn != nil {
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
s.conns <- model.IntermediateConnection{
Conn: conn,
Type: model.ConnectionTypeRelayDial,
}
continue nextDevice
}
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
}
}
time.Sleep(delay)
delay *= 2
if maxD := time.Duration(s.cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
delay = maxD
}
}
}
func (s *Service) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
for _, addr := range inAddrs {
if addr == "dynamic" {
if s.discoverer != nil {
if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
addrs = append(addrs, t...)
relays = append(relays, r...)
}
}
} else {
addrs = append(addrs, addr)
}
}
return
}
func (s *Service) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse connection url:", addr, err)
return nil
}
dialer, ok := dialers[uri.Scheme]
if !ok {
l.Infoln("Unknown address schema", uri)
return nil
}
l.Debugln("dial", deviceID, uri)
conn, err := dialer(uri, s.tlsCfg)
if err != nil {
l.Debugln("dial failed", deviceID, uri, err)
return nil
}
return conn
}
func (s *Service) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.Conn {
uri, err := url.Parse(addr.URL)
if err != nil {
l.Infoln("Failed to parse relay connection url:", addr, err)
return nil
}
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates, 10*time.Second)
if err != nil {
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
return nil
}
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
conn, err := client.JoinSession(inv)
if err != nil {
l.Debugf("Failed to join relay session %s: %v", inv, err)
return nil
}
l.Debugln("Successfully joined relay session", inv)
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, s.tlsCfg)
} else {
tc = tls.Client(conn, s.tlsCfg)
}
err = tc.Handshake()
if err != nil {
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
tc.Close()
return nil
}
return tc
}
func (s *Service) acceptRelayConns() {
for {
conn := s.relayService.Accept()
s.conns <- model.IntermediateConnection{
Conn: conn,
Type: model.ConnectionTypeRelayAccept,
}
}
}
func (s *Service) shouldLimit(addr net.Addr) bool {
if s.cfg.Options().LimitBandwidthInLan {
return true
}
tcpaddr, ok := addr.(*net.TCPAddr)
if !ok {
return true
}
for _, lan := range s.lans {
if lan.Contains(tcpaddr.IP) {
return false
}
}
return !tcpaddr.IP.IsLoopback()
}
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
// We require a restart if a device as been removed.
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
for _, dev := range to.Devices {
newDevices[dev.DeviceID] = true
}
for _, dev := range from.Devices {
if !newDevices[dev.DeviceID] {
return false
}
}
return true
}
// ExternalAddresses returns a list of addresses that are our best guess for
// where we are reachable from the outside. As a special case, we may return
// one or more addresses with an empty IP address (0.0.0.0 or ::) and just
// port number - this means that the outside address of a NAT gateway should
// be substituted.
func (s *Service) ExternalAddresses() []string {
return s.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 (s *Service) AllAddresses() []string {
return s.addresses(true)
}
func (s *Service) addresses(includePrivateIPV4 bool) []string {
var addrs []string
// Grab our listen addresses from the config. Unspecified ones are passed
// on verbatim (to be interpreted by a global discovery server or local
// discovery peer). Public addresses are passed on verbatim. Private
// addresses are filtered.
for _, addrStr := range s.cfg.Options().ListenAddress {
addrURL, err := url.Parse(addrStr)
if err != nil {
l.Infoln("Listen address", addrStr, "is invalid:", err)
continue
}
addr, err := net.ResolveTCPAddr(addrURL.Scheme, addrURL.Host)
if err != nil {
l.Infoln("Listen address", addrStr, "is invalid:", err)
continue
}
if addr.IP == nil || addr.IP.IsUnspecified() {
// Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is.
addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
// A public address; include as is.
addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
// A private IPv4 address.
addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
}
}
// Add addresses provided by the mappings from the NAT service.
for _, mapping := range s.mappings {
for _, addr := range mapping.ExternalAddresses() {
addrs = append(addrs, fmt.Sprintf("tcp://%s", addr))
}
}
return addrs
}
func isPublicIPv4(ip net.IP) bool {
ip = ip.To4()
if ip == nil {
// Not an IPv4 address (IPv6)
return false
}
// IsGlobalUnicast below only checks that it's not link local or
// multicast, and we want to exclude private (NAT:ed) addresses as well.
rfc1918 := []net.IPNet{
{IP: net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0}},
{IP: net.IP{172, 16, 0, 0}, Mask: net.IPMask{255, 240, 0, 0}},
{IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 255, 0, 0}},
}
for _, n := range rfc1918 {
if n.Contains(ip) {
return false
}
}
return ip.IsGlobalUnicast()
}
func isPublicIPv6(ip net.IP) bool {
if ip.To4() != nil {
// Not an IPv6 address (IPv4)
// (To16() returns a v6 mapped v4 address so can't be used to check
// that it's an actual v6 address)
return false
}
return ip.IsGlobalUnicast()
}
func exchangeHello(c net.Conn, h protocol.HelloMessage) (protocol.HelloMessage, error) {
if err := c.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
return protocol.HelloMessage{}, err
}
defer c.SetDeadline(time.Time{})
header := make([]byte, 8)
msg := h.MustMarshalXDR()
binary.BigEndian.PutUint32(header[:4], protocol.HelloMessageMagic)
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
if _, err := c.Write(header); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := c.Write(msg); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := io.ReadFull(c, header); err != nil {
return protocol.HelloMessage{}, err
}
if binary.BigEndian.Uint32(header[:4]) != protocol.HelloMessageMagic {
return protocol.HelloMessage{}, fmt.Errorf("incorrect magic")
}
msgSize := binary.BigEndian.Uint32(header[4:])
if msgSize > 1024 {
return protocol.HelloMessage{}, fmt.Errorf("hello message too big")
}
buf := make([]byte, msgSize)
var hello protocol.HelloMessage
if _, err := io.ReadFull(c, buf); err != nil {
return protocol.HelloMessage{}, err
}
if err := hello.UnmarshalXDR(buf); err != nil {
return protocol.HelloMessage{}, err
}
return hello, nil
}
// serviceFunc wraps a function to create a suture.Service without stop
// functionality.
type serviceFunc func()
func (f serviceFunc) Serve() { f() }
func (f serviceFunc) Stop() {}

View File

@ -1,99 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"net"
"net/url"
"strings"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/model"
)
func init() {
for _, network := range []string{"tcp", "tcp4", "tcp6"} {
dialers[network] = makeTCPDialer(network)
listeners[network] = makeTCPListener(network)
}
}
func makeTCPDialer(network string) DialerFactory {
return func(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
// Check that there is a port number in uri.Host, otherwise add one.
host, port, err := net.SplitHostPort(uri.Host)
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
// addr is on the form "1.2.3.4"
uri.Host = net.JoinHostPort(uri.Host, "22000")
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:"
uri.Host = net.JoinHostPort(host, "22000")
}
// Don't try to resolve the address before dialing. The dialer may be a
// proxy, and we should let the proxy do the resolving in that case.
conn, err := dialer.Dial(network, uri.Host)
if err != nil {
l.Debugln(err)
return nil, err
}
tc := tls.Client(conn, tlsCfg)
err = tc.Handshake()
if err != nil {
tc.Close()
return nil, err
}
return tc, nil
}
}
func makeTCPListener(network string) ListenerFactory {
return func(uri *url.URL, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) {
tcaddr, err := net.ResolveTCPAddr(network, uri.Host)
if err != nil {
l.Fatalln("listen (BEP/tcp):", err)
return
}
listener, err := net.ListenTCP(network, tcaddr)
if err != nil {
l.Fatalln("listen (BEP/tcp):", err)
return
}
for {
conn, err := listener.Accept()
if err != nil {
l.Warnln("Accepting connection (BEP/tcp):", err)
continue
}
l.Debugln("connect from", conn.RemoteAddr())
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
tc := tls.Server(conn, tlsCfg)
err = tc.Handshake()
if err != nil {
l.Infoln("TLS handshake (BEP/tcp):", err)
tc.Close()
continue
}
conns <- model.IntermediateConnection{
Conn: tc,
Type: model.ConnectionTypeDirectAccept,
}
}
}
}

View File

@ -0,0 +1,82 @@
// Copyright (C) 2016 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 connections
import (
"crypto/tls"
"net"
"net/url"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay/client"
)
const relayPriority = 200
func init() {
dialers["relay"] = newRelayDialer
}
type relayDialer struct {
cfg *config.Wrapper
tlsCfg *tls.Config
}
func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
inv, err := client.GetInvitationFromRelay(uri, id, d.tlsCfg.Certificates, 10*time.Second)
if err != nil {
return IntermediateConnection{}, err
}
conn, err := client.JoinSession(inv)
if err != nil {
return IntermediateConnection{}, err
}
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
conn.Close()
return IntermediateConnection{}, err
}
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, d.tlsCfg)
} else {
tc = tls.Client(conn, d.tlsCfg)
}
err = tc.Handshake()
if err != nil {
tc.Close()
return IntermediateConnection{}, err
}
return IntermediateConnection{tc, "Relay (Client)", relayPriority}, nil
}
func (relayDialer) Priority() int {
return relayPriority
}
func (d *relayDialer) RedialFrequency() time.Duration {
return time.Duration(d.cfg.Options().RelayReconnectIntervalM) * time.Minute
}
func (d *relayDialer) String() string {
return "Relay Dialer"
}
func newRelayDialer(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
return &relayDialer{
cfg: cfg,
tlsCfg: tlsCfg,
}
}

View File

@ -0,0 +1,167 @@
// Copyright (C) 2016 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 connections
import (
"crypto/tls"
"net"
"net/url"
"sync"
"time"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/relay/client"
)
func init() {
listeners["relay"] = newRelayListener
listeners["dynamic+http"] = newRelayListener
listeners["dynamic+https"] = newRelayListener
}
type relayListener struct {
onAddressesChangedNotifier
uri *url.URL
tlsCfg *tls.Config
conns chan IntermediateConnection
err error
client client.RelayClient
mut sync.RWMutex
}
func (t *relayListener) Serve() {
t.mut.Lock()
t.err = nil
t.mut.Unlock()
clnt, err := client.NewClient(t.uri, t.tlsCfg.Certificates, nil, 10*time.Second)
if err != nil {
t.mut.Lock()
t.err = err
t.mut.Unlock()
l.Warnln("listen (BEP/relay):", err)
return
}
go clnt.Serve()
t.mut.Lock()
t.client = clnt
t.mut.Unlock()
oldURI := clnt.URI()
for {
select {
case inv, ok := <-t.client.Invitations():
if !ok {
return
}
conn, err := client.JoinSession(inv)
if err != nil {
l.Warnln("Joining relay session (BEP/relay):", err)
continue
}
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, t.tlsCfg)
} else {
tc = tls.Client(conn, t.tlsCfg)
}
err = tc.Handshake()
if err != nil {
tc.Close()
l.Infoln("TLS handshake (BEP/relay):", err)
continue
}
t.conns <- IntermediateConnection{tc, "Relay (Server)", relayPriority}
// Poor mans notifier that informs the connection service that the
// relay URI has changed. This can only happen when we connect to a
// relay via dynamic+http(s) pool, which upon a relay failing/dropping
// us, would pick a different one.
case <-time.After(10 * time.Second):
currentURI := clnt.URI()
if currentURI != oldURI {
oldURI = currentURI
t.notifyAddressesChanged(t)
}
}
}
}
func (t *relayListener) Stop() {
t.mut.RLock()
if t.client != nil {
t.client.Stop()
}
t.mut.RUnlock()
}
func (t *relayListener) URI() *url.URL {
return t.uri
}
func (t *relayListener) WANAddresses() []*url.URL {
t.mut.RLock()
client := t.client
t.mut.RUnlock()
if client == nil {
return nil
}
curi := client.URI()
if curi == nil {
return nil
}
return []*url.URL{curi}
}
func (t *relayListener) LANAddresses() []*url.URL {
return t.WANAddresses()
}
func (t *relayListener) Error() error {
t.mut.RLock()
err := t.err
var cerr error
if t.client != nil {
cerr = t.client.Error()
}
t.mut.RUnlock()
if err != nil {
return err
}
return cerr
}
func (t *relayListener) String() string {
return t.uri.String()
}
func newRelayListener(uri *url.URL, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
return &relayListener{
uri: uri,
tlsCfg: tlsCfg,
conns: conns,
}
}

564
lib/connections/service.go Normal file
View File

@ -0,0 +1,564 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"time"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
// Registers NAT service providers
_ "github.com/syncthing/syncthing/lib/pmp"
_ "github.com/syncthing/syncthing/lib/upnp"
"github.com/thejerf/suture"
)
var (
dialers = make(map[string]dialerFactory, 0)
listeners = make(map[string]listenerFactory, 0)
)
// Service listens and dials all configured unconnected devices, via supported
// dialers. Successful connections are handed to the model.
type Service struct {
*suture.Supervisor
cfg *config.Wrapper
myID protocol.DeviceID
model Model
tlsCfg *tls.Config
discoverer discover.Finder
conns chan IntermediateConnection
bepProtocolName string
tlsDefaultCommonName string
lans []*net.IPNet
writeRateLimit *ratelimit.Bucket
readRateLimit *ratelimit.Bucket
natService *nat.Service
natServiceToken *suture.ServiceToken
mut sync.RWMutex
listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken
currentConnection map[protocol.DeviceID]Connection
}
func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
service := &Service{
Supervisor: suture.NewSimple("connections.Service"),
cfg: cfg,
myID: myID,
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
conns: make(chan IntermediateConnection),
bepProtocolName: bepProtocolName,
tlsDefaultCommonName: tlsDefaultCommonName,
lans: lans,
natService: nat.NewService(myID, cfg),
mut: sync.NewRWMutex(),
listeners: make(map[string]genericListener),
listenerTokens: make(map[string]suture.ServiceToken),
currentConnection: make(map[protocol.DeviceID]Connection),
}
cfg.Subscribe(service)
// The rate variables are in KiB/s in the UI (despite the camel casing
// of the name). We multiply by 1024 here to get B/s.
if service.cfg.Options().MaxSendKbps > 0 {
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxSendKbps), int64(5*1024*service.cfg.Options().MaxSendKbps))
}
if service.cfg.Options().MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxRecvKbps), int64(5*1024*service.cfg.Options().MaxRecvKbps))
}
// There are several moving parts here; one routine per listening address
// (handled in configuration changing) to handle incoming connections,
// one routine to periodically attempt outgoing connections, one routine to
// the the common handling regardless of whether the connection was
// incoming or outgoing.
service.Add(serviceFunc(service.connect))
service.Add(serviceFunc(service.handle))
raw := cfg.Raw()
// Actually starts the listeners and NAT service
service.CommitConfiguration(raw, raw)
return service
}
func (s *Service) handle() {
next:
for c := range s.conns {
cs := c.ConnectionState()
// We should have negotiated the next level protocol "bep/1.0" as part
// of the TLS handshake. Unfortunately this can't be a hard error,
// because there are implementations out there that don't support
// protocol negotiation (iOS for one...).
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != s.bepProtocolName {
l.Infof("Peer %s did not negotiate bep/1.0", c.RemoteAddr())
}
// We should have received exactly one certificate from the other
// side. If we didn't, they don't have a device ID and we drop the
// connection.
certs := cs.PeerCertificates
if cl := len(certs); cl != 1 {
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.RemoteAddr())
c.Close()
continue
}
remoteCert := certs[0]
remoteID := protocol.NewDeviceID(remoteCert.Raw)
// The device ID should not be that of ourselves. It can happen
// though, especially in the presence of NAT hairpinning, multiple
// clients between the same NAT gateway, and global discovery.
if remoteID == s.myID {
l.Infof("Connected to myself (%s) - should not happen", remoteID)
c.Close()
continue
}
hello, err := exchangeHello(c, s.model.GetHello(remoteID))
if err != nil {
l.Infof("Failed to exchange Hello messages with %s (%s): %s", remoteID, c.RemoteAddr(), err)
c.Close()
continue
}
s.model.OnHello(remoteID, c.RemoteAddr(), hello)
// If we have a relay connection, and the new incoming connection is
// not a relay connection, we should drop that, and prefer the this one.
s.mut.RLock()
skip := false
ct, ok := s.currentConnection[remoteID]
// Lower priority is better, just like nice etc.
if ok && ct.Priority > c.Priority {
l.Debugln("Switching connections", remoteID)
s.model.Close(remoteID, protocol.ErrSwitchingConnections)
} else if s.model.ConnectedTo(remoteID) {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
l.Infof("Connected to already connected device (%s)", remoteID)
c.Close()
skip = true
} else if s.model.IsPaused(remoteID) {
l.Infof("Connection from paused device (%s)", remoteID)
c.Close()
skip = true
}
s.mut.RUnlock()
if skip {
continue
}
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == remoteID {
// Verify the name on the certificate. By default we set it to
// "syncthing" when generating, but the user may have replaced
// the certificate and used another name.
certName := deviceCfg.CertName
if certName == "" {
certName = s.tlsDefaultCommonName
}
err := remoteCert.VerifyHostname(certName)
if err != nil {
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.RemoteAddr(), err)
c.Close()
continue next
}
// If rate limiting is set, and based on the address we should
// limit the connection, then we wrap it in a limiter.
limit := s.shouldLimit(c.RemoteAddr())
wr := io.Writer(c)
if limit && s.writeRateLimit != nil {
wr = NewWriteLimiter(c, s.writeRateLimit)
}
rd := io.Reader(c)
if limit && s.readRateLimit != nil {
rd = NewReadLimiter(c, s.readRateLimit)
}
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
modelConn := Connection{c, protoConn}
l.Infof("Established secure connection to %s at %s", remoteID, name)
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
s.mut.Lock()
s.model.AddConnection(modelConn, hello)
s.currentConnection[remoteID] = modelConn
s.mut.Unlock()
continue next
}
}
l.Infof("Connection from %s (%s) with ignored device ID %s", c.RemoteAddr(), c.Type, remoteID)
c.Close()
}
}
func (s *Service) connect() {
nextDial := make(map[string]time.Time)
delay := time.Second
sleep := time.Second
for {
l.Debugln("Reconnect loop")
now := time.Now()
var seen []string
nextDevice:
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == s.myID {
continue
}
l.Debugln("Reconnect loop for", deviceID)
s.mut.RLock()
paused := s.model.IsPaused(deviceID)
connected := s.model.ConnectedTo(deviceID)
ct := s.currentConnection[deviceID]
s.mut.RUnlock()
if paused {
continue
}
var addrs []string
for _, addr := range deviceCfg.Addresses {
if addr == "dynamic" {
if s.discoverer != nil {
if t, err := s.discoverer.Lookup(deviceID); err == nil {
addrs = append(addrs, t...)
}
}
} else {
addrs = append(addrs, addr)
}
}
seen = append(seen, addrs...)
for _, addr := range addrs {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse connection url:", addr, err)
continue
}
dialerFactory, ok := dialers[uri.Scheme]
if !ok {
l.Debugln("Unknown address schema", uri)
continue
}
dialer := dialerFactory(s.cfg, s.tlsCfg)
nextDialAt, ok := nextDial[uri.String()]
// See below for comments on this delay >= sleep check
if delay >= sleep && ok && nextDialAt.After(now) {
l.Debugf("Not dialing as next dial is at %s and current time is %s", nextDialAt, now)
continue
}
nextDial[uri.String()] = now.Add(dialer.RedialFrequency())
if connected && dialer.Priority() >= ct.Priority {
l.Debugf("Not dialing using %s as priorty is less than current connection (%d >= %d)", dialer, dialer.Priority(), ct.Priority)
continue
}
l.Debugln("dial", deviceCfg.DeviceID, uri)
conn, err := dialer.Dial(deviceID, uri)
if err != nil {
l.Debugln("dial failed", deviceCfg.DeviceID, uri, err)
continue
}
if connected {
s.model.Close(deviceID, protocol.ErrSwitchingConnections)
}
s.conns <- conn
continue nextDevice
}
}
nextDial, sleep = filterAndFindSleepDuration(nextDial, seen, now)
// delay variable is used to trigger much more frequent dialing after
// initial startup, essentially causing redials every 1, 2, 4, 8... seconds
if delay < sleep {
time.Sleep(delay)
delay *= 2
} else {
time.Sleep(sleep)
}
}
}
func (s *Service) shouldLimit(addr net.Addr) bool {
if s.cfg.Options().LimitBandwidthInLan {
return true
}
tcpaddr, ok := addr.(*net.TCPAddr)
if !ok {
return true
}
for _, lan := range s.lans {
if lan.Contains(tcpaddr.IP) {
return false
}
}
return !tcpaddr.IP.IsLoopback()
}
func (s *Service) createListener(addr string) {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse listen address:", addr, err)
return
}
listenerFactory, ok := listeners[uri.Scheme]
if !ok {
l.Infoln("Unknown listen address scheme:", uri.String())
return
}
listener := listenerFactory(uri, s.tlsCfg, s.conns, s.natService)
listener.OnAddressesChanged(s.logListenAddressesChangedEvent)
s.mut.Lock()
s.listeners[addr] = listener
s.listenerTokens[addr] = s.Add(listener)
s.mut.Unlock()
}
func (s *Service) logListenAddressesChangedEvent(l genericListener) {
events.Default.Log(events.ListenAddressesChanged, map[string]interface{}{
"address": l.URI(),
"lan": l.LANAddresses(),
"wan": l.WANAddresses(),
})
}
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
// We require a restart if a device as been removed.
restart := false
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
for _, dev := range to.Devices {
newDevices[dev.DeviceID] = true
}
for _, dev := range from.Devices {
if !newDevices[dev.DeviceID] {
restart = true
}
}
s.mut.RLock()
existingListeners := s.listeners
s.mut.RUnlock()
seen := make(map[string]struct{})
for _, addr := range config.Wrap("", to).ListenAddresses() {
if _, ok := existingListeners[addr]; !ok {
l.Debugln("Staring listener", addr)
s.createListener(addr)
}
seen[addr] = struct{}{}
}
s.mut.Lock()
for addr := range s.listeners {
if _, ok := seen[addr]; !ok {
l.Debugln("Stopping listener", addr)
s.Remove(s.listenerTokens[addr])
delete(s.listenerTokens, addr)
delete(s.listeners, addr)
}
}
s.mut.Unlock()
if to.Options.NATEnabled && s.natServiceToken == nil {
l.Debugln("Starting NAT service")
token := s.Add(s.natService)
s.natServiceToken = &token
} else if !to.Options.NATEnabled && s.natServiceToken != nil {
l.Debugln("Stopping NAT service")
s.Remove(*s.natServiceToken)
s.natServiceToken = nil
}
return !restart
}
func (s *Service) AllAddresses() []string {
s.mut.RLock()
var addrs []string
for _, listener := range s.listeners {
for _, lanAddr := range listener.LANAddresses() {
addrs = append(addrs, lanAddr.String())
}
for _, wanAddr := range listener.WANAddresses() {
addrs = append(addrs, wanAddr.String())
}
}
s.mut.RUnlock()
return util.UniqueStrings(addrs)
}
func (s *Service) ExternalAddresses() []string {
s.mut.RLock()
var addrs []string
for _, listener := range s.listeners {
for _, wanAddr := range listener.WANAddresses() {
addrs = append(addrs, wanAddr.String())
}
}
s.mut.RUnlock()
return util.UniqueStrings(addrs)
}
func (s *Service) Status() map[string]interface{} {
s.mut.RLock()
result := make(map[string]interface{})
for addr, listener := range s.listeners {
status := make(map[string]interface{})
err := listener.Error()
if err != nil {
status["error"] = err.Error()
}
status["lanAddresses"] = urlsToStrings(listener.LANAddresses())
status["wanAddresses"] = urlsToStrings(listener.WANAddresses())
result[addr] = status
}
s.mut.RUnlock()
return result
}
func exchangeHello(c net.Conn, h protocol.HelloMessage) (protocol.HelloMessage, error) {
if err := c.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
return protocol.HelloMessage{}, err
}
defer c.SetDeadline(time.Time{})
header := make([]byte, 8)
msg := h.MustMarshalXDR()
binary.BigEndian.PutUint32(header[:4], protocol.HelloMessageMagic)
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
if _, err := c.Write(header); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := c.Write(msg); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := io.ReadFull(c, header); err != nil {
return protocol.HelloMessage{}, err
}
if binary.BigEndian.Uint32(header[:4]) != protocol.HelloMessageMagic {
return protocol.HelloMessage{}, fmt.Errorf("incorrect magic")
}
msgSize := binary.BigEndian.Uint32(header[4:])
if msgSize > 1024 {
return protocol.HelloMessage{}, fmt.Errorf("hello message too big")
}
buf := make([]byte, msgSize)
var hello protocol.HelloMessage
if _, err := io.ReadFull(c, buf); err != nil {
return protocol.HelloMessage{}, err
}
if err := hello.UnmarshalXDR(buf); err != nil {
return protocol.HelloMessage{}, err
}
return hello, nil
}
func filterAndFindSleepDuration(nextDial map[string]time.Time, seen []string, now time.Time) (map[string]time.Time, time.Duration) {
newNextDial := make(map[string]time.Time)
for _, addr := range seen {
nextDialAt, ok := nextDial[addr]
if ok {
newNextDial[addr] = nextDialAt
}
}
min := time.Minute
for _, next := range newNextDial {
cur := next.Sub(now)
if cur < min {
min = cur
}
}
return newNextDial, min
}
func urlsToStrings(urls []*url.URL) []string {
strings := make([]string, len(urls))
for i, url := range urls {
strings[i] = url.String()
}
return strings
}

View File

@ -0,0 +1,88 @@
// Copyright (C) 2016 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 connections
import (
"crypto/tls"
"net"
"net/url"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
)
type IntermediateConnection struct {
*tls.Conn
Type string
Priority int
}
type Connection struct {
IntermediateConnection
protocol.Connection
}
type dialerFactory func(*config.Wrapper, *tls.Config) genericDialer
type genericDialer interface {
Dial(protocol.DeviceID, *url.URL) (IntermediateConnection, error)
Priority() int
RedialFrequency() time.Duration
String() string
}
type listenerFactory func(*url.URL, *tls.Config, chan IntermediateConnection, *nat.Service) genericListener
type genericListener interface {
Serve()
Stop()
URI() *url.URL
// A given address can potentially be mutated by the listener.
// For example we bind to tcp://0.0.0.0, but that for example might return
// tcp://gateway1.ip and tcp://gateway2.ip as WAN addresses due to there
// being multiple gateways, and us managing to get a UPnP mapping on both
// and tcp://192.168.0.1 and tcp://10.0.0.1 due to there being multiple
// network interfaces. (The later case for LAN addresses is made up just
// to provide an example)
WANAddresses() []*url.URL
LANAddresses() []*url.URL
Error() error
OnAddressesChanged(func(genericListener))
String() string
}
type Model interface {
protocol.Model
AddConnection(conn Connection, hello protocol.HelloMessage)
ConnectedTo(remoteID protocol.DeviceID) bool
IsPaused(remoteID protocol.DeviceID) bool
OnHello(protocol.DeviceID, net.Addr, protocol.HelloMessage)
GetHello(protocol.DeviceID) protocol.HelloMessage
}
// serviceFunc wraps a function to create a suture.Service without stop
// functionality.
type serviceFunc func()
func (f serviceFunc) Serve() { f() }
func (f serviceFunc) Stop() {}
type onAddressesChangedNotifier struct {
callbacks []func(genericListener)
}
func (o *onAddressesChangedNotifier) OnAddressesChanged(callback func(genericListener)) {
o.callbacks = append(o.callbacks, callback)
}
func (o *onAddressesChangedNotifier) notifyAddressesChanged(l genericListener) {
for _, callback := range o.callbacks {
callback(l)
}
}

View File

@ -0,0 +1,75 @@
// Copyright (C) 2016 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 connections
import (
"crypto/tls"
"net"
"net/url"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/protocol"
)
const tcpPriority = 10
func init() {
for _, scheme := range []string{"tcp", "tcp4", "tcp6"} {
dialers[scheme] = newTCPDialer
}
}
type tcpDialer struct {
cfg *config.Wrapper
tlsCfg *tls.Config
}
func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
uri = fixupPort(uri)
raddr, err := net.ResolveTCPAddr(uri.Scheme, uri.Host)
if err != nil {
l.Debugln(err)
return IntermediateConnection{}, err
}
conn, err := dialer.DialTimeout(raddr.Network(), raddr.String(), 10*time.Second)
if err != nil {
l.Debugln(err)
return IntermediateConnection{}, err
}
tc := tls.Client(conn, d.tlsCfg)
err = tc.Handshake()
if err != nil {
tc.Close()
return IntermediateConnection{}, err
}
return IntermediateConnection{tc, "TCP (Client)", tcpPriority}, nil
}
func (tcpDialer) Priority() int {
return tcpPriority
}
func (d *tcpDialer) RedialFrequency() time.Duration {
return time.Duration(d.cfg.Options().ReconnectIntervalS) * time.Second
}
func (d *tcpDialer) String() string {
return "TCP Dialer"
}
func newTCPDialer(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
return &tcpDialer{
cfg: cfg,
tlsCfg: tlsCfg,
}
}

View File

@ -0,0 +1,213 @@
// Copyright (C) 2016 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 connections
import (
"crypto/tls"
"net"
"net/url"
"strings"
"sync"
"time"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/nat"
)
func init() {
for _, scheme := range []string{"tcp", "tcp4", "tcp6"} {
listeners[scheme] = newTCPListener
}
}
type tcpListener struct {
onAddressesChangedNotifier
uri *url.URL
tlsCfg *tls.Config
stop chan struct{}
conns chan IntermediateConnection
natService *nat.Service
mapping *nat.Mapping
address *url.URL
err error
mut sync.RWMutex
}
func (t *tcpListener) Serve() {
t.mut.Lock()
t.err = nil
t.mut.Unlock()
tcaddr, err := net.ResolveTCPAddr(t.uri.Scheme, t.uri.Host)
if err != nil {
t.mut.Lock()
t.err = err
t.mut.Unlock()
l.Infoln("listen (BEP/tcp):", err)
return
}
listener, err := net.ListenTCP(t.uri.Scheme, tcaddr)
if err != nil {
t.mut.Lock()
t.err = err
t.mut.Unlock()
l.Infoln("listen (BEP/tcp):", err)
return
}
defer listener.Close()
mapping := t.natService.NewMapping(nat.TCP, tcaddr.IP, tcaddr.Port)
mapping.OnChanged(func(_ *nat.Mapping, _, _ []nat.Address) {
t.notifyAddressesChanged(t)
})
defer t.natService.RemoveMapping(mapping)
t.mut.Lock()
t.mapping = mapping
t.mut.Unlock()
for {
listener.SetDeadline(time.Now().Add(time.Second))
conn, err := listener.Accept()
select {
case <-t.stop:
if err == nil {
conn.Close()
}
t.mut.Lock()
t.mapping = nil
t.mut.Unlock()
return
default:
}
if err != nil {
if err, ok := err.(*net.OpError); !ok || !err.Timeout() {
l.Warnln("Accepting connection (BEP/tcp):", err)
}
continue
}
l.Debugln("connect from", conn.RemoteAddr())
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
tc := tls.Server(conn, t.tlsCfg)
err = tc.Handshake()
if err != nil {
l.Infoln("TLS handshake (BEP/tcp):", err)
tc.Close()
continue
}
t.conns <- IntermediateConnection{tc, "TCP (Server)", tcpPriority}
}
}
func (t *tcpListener) Stop() {
close(t.stop)
}
func (t *tcpListener) URI() *url.URL {
return t.uri
}
func (t *tcpListener) WANAddresses() []*url.URL {
uris := t.LANAddresses()
t.mut.RLock()
if t.mapping != nil {
addrs := t.mapping.ExternalAddresses()
for _, addr := range addrs {
uri := *t.uri
// Does net.JoinHostPort internally
uri.Host = addr.String()
uris = append(uris, &uri)
}
}
t.mut.RUnlock()
return uris
}
func (t *tcpListener) LANAddresses() []*url.URL {
return []*url.URL{t.uri}
}
func (t *tcpListener) Error() error {
t.mut.RLock()
err := t.err
t.mut.RUnlock()
return err
}
func (t *tcpListener) String() string {
return t.uri.String()
}
func newTCPListener(uri *url.URL, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
return &tcpListener{
uri: fixupPort(uri),
tlsCfg: tlsCfg,
conns: conns,
natService: natService,
stop: make(chan struct{}),
}
}
func isPublicIPv4(ip net.IP) bool {
ip = ip.To4()
if ip == nil {
// Not an IPv4 address (IPv6)
return false
}
// IsGlobalUnicast below only checks that it's not link local or
// multicast, and we want to exclude private (NAT:ed) addresses as well.
rfc1918 := []net.IPNet{
{IP: net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0}},
{IP: net.IP{172, 16, 0, 0}, Mask: net.IPMask{255, 240, 0, 0}},
{IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 255, 0, 0}},
}
for _, n := range rfc1918 {
if n.Contains(ip) {
return false
}
}
return ip.IsGlobalUnicast()
}
func isPublicIPv6(ip net.IP) bool {
if ip.To4() != nil {
// Not an IPv6 address (IPv4)
// (To16() returns a v6 mapped v4 address so can't be used to check
// that it's an actual v6 address)
return false
}
return ip.IsGlobalUnicast()
}
func fixupPort(uri *url.URL) *url.URL {
copyURI := *uri
host, port, err := net.SplitHostPort(uri.Host)
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
// addr is on the form "1.2.3.4"
copyURI.Host = net.JoinHostPort(host, "22000")
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:"
copyURI.Host = net.JoinHostPort(host, "22000")
}
return &copyURI
}

View File

@ -78,8 +78,8 @@ func (m *cachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, p
// Lookup attempts to resolve the device ID using any of the added Finders, // Lookup attempts to resolve the device ID using any of the added Finders,
// while obeying the cache settings. // while obeying the cache settings.
func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) { func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
var pdirect []prioritizedAddress var paddresses []prioritizedAddress
m.mut.RLock() m.mut.RLock()
for i, finder := range m.finders { for i, finder := range m.finders {
@ -90,10 +90,9 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
// It's a positive, valid entry. Use it. // It's a positive, valid entry. Use it.
l.Debugln("cached discovery entry for", deviceID, "at", finder) l.Debugln("cached discovery entry for", deviceID, "at", finder)
l.Debugln(" cache:", cacheEntry) l.Debugln(" cache:", cacheEntry)
for _, addr := range cacheEntry.Direct { for _, addr := range cacheEntry.Addresses {
pdirect = append(pdirect, prioritizedAddress{finder.priority, addr}) paddresses = append(paddresses, prioritizedAddress{finder.priority, addr})
} }
relays = append(relays, cacheEntry.Relays...)
continue continue
} }
@ -109,19 +108,16 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
} }
// Perform the actual lookup and cache the result. // Perform the actual lookup and cache the result.
if td, tr, err := finder.Lookup(deviceID); err == nil { if addrs, err := finder.Lookup(deviceID); err == nil {
l.Debugln("lookup for", deviceID, "at", finder) l.Debugln("lookup for", deviceID, "at", finder)
l.Debugln(" direct:", td) l.Debugln(" addresses:", addrs)
l.Debugln(" relays:", tr) for _, addr := range addrs {
for _, addr := range td { paddresses = append(paddresses, prioritizedAddress{finder.priority, addr})
pdirect = append(pdirect, prioritizedAddress{finder.priority, addr})
} }
relays = append(relays, tr...)
m.caches[i].Set(deviceID, CacheEntry{ m.caches[i].Set(deviceID, CacheEntry{
Direct: td, Addresses: addrs,
Relays: tr,
when: time.Now(), when: time.Now(),
found: len(td)+len(tr) > 0, found: len(addrs) > 0,
}) })
} else { } else {
// Lookup returned error, add a negative cache entry. // Lookup returned error, add a negative cache entry.
@ -137,13 +133,11 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
} }
m.mut.RUnlock() m.mut.RUnlock()
direct = uniqueSortedAddrs(pdirect) addresses = uniqueSortedAddrs(paddresses)
relays = uniqueSortedRelays(relays)
l.Debugln("lookup results for", deviceID) l.Debugln("lookup results for", deviceID)
l.Debugln(" direct: ", direct) l.Debugln(" addresses: ", addresses)
l.Debugln(" relays: ", relays)
return direct, relays, nil return addresses, nil
} }
func (m *cachingMux) String() string { func (m *cachingMux) String() string {
@ -245,36 +239,6 @@ func uniqueSortedAddrs(ss []prioritizedAddress) []string {
return filtered return filtered
} }
func uniqueSortedRelays(rs []Relay) []Relay {
m := make(map[string]Relay, len(rs))
for _, r := range rs {
m[r.URL] = r
}
var ur = make([]Relay, 0, len(m))
for _, r := range m {
ur = append(ur, r)
}
sort.Sort(relayList(ur))
return ur
}
type relayList []Relay
func (l relayList) Len() int {
return len(l)
}
func (l relayList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l relayList) Less(a, b int) bool {
return l[a].URL < l[b].URL
}
type prioritizedAddressList []prioritizedAddress type prioritizedAddressList []prioritizedAddress
func (l prioritizedAddressList) Len() int { func (l prioritizedAddressList) Len() int {

View File

@ -15,13 +15,13 @@ import (
) )
func TestCacheUnique(t *testing.T) { func TestCacheUnique(t *testing.T) {
direct0 := []string{"tcp://192.0.2.44:22000", "tcp://192.0.2.42:22000"} // prio 0 addresses0 := []string{"tcp://192.0.2.44:22000", "tcp://192.0.2.42:22000"} // prio 0
direct1 := []string{"tcp://192.0.2.43:22000", "tcp://192.0.2.42:22000"} // prio 1 addresses1 := []string{"tcp://192.0.2.43:22000", "tcp://192.0.2.42:22000"} // prio 1
// what we expect from just direct0 // what we expect from just addresses0
direct0Sorted := []string{"tcp://192.0.2.42:22000", "tcp://192.0.2.44:22000"} addresses0Sorted := []string{"tcp://192.0.2.42:22000", "tcp://192.0.2.44:22000"}
// what we expect from direct0+direct1 // what we expect from addresses0+addresses1
totalSorted := []string{ totalSorted := []string{
// first prio 0, sorted // first prio 0, sorted
"tcp://192.0.2.42:22000", "tcp://192.0.2.44:22000", "tcp://192.0.2.42:22000", "tcp://192.0.2.44:22000",
@ -30,8 +30,6 @@ func TestCacheUnique(t *testing.T) {
// no duplicate .42 // no duplicate .42
} }
relays := []Relay{{URL: "relay://192.0.2.44:443"}, {URL: "tcp://192.0.2.45:443"}}
c := NewCachingMux() c := NewCachingMux()
c.(*cachingMux).ServeBackground() c.(*cachingMux).ServeBackground()
defer c.Stop() defer c.Stop()
@ -39,45 +37,38 @@ func TestCacheUnique(t *testing.T) {
// Add a fake discovery service and verify we get it's answers through the // Add a fake discovery service and verify we get it's answers through the
// cache. // cache.
f1 := &fakeDiscovery{direct0, relays} f1 := &fakeDiscovery{addresses0}
c.Add(f1, time.Minute, 0, 0) c.Add(f1, time.Minute, 0, 0)
dir, rel, err := c.Lookup(protocol.LocalDeviceID) addr, err := c.Lookup(protocol.LocalDeviceID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(dir, direct0Sorted) { if !reflect.DeepEqual(addr, addresses0Sorted) {
t.Errorf("Incorrect direct; %+v != %+v", dir, direct0Sorted) t.Errorf("Incorrect addresses; %+v != %+v", addr, addresses0Sorted)
}
if !reflect.DeepEqual(rel, relays) {
t.Errorf("Incorrect relays; %+v != %+v", rel, relays)
} }
// Add one more that answers in the same way and check that we don't // Add one more that answers in the same way and check that we don't
// duplicate or otherwise mess up the responses now. // duplicate or otherwise mess up the responses now.
f2 := &fakeDiscovery{direct1, relays} f2 := &fakeDiscovery{addresses1}
c.Add(f2, time.Minute, 0, 1) c.Add(f2, time.Minute, 0, 1)
dir, rel, err = c.Lookup(protocol.LocalDeviceID) addr, err = c.Lookup(protocol.LocalDeviceID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(dir, totalSorted) { if !reflect.DeepEqual(addr, totalSorted) {
t.Errorf("Incorrect direct; %+v != %+v", dir, totalSorted) t.Errorf("Incorrect addresses; %+v != %+v", addr, totalSorted)
}
if !reflect.DeepEqual(rel, relays) {
t.Errorf("Incorrect relays; %+v != %+v", rel, relays)
} }
} }
type fakeDiscovery struct { type fakeDiscovery struct {
direct []string addresses []string
relays []Relay
} }
func (f *fakeDiscovery) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) { func (f *fakeDiscovery) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
return f.direct, f.relays, nil return f.addresses, nil
} }
func (f *fakeDiscovery) Error() error { func (f *fakeDiscovery) Error() error {
@ -126,10 +117,10 @@ type slowDiscovery struct {
started chan struct{} started chan struct{}
} }
func (f *slowDiscovery) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) { func (f *slowDiscovery) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
close(f.started) close(f.started)
time.Sleep(f.delay) time.Sleep(f.delay)
return nil, nil, nil return nil, nil
} }
func (f *slowDiscovery) Error() error { func (f *slowDiscovery) Error() error {

View File

@ -15,15 +15,14 @@ import (
// A Finder provides lookup services of some kind. // A Finder provides lookup services of some kind.
type Finder interface { type Finder interface {
Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) Lookup(deviceID protocol.DeviceID) (address []string, err error)
Error() error Error() error
String() string String() string
Cache() map[protocol.DeviceID]CacheEntry Cache() map[protocol.DeviceID]CacheEntry
} }
type CacheEntry struct { type CacheEntry struct {
Direct []string `json:"direct"` Addresses []string `json:"addresses"`
Relays []Relay `json:"relays"`
when time.Time // When did we get the result when time.Time // When did we get the result
found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)? found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
validUntil time.Time // Validity time, overrides normal calculation validUntil time.Time // Validity time, overrides normal calculation
@ -41,12 +40,6 @@ type FinderMux interface {
ChildStatus() map[string]error ChildStatus() map[string]error
} }
// The RelayStatusProvider answers questions about current relay status.
type RelayStatusProvider interface {
Relays() []string
RelayStatus(uri string) (time.Duration, bool)
}
// The AddressLister answers questions about what addresses we are listening // The AddressLister answers questions about what addresses we are listening
// on. // on.
type AddressLister interface { type AddressLister interface {

View File

@ -26,7 +26,6 @@ import (
type globalClient struct { type globalClient struct {
server string server string
addrList AddressLister addrList AddressLister
relayStat RelayStatusProvider
announceClient httpClient announceClient httpClient
queryClient httpClient queryClient httpClient
noAnnounce bool noAnnounce bool
@ -46,8 +45,7 @@ const (
) )
type announcement struct { type announcement struct {
Direct []string `json:"direct"` Addresses []string `json:"addresses"`
Relays []Relay `json:"relays"`
} }
type serverOptions struct { type serverOptions struct {
@ -66,7 +64,7 @@ func (e lookupError) CacheFor() time.Duration {
return e.cacheFor return e.cacheFor
} }
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) { func NewGlobal(server string, cert tls.Certificate, addrList AddressLister) (FinderService, error) {
server, opts, err := parseOptions(server) server, opts, err := parseOptions(server)
if err != nil { if err != nil {
return nil, err return nil, err
@ -117,7 +115,6 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, rela
cl := &globalClient{ cl := &globalClient{
server: server, server: server,
addrList: addrList, addrList: addrList,
relayStat: relayStat,
announceClient: announceClient, announceClient: announceClient,
queryClient: queryClient, queryClient: queryClient,
noAnnounce: opts.noAnnounce, noAnnounce: opts.noAnnounce,
@ -128,12 +125,11 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, rela
return cl, nil return cl, nil
} }
// Lookup returns the list of addresses where the given device is available; // Lookup returns the list of addresses where the given device is available
// direct, and via relays. func (c *globalClient) Lookup(device protocol.DeviceID) (addresses []string, err error) {
func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
qURL, err := url.Parse(c.server) qURL, err := url.Parse(c.server)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
q := qURL.Query() q := qURL.Query()
@ -143,7 +139,7 @@ func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays
resp, err := c.queryClient.Get(qURL.String()) resp, err := c.queryClient.Get(qURL.String())
if err != nil { if err != nil {
l.Debugln("globalClient.Lookup", qURL, err) l.Debugln("globalClient.Lookup", qURL, err)
return nil, nil, err return nil, err
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
resp.Body.Close() resp.Body.Close()
@ -155,13 +151,13 @@ func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays
cacheFor: time.Duration(secs) * time.Second, cacheFor: time.Duration(secs) * time.Second,
} }
} }
return nil, nil, err return nil, err
} }
var ann announcement var ann announcement
err = json.NewDecoder(resp.Body).Decode(&ann) err = json.NewDecoder(resp.Body).Decode(&ann)
resp.Body.Close() resp.Body.Close()
return ann.Direct, ann.Relays, err return ann.Addresses, err
} }
func (c *globalClient) String() string { func (c *globalClient) String() string {
@ -179,13 +175,15 @@ func (c *globalClient) Serve() {
timer := time.NewTimer(0) timer := time.NewTimer(0)
defer timer.Stop() defer timer.Stop()
eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged | events.RelayStateChanged) eventSub := events.Default.Subscribe(events.ListenAddressesChanged)
defer events.Default.Unsubscribe(eventSub) defer events.Default.Unsubscribe(eventSub)
for { for {
select { select {
case <-eventSub.C(): case <-eventSub.C():
c.sendAnnouncement(timer) // Defer announcement by 2 seconds, essentially debouncing
// if we have a stream of events incoming in quick succession.
timer.Reset(2 * time.Second)
case <-timer.C: case <-timer.C:
c.sendAnnouncement(timer) c.sendAnnouncement(timer)
@ -200,22 +198,10 @@ func (c *globalClient) sendAnnouncement(timer *time.Timer) {
var ann announcement var ann announcement
if c.addrList != nil { if c.addrList != nil {
ann.Direct = c.addrList.ExternalAddresses() ann.Addresses = c.addrList.ExternalAddresses()
} }
if c.relayStat != nil { if len(ann.Addresses) == 0 {
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")) c.setError(errors.New("nothing to announce"))
l.Debugln("Nothing to announce") l.Debugln("Nothing to announce")
timer.Reset(announceErrorRetryInterval) timer.Reset(announceErrorRetryInterval)

View File

@ -54,15 +54,15 @@ func TestGlobalOverHTTP(t *testing.T) {
// is only allowed in combination with the "insecure" and "noannounce" // is only allowed in combination with the "insecure" and "noannounce"
// parameters. // parameters.
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, nil); err == nil { if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil); err == nil {
t.Fatal("http is not allowed without insecure and noannounce") 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 { if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil); err == nil {
t.Fatal("http is not allowed without noannounce") t.Fatal("http is not allowed without noannounce")
} }
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, nil); err == nil { if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil); err == nil {
t.Fatal("http is not allowed without insecure") t.Fatal("http is not allowed without insecure")
} }
@ -80,30 +80,27 @@ func TestGlobalOverHTTP(t *testing.T) {
go http.Serve(list, mux) go http.Serve(list, mux)
// This should succeed // This should succeed
direct, relays, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce") addresses, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if !testing.Short() { if !testing.Short() {
// This should time out // This should time out
_, _, err = testLookup("http://" + list.Addr().String() + "/block?insecure&noannounce") _, err = testLookup("http://" + list.Addr().String() + "/block?insecure&noannounce")
if err == nil { if err == nil {
t.Fatalf("unexpected nil error, should have been a timeout") t.Fatalf("unexpected nil error, should have been a timeout")
} }
} }
// This should work again // This should work again
_, _, err = testLookup("http://" + list.Addr().String() + "?insecure&noannounce") _, err = testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" { if len(addresses) != 1 || addresses[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect direct list: %+v", direct) t.Errorf("incorrect addresses list: %+v", addresses)
}
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
t.Errorf("incorrect relays list: %+v", direct)
} }
} }
@ -136,7 +133,7 @@ func TestGlobalOverHTTPS(t *testing.T) {
// here so we expect the lookup to fail. // here so we expect the lookup to fail.
url := "https://" + list.Addr().String() url := "https://" + list.Addr().String()
if _, _, err := testLookup(url); err == nil { if _, err := testLookup(url); err == nil {
t.Fatalf("unexpected nil error when we should have got a certificate error") t.Fatalf("unexpected nil error when we should have got a certificate error")
} }
@ -144,21 +141,18 @@ func TestGlobalOverHTTPS(t *testing.T) {
// be accepted. // be accepted.
url = "https://" + list.Addr().String() + "?insecure" url = "https://" + list.Addr().String() + "?insecure"
if direct, relays, err := testLookup(url); err != nil { if addresses, err := testLookup(url); err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} else { } else {
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" { if len(addresses) != 1 || addresses[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect direct list: %+v", direct) t.Errorf("incorrect addresses list: %+v", addresses)
}
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. // With "id" set to something incorrect, the checks should fail again.
url = "https://" + list.Addr().String() + "?id=" + protocol.LocalDeviceID.String() url = "https://" + list.Addr().String() + "?id=" + protocol.LocalDeviceID.String()
if _, _, err := testLookup(url); err == nil { if _, err := testLookup(url); err == nil {
t.Fatalf("unexpected nil error for incorrect discovery server ID") t.Fatalf("unexpected nil error for incorrect discovery server ID")
} }
@ -167,14 +161,11 @@ func TestGlobalOverHTTPS(t *testing.T) {
id := protocol.NewDeviceID(cert.Certificate[0]) id := protocol.NewDeviceID(cert.Certificate[0])
url = "https://" + list.Addr().String() + "?id=" + id.String() url = "https://" + list.Addr().String() + "?id=" + id.String()
if direct, relays, err := testLookup(url); err != nil { if addresses, err := testLookup(url); err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} else { } else {
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" { if len(addresses) != 1 || addresses[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect direct list: %+v", direct) t.Errorf("incorrect addresses list: %+v", addresses)
}
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
t.Errorf("incorrect relays list: %+v", direct)
} }
} }
} }
@ -204,7 +195,7 @@ func TestGlobalAnnounce(t *testing.T) {
go http.Serve(list, mux) go http.Serve(list, mux)
url := "https://" + list.Addr().String() + "?insecure" url := "https://" + list.Addr().String() + "?insecure"
disco, err := NewGlobal(url, cert, new(fakeAddressLister), new(fakeRelayStatus)) disco, err := NewGlobal(url, cert, new(fakeAddressLister))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -223,17 +214,14 @@ func TestGlobalAnnounce(t *testing.T) {
} }
if !strings.Contains(string(s.announce), "tcp://0.0.0.0:22000") { if !strings.Contains(string(s.announce), "tcp://0.0.0.0:22000") {
t.Errorf("announce missing direct address: %s", s.announce) t.Errorf("announce missing addresses 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) { func testLookup(url string) ([]string, error) {
disco, err := NewGlobal(url, tls.Certificate{}, nil, nil) disco, err := NewGlobal(url, tls.Certificate{}, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
go disco.Serve() go disco.Serve()
defer disco.Stop() defer disco.Stop()
@ -256,7 +244,7 @@ func (s *fakeDiscoveryServer) handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204) w.WriteHeader(204)
} else { } else {
w.Header().Set("Content-Type", "application/json") 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}]}`)) w.Write([]byte(`{"addresses":["tcp://192.0.2.42::22000"], "relays":[{"url": "relay://192.0.2.43:443", "latency": 42}]}`))
} }
} }
@ -268,12 +256,3 @@ func (f *fakeAddressLister) ExternalAddresses() []string {
func (f *fakeAddressLister) AllAddresses() []string { func (f *fakeAddressLister) AllAddresses() []string {
return []string{"tcp://0.0.0.0:22000", "tcp://192.168.0.1:22000"} 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
}

View File

@ -9,7 +9,6 @@ package discover
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"io" "io"
"net" "net"
"net/url" "net/url"
@ -26,7 +25,6 @@ type localClient struct {
*suture.Supervisor *suture.Supervisor
myID protocol.DeviceID myID protocol.DeviceID
addrList AddressLister addrList AddressLister
relayStat RelayStatusProvider
name string name string
beacon beacon.Interface beacon beacon.Interface
@ -42,16 +40,11 @@ const (
CacheLifeTime = 3 * BroadcastInterval CacheLifeTime = 3 * BroadcastInterval
) )
var ( func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister) (FinderService, error) {
ErrIncorrectMagic = errors.New("incorrect magic number")
)
func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
c := &localClient{ c := &localClient{
Supervisor: suture.NewSimple("local"), Supervisor: suture.NewSimple("local"),
myID: id, myID: id,
addrList: addrList, addrList: addrList,
relayStat: relayStat,
localBcastTick: time.Tick(BroadcastInterval), localBcastTick: time.Tick(BroadcastInterval),
forcedBcastTick: make(chan time.Time), forcedBcastTick: make(chan time.Time),
localBcastStart: time.Now(), localBcastStart: time.Now(),
@ -94,13 +87,11 @@ func (c *localClient) startLocalIPv6Multicasts(localMCAddr string) {
go c.recvAnnouncements(c.beacon) go c.recvAnnouncements(c.beacon)
} }
// Lookup returns a list of addresses the device is available at. Local // Lookup returns a list of addresses the device is available at.
// discovery never returns relays. func (c *localClient) Lookup(device protocol.DeviceID) (addresses []string, err error) {
func (c *localClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
if cache, ok := c.Get(device); ok { if cache, ok := c.Get(device); ok {
if time.Since(cache.when) < CacheLifeTime { if time.Since(cache.when) < CacheLifeTime {
direct = cache.Direct addresses = cache.Addresses
relays = cache.Relays
} }
} }
@ -123,25 +114,11 @@ func (c *localClient) announcementPkt() Announce {
}) })
} }
var relays []Relay
if c.relayStat != nil {
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{ return Announce{
Magic: AnnouncementMagic, Magic: AnnouncementMagic,
This: Device{ This: Device{
ID: c.myID[:], ID: c.myID[:],
Addresses: addrs, Addresses: addrs,
Relays: relays,
}, },
} }
} }
@ -171,6 +148,11 @@ func (c *localClient) recvAnnouncements(b beacon.Interface) {
continue continue
} }
if pkt.Magic != AnnouncementMagic {
l.Debugf("discover: Incorrect magic from %s: %s != %s", addr, pkt.Magic, AnnouncementMagic)
continue
}
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID)) l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
var newDevice bool var newDevice bool
@ -230,8 +212,7 @@ func (c *localClient) registerDevice(src net.Addr, device Device) bool {
} }
c.Set(id, CacheEntry{ c.Set(id, CacheEntry{
Direct: validAddresses, Addresses: validAddresses,
Relays: device.Relays,
when: time.Now(), when: time.Now(),
found: true, found: true,
}) })
@ -240,7 +221,6 @@ func (c *localClient) registerDevice(src net.Addr, device Device) bool {
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{ events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
"device": id.String(), "device": id.String(),
"addrs": validAddresses, "addrs": validAddresses,
"relays": device.Relays,
}) })
} }

View File

@ -10,7 +10,7 @@
package discover package discover
const ( const (
AnnouncementMagic = 0x9D79BC40 AnnouncementMagic = 0x7D79BC40
) )
type Announce struct { type Announce struct {
@ -22,14 +22,8 @@ type Announce struct {
type Device struct { type Device struct {
ID []byte // max:32 ID []byte // max:32
Addresses []Address // max:16 Addresses []Address // max:16
Relays []Relay // max:16
} }
type Address struct { type Address struct {
URL string // max:2083 URL string // max:2083
} }
type Relay struct {
URL string `json:"url"` // max:2083
Latency int32 `json:"latency"`
}

View File

@ -119,26 +119,18 @@ Device Structure:
\ Zero or more Address Structures \ \ Zero or more Address Structures \
/ / / /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Number of Relays |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Zero or more Relay Structures \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct Device { struct Device {
opaque ID<32>; opaque ID<32>;
Address Addresses<16>; Address Addresses<16>;
Relay Relays<16>;
} }
*/ */
func (o Device) XDRSize() int { func (o Device) XDRSize() int {
return 4 + len(o.ID) + xdr.Padding(len(o.ID)) + return 4 + len(o.ID) + xdr.Padding(len(o.ID)) +
4 + xdr.SizeOfSlice(o.Addresses) + 4 + xdr.SizeOfSlice(o.Addresses)
4 + xdr.SizeOfSlice(o.Relays)
} }
func (o Device) MarshalXDR() ([]byte, error) { func (o Device) MarshalXDR() ([]byte, error) {
@ -169,15 +161,6 @@ func (o Device) MarshalXDRInto(m *xdr.Marshaller) error {
return err return err
} }
} }
if l := len(o.Relays); l > 16 {
return xdr.ElementSizeExceeded("Relays", l, 16)
}
m.MarshalUint32(uint32(len(o.Relays)))
for i := range o.Relays {
if err := o.Relays[i].MarshalXDRInto(m); err != nil {
return err
}
}
return m.Error return m.Error
} }
@ -205,24 +188,6 @@ func (o *Device) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
(&o.Addresses[i]).UnmarshalXDRFrom(u) (&o.Addresses[i]).UnmarshalXDRFrom(u)
} }
} }
_RelaysSize := int(u.UnmarshalUint32())
if _RelaysSize < 0 {
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
} else if _RelaysSize == 0 {
o.Relays = nil
} else {
if _RelaysSize > 16 {
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
}
if _RelaysSize <= len(o.Relays) {
o.Relays = o.Relays[:_RelaysSize]
} else {
o.Relays = make([]Relay, _RelaysSize)
}
for i := range o.Relays {
(&o.Relays[i]).UnmarshalXDRFrom(u)
}
}
return u.Error return u.Error
} }
@ -279,62 +244,3 @@ func (o *Address) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
o.URL = u.UnmarshalStringMax(2083) o.URL = u.UnmarshalStringMax(2083)
return u.Error return u.Error
} }
/*
Relay Structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ URL (length + padded data) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Latency |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct Relay {
string URL<2083>;
int Latency;
}
*/
func (o Relay) XDRSize() int {
return 4 + len(o.URL) + xdr.Padding(len(o.URL)) + 4
}
func (o Relay) MarshalXDR() ([]byte, error) {
buf := make([]byte, o.XDRSize())
m := &xdr.Marshaller{Data: buf}
return buf, o.MarshalXDRInto(m)
}
func (o Relay) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o Relay) MarshalXDRInto(m *xdr.Marshaller) error {
if l := len(o.URL); l > 2083 {
return xdr.ElementSizeExceeded("URL", l, 2083)
}
m.MarshalString(o.URL)
m.MarshalUint32(uint32(o.Latency))
return m.Error
}
func (o *Relay) UnmarshalXDR(bs []byte) error {
u := &xdr.Unmarshaller{Data: bs}
return o.UnmarshalXDRFrom(u)
}
func (o *Relay) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
o.URL = u.UnmarshalStringMax(2083)
o.Latency = int32(u.UnmarshalUint32())
return u.Error
}

View File

@ -39,8 +39,7 @@ const (
FolderCompletion FolderCompletion
FolderErrors FolderErrors
FolderScanProgress FolderScanProgress
ExternalPortMappingChanged ListenAddressesChanged
RelayStateChanged
LoginAttempt LoginAttempt
AllEvents = (1 << iota) - 1 AllEvents = (1 << iota) - 1
@ -90,10 +89,8 @@ func (t EventType) String() string {
return "DeviceResumed" return "DeviceResumed"
case FolderScanProgress: case FolderScanProgress:
return "FolderScanProgress" return "FolderScanProgress"
case ExternalPortMappingChanged: case ListenAddressesChanged:
return "ExternalPortMappingChanged" return "ListenAddressesChanged"
case RelayStateChanged:
return "RelayStateChanged"
case LoginAttempt: case LoginAttempt:
return "LoginAttempt" return "LoginAttempt"
default: default:

View File

@ -1,52 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package model
import (
"crypto/tls"
"net"
"github.com/syncthing/syncthing/lib/protocol"
)
type IntermediateConnection struct {
*tls.Conn
Type ConnectionType
}
type Connection struct {
net.Conn
protocol.Connection
Type ConnectionType
}
const (
ConnectionTypeDirectAccept ConnectionType = iota
ConnectionTypeDirectDial
ConnectionTypeRelayAccept
ConnectionTypeRelayDial
)
type ConnectionType int
func (t ConnectionType) String() string {
switch t {
case ConnectionTypeDirectAccept:
return "direct-accept"
case ConnectionTypeDirectDial:
return "direct-dial"
case ConnectionTypeRelayAccept:
return "relay-accept"
case ConnectionTypeRelayDial:
return "relay-dial"
}
return "unknown"
}
func (t ConnectionType) IsDirect() bool {
return t == ConnectionTypeDirectAccept || t == ConnectionTypeDirectDial
}

View File

@ -24,6 +24,7 @@ import (
"time" "time"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/ignore" "github.com/syncthing/syncthing/lib/ignore"
@ -92,7 +93,7 @@ type Model struct {
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
fmut sync.RWMutex // protects the above fmut sync.RWMutex // protects the above
conn map[protocol.DeviceID]Connection conn map[protocol.DeviceID]connections.Connection
helloMessages map[protocol.DeviceID]protocol.HelloMessage helloMessages map[protocol.DeviceID]protocol.HelloMessage
deviceClusterConf map[protocol.DeviceID]protocol.ClusterConfigMessage deviceClusterConf map[protocol.DeviceID]protocol.ClusterConfigMessage
devicePaused map[protocol.DeviceID]bool devicePaused map[protocol.DeviceID]bool
@ -137,7 +138,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
folderRunners: make(map[string]service), folderRunners: make(map[string]service),
folderRunnerTokens: make(map[string][]suture.ServiceToken), folderRunnerTokens: make(map[string][]suture.ServiceToken),
folderStatRefs: make(map[string]*stats.FolderStatisticsReference), folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
conn: make(map[protocol.DeviceID]Connection), conn: make(map[protocol.DeviceID]connections.Connection),
helloMessages: make(map[protocol.DeviceID]protocol.HelloMessage), helloMessages: make(map[protocol.DeviceID]protocol.HelloMessage),
deviceClusterConf: make(map[protocol.DeviceID]protocol.ClusterConfigMessage), deviceClusterConf: make(map[protocol.DeviceID]protocol.ClusterConfigMessage),
devicePaused: make(map[protocol.DeviceID]bool), devicePaused: make(map[protocol.DeviceID]bool),
@ -277,7 +278,7 @@ type ConnectionInfo struct {
Paused bool Paused bool
Address string Address string
ClientVersion string ClientVersion string
Type ConnectionType Type string
} }
func (info ConnectionInfo) MarshalJSON() ([]byte, error) { func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
@ -289,7 +290,7 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
"paused": info.Paused, "paused": info.Paused,
"address": info.Address, "address": info.Address,
"clientVersion": info.ClientVersion, "clientVersion": info.ClientVersion,
"type": info.Type.String(), "type": info.Type,
}) })
} }
@ -308,7 +309,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
versionString = hello.ClientName + " " + hello.ClientVersion versionString = hello.ClientName + " " + hello.ClientVersion
} }
ci := ConnectionInfo{ ci := ConnectionInfo{
ClientVersion: versionString, ClientVersion: strings.TrimSpace(versionString),
Paused: m.devicePaused[device], Paused: m.devicePaused[device],
} }
if conn, ok := m.conn[device]; ok { if conn, ok := m.conn[device]; ok {
@ -1004,7 +1005,7 @@ func (m *Model) GetHello(protocol.DeviceID) protocol.HelloMessage {
// AddConnection adds a new peer connection to the model. An initial index will // AddConnection adds a new peer connection to the model. An initial index will
// be sent to the connected peer, thereafter index updates whenever the local // be sent to the connected peer, thereafter index updates whenever the local
// folder changes. // folder changes.
func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) { func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloMessage) {
deviceID := conn.ID() deviceID := conn.ID()
m.pmut.Lock() m.pmut.Lock()
@ -1021,7 +1022,7 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
"deviceName": hello.DeviceName, "deviceName": hello.DeviceName,
"clientName": hello.ClientName, "clientName": hello.ClientName,
"clientVersion": hello.ClientVersion, "clientVersion": hello.ClientVersion,
"type": conn.Type.String(), "type": conn.Type,
} }
addr := conn.RemoteAddr() addr := conn.RemoteAddr()

View File

@ -8,6 +8,7 @@ package model
import ( import (
"bytes" "bytes"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -22,6 +23,7 @@ import (
"github.com/d4l3k/messagediff" "github.com/d4l3k/messagediff"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
) )
@ -292,10 +294,13 @@ func BenchmarkRequest(b *testing.B) {
id: device1, id: device1,
requestData: []byte("some data to return"), requestData: []byte("some data to return"),
} }
m.AddConnection(Connection{ m.AddConnection(connections.Connection{
&net.TCPConn{}, IntermediateConnection: connections.IntermediateConnection{
fc, Conn: tls.Client(&fakeConn{}, nil),
ConnectionTypeDirectAccept, Type: "foo",
Priority: 10,
},
Connection: fc,
}, protocol.HelloMessage{}) }, protocol.HelloMessage{})
m.Index(device1, "default", files, 0, nil) m.Index(device1, "default", files, 0, nil)
@ -333,13 +338,16 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device already has a name") t.Errorf("Device already has a name")
} }
conn := Connection{ conn := connections.Connection{
&net.TCPConn{}, IntermediateConnection: connections.IntermediateConnection{
&FakeConnection{ Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: device1, id: device1,
requestData: []byte("some data to return"), requestData: []byte("some data to return"),
}, },
ConnectionTypeDirectAccept,
} }
m.AddConnection(conn, hello) m.AddConnection(conn, hello)
@ -1351,3 +1359,47 @@ func TestUnifySubs(t *testing.T) {
} }
} }
} }
type fakeAddr struct{}
func (fakeAddr) Network() string {
return "network"
}
func (fakeAddr) String() string {
return "address"
}
type fakeConn struct{}
func (fakeConn) Close() error {
return nil
}
func (fakeConn) LocalAddr() net.Addr {
return &fakeAddr{}
}
func (fakeConn) RemoteAddr() net.Addr {
return &fakeAddr{}
}
func (fakeConn) Read([]byte) (int, error) {
return 0, nil
}
func (fakeConn) Write([]byte) (int, error) {
return 0, nil
}
func (fakeConn) SetDeadline(time.Time) error {
return nil
}
func (fakeConn) SetReadDeadline(time.Time) error {
return nil
}
func (fakeConn) SetWriteDeadline(time.Time) error {
return nil
}

View File

@ -24,11 +24,9 @@ type Service struct {
id protocol.DeviceID id protocol.DeviceID
cfg *config.Wrapper cfg *config.Wrapper
stop chan struct{} stop chan struct{}
immediate chan chan struct{}
timer *time.Timer
announce *stdsync.Once
mappings []*Mapping mappings []*Mapping
timer *time.Timer
mut sync.RWMutex mut sync.RWMutex
} }
@ -37,33 +35,45 @@ func NewService(id protocol.DeviceID, cfg *config.Wrapper) *Service {
id: id, id: id,
cfg: cfg, cfg: cfg,
immediate: make(chan chan struct{}), timer: time.NewTimer(0),
timer: time.NewTimer(time.Second),
mut: sync.NewRWMutex(), mut: sync.NewRWMutex(),
} }
} }
func (s *Service) Serve() { func (s *Service) Serve() {
announce := stdsync.Once{}
s.timer.Reset(0) s.timer.Reset(0)
s.mut.Lock()
s.stop = make(chan struct{}) s.stop = make(chan struct{})
s.announce = &stdsync.Once{} s.mut.Unlock()
for { for {
select { select {
case result := <-s.immediate:
s.process()
close(result)
case <-s.timer.C: case <-s.timer.C:
s.process() if found := s.process(); found != -1 {
announce.Do(func() {
suffix := "s"
if found == 1 {
suffix = ""
}
l.Infoln("Detected", found, "NAT device"+suffix)
})
}
case <-s.stop: case <-s.stop:
s.timer.Stop() s.timer.Stop()
s.mut.RLock()
for _, mapping := range s.mappings {
mapping.clearAddresses()
}
s.mut.RUnlock()
return return
} }
} }
} }
func (s *Service) process() { func (s *Service) process() int {
// toRenew are mappings which are due for renewal // toRenew are mappings which are due for renewal
// toUpdate are the remaining mappings, which will only be updated if one of // toUpdate are the remaining mappings, which will only be updated if one of
// the old IGDs has gone away, or a new IGD has appeared, but only if we // the old IGDs has gone away, or a new IGD has appeared, but only if we
@ -88,25 +98,25 @@ func (s *Service) process() {
} }
} }
} }
s.mut.RUnlock() // Reset the timer while holding the lock, because of the following race:
// T1: process acquires lock
// T1: process checks the mappings and gets next renewal time in 30m
// T2: process releases the lock
// T2: NewMapping acquires the lock
// T2: NewMapping adds mapping
// T2: NewMapping releases the lock
// T2: NewMapping resets timer to 1s
// T1: process resets timer to 30
s.timer.Reset(renewIn) s.timer.Reset(renewIn)
s.mut.RUnlock()
// Don't do anything, unless we really need to renew // Don't do anything, unless we really need to renew
if len(toRenew) == 0 { if len(toRenew) == 0 {
return return -1
} }
nats := discoverAll(time.Duration(s.cfg.Options().NATRenewalM)*time.Minute, time.Duration(s.cfg.Options().NATTimeoutS)*time.Second) nats := discoverAll(time.Duration(s.cfg.Options().NATRenewalM)*time.Minute, time.Duration(s.cfg.Options().NATTimeoutS)*time.Second)
s.announce.Do(func() {
suffix := "s"
if len(nats) == 1 {
suffix = ""
}
l.Infoln("Detected", len(nats), "NAT device"+suffix)
})
for _, mapping := range toRenew { for _, mapping := range toRenew {
s.updateMapping(mapping, nats, true) s.updateMapping(mapping, nats, true)
} }
@ -114,10 +124,14 @@ func (s *Service) process() {
for _, mapping := range toUpdate { for _, mapping := range toUpdate {
s.updateMapping(mapping, nats, false) s.updateMapping(mapping, nats, false)
} }
return len(nats)
} }
func (s *Service) Stop() { func (s *Service) Stop() {
s.mut.RLock()
close(s.stop) close(s.stop)
s.mut.RUnlock()
} }
func (s *Service) NewMapping(protocol Protocol, ip net.IP, port int) *Mapping { func (s *Service) NewMapping(protocol Protocol, ip net.IP, port int) *Mapping {
@ -133,16 +147,30 @@ func (s *Service) NewMapping(protocol Protocol, ip net.IP, port int) *Mapping {
s.mut.Lock() s.mut.Lock()
s.mappings = append(s.mappings, mapping) s.mappings = append(s.mappings, mapping)
// Reset the timer while holding the lock, see process() for explanation
s.timer.Reset(time.Second)
s.mut.Unlock() s.mut.Unlock()
return mapping return mapping
} }
// Sync forces the service to recheck all mappings. // RemoveMapping does not actually remove the mapping from the IGD, it just
func (s *Service) Sync() { // internally removes it which stops renewing the mapping. Also, it clears any
wait := make(chan struct{}) // existing mapped addresses from the mapping, which as a result should cause
s.immediate <- wait // discovery to reannounce the new addresses.
<-wait func (s *Service) RemoveMapping(mapping *Mapping) {
s.mut.Lock()
defer s.mut.Unlock()
for i, existing := range s.mappings {
if existing == mapping {
mapping.clearAddresses()
last := len(s.mappings) - 1
s.mappings[i] = s.mappings[last]
s.mappings[last] = nil
s.mappings = s.mappings[:last]
return
}
}
} }
// updateMapping compares the addresses of the existing mapping versus the natds // updateMapping compares the addresses of the existing mapping versus the natds

View File

@ -45,6 +45,21 @@ func (m *Mapping) removeAddress(id string) {
m.mut.Unlock() m.mut.Unlock()
} }
func (m *Mapping) clearAddresses() {
m.mut.Lock()
var removed []Address
for id, addr := range m.extAddresses {
l.Debugf("Clearing mapping %s: ID: %s Address: %s", m, id, addr)
removed = append(removed, addr)
delete(m.extAddresses, id)
}
if len(removed) > 0 {
m.notify(nil, removed)
}
m.expires = time.Time{}
m.mut.Unlock()
}
func (m *Mapping) notify(added, removed []Address) { func (m *Mapping) notify(added, removed []Address) {
m.mut.RLock() m.mut.RLock()
for _, subscriber := range m.subscribers { for _, subscriber := range m.subscribers {

View File

@ -24,7 +24,7 @@ var (
type RelayClient interface { type RelayClient interface {
Serve() Serve()
Stop() Stop()
StatusOK() bool Error() error
Latency() time.Duration Latency() time.Duration
String() string String() string
Invitations() chan protocol.SessionInvitation Invitations() chan protocol.SessionInvitation

View File

@ -25,6 +25,7 @@ type dynamicClient struct {
timeout time.Duration timeout time.Duration
mut sync.RWMutex mut sync.RWMutex
err error
client RelayClient client RelayClient
stop chan struct{} stop chan struct{}
} }
@ -61,6 +62,7 @@ func (c *dynamicClient) Serve() {
data, err := http.Get(uri.String()) data, err := http.Get(uri.String())
if err != nil { if err != nil {
l.Debugln(c, "failed to lookup dynamic relays", err) l.Debugln(c, "failed to lookup dynamic relays", err)
c.setError(err)
return return
} }
@ -69,6 +71,7 @@ func (c *dynamicClient) Serve() {
data.Body.Close() data.Body.Close()
if err != nil { if err != nil {
l.Debugln(c, "failed to lookup dynamic relays", err) l.Debugln(c, "failed to lookup dynamic relays", err)
c.setError(err)
return return
} }
@ -89,6 +92,7 @@ func (c *dynamicClient) Serve() {
select { select {
case <-c.stop: case <-c.stop:
l.Debugln(c, "stopping") l.Debugln(c, "stopping")
c.setError(nil)
return return
default: default:
ruri, err := url.Parse(addr) ruri, err := url.Parse(addr)
@ -112,6 +116,7 @@ func (c *dynamicClient) Serve() {
} }
} }
l.Debugln(c, "could not find a connectable relay") l.Debugln(c, "could not find a connectable relay")
c.setError(fmt.Errorf("could not find a connectable relay"))
} }
func (c *dynamicClient) Stop() { func (c *dynamicClient) Stop() {
@ -124,13 +129,13 @@ func (c *dynamicClient) Stop() {
c.client.Stop() c.client.Stop()
} }
func (c *dynamicClient) StatusOK() bool { func (c *dynamicClient) Error() error {
c.mut.RLock() c.mut.RLock()
defer c.mut.RUnlock() defer c.mut.RUnlock()
if c.client == nil { if c.client == nil {
return false return c.err
} }
return c.client.StatusOK() return c.client.Error()
} }
func (c *dynamicClient) Latency() time.Duration { func (c *dynamicClient) Latency() time.Duration {
@ -150,7 +155,7 @@ func (c *dynamicClient) URI() *url.URL {
c.mut.RLock() c.mut.RLock()
defer c.mut.RUnlock() defer c.mut.RUnlock()
if c.client == nil { if c.client == nil {
return c.pooladdr return nil
} }
return c.client.URI() return c.client.URI()
} }
@ -171,6 +176,12 @@ func (c *dynamicClient) cleanup() {
c.mut.Unlock() c.mut.Unlock()
} }
func (c *dynamicClient) setError(err error) {
c.mut.Lock()
c.err = err
c.mut.Unlock()
}
// 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 {

View File

@ -32,6 +32,7 @@ type staticClient struct {
conn *tls.Conn conn *tls.Conn
mut sync.RWMutex mut sync.RWMutex
err error
connected bool connected bool
latency time.Duration latency time.Duration
} }
@ -58,7 +59,6 @@ func newStaticClient(uri *url.URL, certs []tls.Certificate, invitations chan pro
stopped: make(chan struct{}), stopped: make(chan struct{}),
mut: sync.NewRWMutex(), mut: sync.NewRWMutex(),
connected: false,
} }
} }
@ -69,6 +69,7 @@ func (c *staticClient) Serve() {
if err := c.connect(); err != nil { if err := c.connect(); err != nil {
l.Infof("Could not connect to relay %s: %s", c.uri, err) l.Infof("Could not connect to relay %s: %s", c.uri, err)
c.setError(err)
return return
} }
@ -77,12 +78,14 @@ func (c *staticClient) Serve() {
if err := c.join(); err != nil { if err := c.join(); err != nil {
c.conn.Close() c.conn.Close()
l.Infof("Could not join relay %s: %s", c.uri, err) l.Infof("Could not join relay %s: %s", c.uri, err)
c.setError(err)
return return
} }
if err := c.conn.SetDeadline(time.Time{}); err != nil { if err := c.conn.SetDeadline(time.Time{}); err != nil {
c.conn.Close() c.conn.Close()
l.Infoln("Relay set deadline:", err) l.Infoln("Relay set deadline:", err)
c.setError(err)
return return
} }
@ -109,6 +112,7 @@ func (c *staticClient) Serve() {
case protocol.Ping: case protocol.Ping:
if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil { if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil {
l.Infoln("Relay write:", err) l.Infoln("Relay write:", err)
c.setError(err)
c.disconnect() c.disconnect()
} else { } else {
l.Debugln(c, "sent pong") l.Debugln(c, "sent pong")
@ -123,15 +127,18 @@ func (c *staticClient) Serve() {
case protocol.RelayFull: case protocol.RelayFull:
l.Infof("Disconnected from relay %s due to it becoming full.", c.uri) l.Infof("Disconnected from relay %s due to it becoming full.", c.uri)
c.setError(fmt.Errorf("Relay full"))
c.disconnect() c.disconnect()
default: default:
l.Infoln("Relay: protocol error: unexpected message %v", msg) l.Infoln("Relay: protocol error: unexpected message %v", msg)
c.setError(fmt.Errorf("protocol error: unexpected message %v", msg))
c.disconnect() c.disconnect()
} }
case <-c.stop: case <-c.stop:
l.Debugln(c, "stopping") l.Debugln(c, "stopping")
c.setError(nil)
c.disconnect() c.disconnect()
// We always exit via this branch of the select, to make sure the // We always exit via this branch of the select, to make sure the
@ -144,6 +151,9 @@ func (c *staticClient) Serve() {
c.conn.Close() c.conn.Close()
c.connected = false c.connected = false
l.Infof("Disconnecting from relay %s due to error: %s", c.uri, err) l.Infof("Disconnecting from relay %s due to error: %s", c.uri, err)
c.err = err
} else {
c.err = nil
} }
if c.closeInvitationsOnFinish { if c.closeInvitationsOnFinish {
close(c.invitations) close(c.invitations)
@ -155,6 +165,7 @@ func (c *staticClient) Serve() {
case <-timeout.C: case <-timeout.C:
l.Debugln(c, "timed out") l.Debugln(c, "timed out")
c.disconnect() c.disconnect()
c.setError(fmt.Errorf("timed out"))
} }
} }
} }
@ -237,6 +248,19 @@ func (c *staticClient) disconnect() {
c.conn.Close() c.conn.Close()
} }
func (c *staticClient) setError(err error) {
c.mut.Lock()
c.err = err
c.mut.Unlock()
}
func (c *staticClient) Error() error {
c.mut.RLock()
err := c.err
c.mut.RUnlock()
return err
}
func (c *staticClient) join() error { func (c *staticClient) join() error {
if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil { if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil {
return err return err

View File

@ -1,22 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package relay
import (
"os"
"strings"
"github.com/syncthing/syncthing/lib/logger"
)
var (
l = logger.DefaultLogger.NewFacility("relay", "Relay connection handling")
)
func init() {
l.SetDebug("relay", strings.Contains(os.Getenv("STTRACE"), "relay") || os.Getenv("STTRACE") == "all")
}

View File

@ -1,286 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package relay
import (
"crypto/tls"
"net/url"
"sort"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/relay/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/thejerf/suture"
)
const (
eventBroadcasterCheckInterval = 10 * time.Second
)
type Service interface {
suture.Service
Accept() *tls.Conn
Relays() []string
RelayStatus(uri string) (time.Duration, bool)
}
type service struct {
*suture.Supervisor
cfg *config.Wrapper
tlsCfg *tls.Config
tokens map[string]suture.ServiceToken
clients map[string]client.RelayClient
mut sync.RWMutex
invitations chan protocol.SessionInvitation
conns chan *tls.Conn
}
func NewService(cfg *config.Wrapper, tlsCfg *tls.Config) Service {
conns := make(chan *tls.Conn)
service := &service{
Supervisor: suture.New("Service", suture.Spec{
Log: func(log string) {
l.Debugln(log)
},
FailureBackoff: 5 * time.Minute,
FailureDecay: float64((10 * time.Minute) / time.Second),
FailureThreshold: 5,
}),
cfg: cfg,
tlsCfg: tlsCfg,
tokens: make(map[string]suture.ServiceToken),
clients: make(map[string]client.RelayClient),
mut: sync.NewRWMutex(),
invitations: make(chan protocol.SessionInvitation),
conns: conns,
}
rcfg := cfg.Raw()
service.CommitConfiguration(rcfg, rcfg)
cfg.Subscribe(service)
receiver := &invitationReceiver{
tlsCfg: tlsCfg,
conns: conns,
invitations: service.invitations,
stop: make(chan struct{}),
}
eventBc := &eventBroadcaster{
Service: service,
stop: make(chan struct{}),
}
service.Add(receiver)
service.Add(eventBc)
return service
}
func (s *service) VerifyConfiguration(from, to config.Configuration) error {
for _, addr := range to.Options.RelayServers {
_, err := url.Parse(addr)
if err != nil {
return err
}
}
return nil
}
func (s *service) CommitConfiguration(from, to config.Configuration) bool {
existing := make(map[string]*url.URL, len(to.Options.RelayServers))
for _, addr := range to.Options.RelayServers {
uri, err := url.Parse(addr)
if err != nil {
l.Debugln("Failed to parse relay address", addr, err)
continue
}
existing[uri.String()] = uri
}
s.mut.Lock()
for key, uri := range existing {
_, ok := s.tokens[key]
if !ok {
l.Debugln("Connecting to relay", uri)
c, err := client.NewClient(uri, s.tlsCfg.Certificates, s.invitations, 10*time.Second)
if err != nil {
l.Infoln("Failed to connect to relay", uri, err)
continue
}
s.tokens[key] = s.Add(c)
s.clients[key] = c
}
}
for key, token := range s.tokens {
_, ok := existing[key]
if !ok {
err := s.Remove(token)
delete(s.tokens, key)
delete(s.clients, key)
l.Debugln("Disconnecting from relay", key, err)
}
}
s.mut.Unlock()
return true
}
type Status struct {
URL string
OK bool
Latency int
}
// Relays return the list of relays that currently have an OK status.
func (s *service) Relays() []string {
if s == nil {
// A nil client does not have a status, really. Yet we may be called
// this way, for raisins...
return nil
}
s.mut.RLock()
relays := make([]string, 0, len(s.clients))
for _, client := range s.clients {
relays = append(relays, client.URI().String())
}
s.mut.RUnlock()
sort.Strings(relays)
return relays
}
// RelayStatus returns the latency and OK status for a given relay.
func (s *service) 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()
defer s.mut.RUnlock()
for _, client := range s.clients {
if client.URI().String() == uri {
return client.Latency(), client.StatusOK()
}
}
return time.Hour, false
}
// Accept returns a new *tls.Conn. The connection is already handshaken.
func (s *service) Accept() *tls.Conn {
return <-s.conns
}
type invitationReceiver struct {
invitations chan protocol.SessionInvitation
tlsCfg *tls.Config
conns chan<- *tls.Conn
stop chan struct{}
}
func (r *invitationReceiver) Serve() {
for {
select {
case inv := <-r.invitations:
l.Debugln("Received relay invitation", inv)
conn, err := client.JoinSession(inv)
if err != nil {
l.Debugf("Failed to join relay session %s: %v", inv, err)
continue
}
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, r.tlsCfg)
} else {
tc = tls.Client(conn, r.tlsCfg)
}
err = tc.Handshake()
if err != nil {
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
tc.Close()
continue
}
r.conns <- tc
case <-r.stop:
return
}
}
}
func (r *invitationReceiver) Stop() {
close(r.stop)
}
// The eventBroadcaster sends a RelayStateChanged event when the relay status
// changes. We need this somewhat ugly polling mechanism as there's currently
// no way to get the event feed directly from the relay lib. This may be
// something to revisit later, possibly.
type eventBroadcaster struct {
Service Service
stop chan struct{}
}
func (e *eventBroadcaster) Serve() {
timer := time.NewTicker(eventBroadcasterCheckInterval)
defer timer.Stop()
var prevOKRelays []string
for {
select {
case <-timer.C:
curOKRelays := e.Service.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)
}