diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go
index f4b3b56cd..cec27f163 100644
--- a/cmd/syncthing/main.go
+++ b/cmd/syncthing/main.go
@@ -38,13 +38,14 @@ import (
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/model"
+ "github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
- "github.com/syncthing/syncthing/lib/upnp"
+ _ "github.com/syncthing/syncthing/lib/upnp"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
@@ -557,10 +558,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
- // We reinitialize the predictable RNG with our device ID, to get a
- // sequence that is always the same but unique to this syncthing instance.
- util.PredictableRandom.Seed(util.SeedFromBytes(cert.Certificate[0]))
-
myID = protocol.NewDeviceID(cert.Certificate[0])
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
@@ -709,26 +706,30 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
mainService.Add(m)
- // The default port we announce, possibly modified by setupUPnP next.
+ // 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)
+ }
- uri, err := url.Parse(opts.ListenAddress[0])
- if err != nil {
- l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], 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)
+ }
- addr, err := net.ResolveTCPAddr("tcp", uri.Host)
- if err != nil {
- l.Fatalln("Bad listen address:", err)
- }
- if addr.Port == 0 {
- l.Fatalf("Listen address %s: invalid port", uri)
- }
-
- // Start UPnP
- var upnpService *upnp.Service
- if opts.UPnPEnabled {
- upnpService = upnp.NewUPnPService(cfg, addr.Port)
- mainService.Add(upnpService)
+ mappings = append(mappings, natService.NewMapping(nat.TCP, addr.IP, addr.Port))
+ }
+ }
+ mainService.Add(natService)
}
// Start relay management
@@ -746,7 +747,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Start connection management
- connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, upnpService, relayService, bepProtocolName, tlsDefaultCommonName, lans)
+ connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, mappings, relayService, bepProtocolName, tlsDefaultCommonName, lans)
mainService.Add(connectionService)
if cfg.Options().GlobalAnnEnabled {
diff --git a/cmd/syncthing/verboseservice.go b/cmd/syncthing/verboseservice.go
index c1b5e442f..1852920d6 100644
--- a/cmd/syncthing/verboseservice.go
+++ b/cmd/syncthing/verboseservice.go
@@ -147,11 +147,13 @@ func (s *verboseService) formatEvent(ev events.Event) string {
data := ev.Data.(map[string]string)
device := data["device"]
return fmt.Sprintf("Device %v was resumed", device)
-
case events.ExternalPortMappingChanged:
- data := ev.Data.(map[string]int)
- port := data["port"]
- return fmt.Sprintf("External port mapping changed; new port is %d.", port)
+ data := ev.Data.(map[string]interface{})
+ protocol := data["protocol"]
+ local := data["local"]
+ added := data["added"]
+ removed := data["removed"]
+ 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"]
diff --git a/gui/default/syncthing/settings/settingsModalView.html b/gui/default/syncthing/settings/settingsModalView.html
index 810b2ffe3..3247805e2 100644
--- a/gui/default/syncthing/settings/settingsModalView.html
+++ b/gui/default/syncthing/settings/settingsModalView.html
@@ -40,7 +40,7 @@
diff --git a/lib/config/config.go b/lib/config/config.go
index 097c2182a..664b56517 100644
--- a/lib/config/config.go
+++ b/lib/config/config.go
@@ -242,6 +242,10 @@ func convertV12V13(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.
cfg.Options.CacheIgnoredFiles = false
+ cfg.Options.NATEnabled = cfg.Options.DeprecatedUPnPEnabled
+ cfg.Options.NATLeaseM = cfg.Options.DeprecatedUPnPLeaseM
+ cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM
+ cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS
cfg.Version = 13
}
diff --git a/lib/config/config_test.go b/lib/config/config_test.go
index 7edd65197..70241e851 100644
--- a/lib/config/config_test.go
+++ b/lib/config/config_test.go
@@ -44,10 +44,10 @@ func TestDefaultValues(t *testing.T) {
RelaysEnabled: true,
RelayReconnectIntervalM: 10,
StartBrowser: true,
- UPnPEnabled: true,
- UPnPLeaseM: 60,
- UPnPRenewalM: 30,
- UPnPTimeoutS: 10,
+ NATEnabled: true,
+ NATLeaseM: 60,
+ NATRenewalM: 30,
+ NATTimeoutS: 10,
RestartOnWakeup: true,
AutoUpgradeIntervalH: 12,
KeepTemporariesH: 24,
@@ -174,10 +174,10 @@ func TestOverriddenValues(t *testing.T) {
RelaysEnabled: false,
RelayReconnectIntervalM: 20,
StartBrowser: false,
- UPnPEnabled: false,
- UPnPLeaseM: 90,
- UPnPRenewalM: 15,
- UPnPTimeoutS: 15,
+ NATEnabled: false,
+ NATLeaseM: 90,
+ NATRenewalM: 15,
+ NATTimeoutS: 15,
RestartOnWakeup: false,
AutoUpgradeIntervalH: 24,
KeepTemporariesH: 48,
diff --git a/lib/config/optionsconfiguration.go b/lib/config/optionsconfiguration.go
index 783b43a3e..cfca3161d 100644
--- a/lib/config/optionsconfiguration.go
+++ b/lib/config/optionsconfiguration.go
@@ -20,10 +20,10 @@ type OptionsConfiguration struct {
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
- UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
- UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
- UPnPRenewalM int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
- UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
+ NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"`
+ NATLeaseM int `xml:"natLeaseMinutes" json:"natLeaseMinutes" default:"60"`
+ NATRenewalM int `xml:"natRenewalMinutes" json:"natRenewalMinutes" default:"30"`
+ NATTimeoutS int `xml:"natTimeoutSeconds" json:"natTimeoutSeconds" default:"10"`
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
@@ -40,6 +40,11 @@ type OptionsConfiguration struct {
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteNames bool `xml:"overwriteNames" json:"overwriteNames" default:"false"`
+
+ DeprecatedUPnPEnabled bool `xml:"upnpEnabled"`
+ DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes"`
+ DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes"`
+ DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds"`
}
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
diff --git a/lib/config/testdata/overridenvalues.xml b/lib/config/testdata/overridenvalues.xml
index ee8a83eb9..bc60ab399 100755
--- a/lib/config/testdata/overridenvalues.xml
+++ b/lib/config/testdata/overridenvalues.xml
@@ -17,10 +17,10 @@
20
true
false
- false
- 90
- 15
- 15
+ false
+ 90
+ 15
+ 15
false
24
48
diff --git a/lib/connections/connections.go b/lib/connections/connections.go
index 01dbe82e5..8452f5b6e 100644
--- a/lib/connections/connections.go
+++ b/lib/connections/connections.go
@@ -19,11 +19,12 @@ import (
"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/upnp"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
@@ -56,7 +57,7 @@ type Service struct {
tlsCfg *tls.Config
discoverer discover.Finder
conns chan model.IntermediateConnection
- upnpService *upnp.Service
+ mappings []*nat.Mapping
relayService relay.Service
bepProtocolName string
tlsDefaultCommonName string
@@ -71,7 +72,7 @@ type Service struct {
relaysEnabled bool
}
-func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, upnpService *upnp.Service,
+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"),
@@ -80,7 +81,7 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
- upnpService: upnpService,
+ mappings: mappings,
relayService: relayService,
conns: make(chan model.IntermediateConnection),
bepProtocolName: bepProtocolName,
@@ -140,6 +141,17 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
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
}
@@ -531,11 +543,10 @@ func (s *Service) addresses(includePrivateIPV4 bool) []string {
}
}
- // Get an external port mapping from the upnpService, if it has one. If so,
- // add it as another unspecified address.
- if s.upnpService != nil {
- if port := s.upnpService.ExternalPort(); port != 0 {
- addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
+ // 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))
}
}
diff --git a/lib/nat/debug.go b/lib/nat/debug.go
new file mode 100644
index 000000000..d8633b050
--- /dev/null
+++ b/lib/nat/debug.go
@@ -0,0 +1,22 @@
+// 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 nat
+
+import (
+ "os"
+ "strings"
+
+ "github.com/syncthing/syncthing/lib/logger"
+)
+
+var (
+ l = logger.DefaultLogger.NewFacility("nat", "NAT discovery and port mapping")
+)
+
+func init() {
+ l.SetDebug("nat", strings.Contains(os.Getenv("STTRACE"), "nat") || os.Getenv("STTRACE") == "all")
+}
diff --git a/lib/nat/interface.go b/lib/nat/interface.go
new file mode 100644
index 000000000..658a67b04
--- /dev/null
+++ b/lib/nat/interface.go
@@ -0,0 +1,26 @@
+// 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 nat
+
+import (
+ "net"
+ "time"
+)
+
+type Protocol string
+
+const (
+ TCP Protocol = "TCP"
+ UDP = "UDP"
+)
+
+type Device interface {
+ ID() string
+ GetLocalIPAddress() net.IP
+ AddPortMapping(protocol Protocol, internalPort, externalPort int, description string, duration time.Duration) (int, error)
+ GetExternalIPAddress() (net.IP, error)
+}
diff --git a/lib/nat/registry.go b/lib/nat/registry.go
new file mode 100644
index 000000000..bab3d3c19
--- /dev/null
+++ b/lib/nat/registry.go
@@ -0,0 +1,30 @@
+// 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 nat
+
+import (
+ "time"
+)
+
+type DiscoverFunc func(renewal, timeout time.Duration) []Device
+
+var providers []DiscoverFunc
+
+func Register(provider DiscoverFunc) {
+ providers = append(providers, provider)
+}
+
+func discoverAll(renewal, timeout time.Duration) map[string]Device {
+ nats := make(map[string]Device)
+ for _, discoverFunc := range providers {
+ discoveredNATs := discoverFunc(renewal, timeout)
+ for _, discoveredNAT := range discoveredNATs {
+ nats[discoveredNAT.ID()] = discoveredNAT
+ }
+ }
+ return nats
+}
diff --git a/lib/nat/service.go b/lib/nat/service.go
new file mode 100644
index 000000000..89e6844fe
--- /dev/null
+++ b/lib/nat/service.go
@@ -0,0 +1,298 @@
+// 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 nat
+
+import (
+ "fmt"
+ "math/rand"
+ "net"
+ stdsync "sync"
+ "time"
+
+ "github.com/syncthing/syncthing/lib/config"
+ "github.com/syncthing/syncthing/lib/protocol"
+ "github.com/syncthing/syncthing/lib/sync"
+)
+
+// Service runs a loop for discovery of IGDs (Internet Gateway Devices) and
+// setup/renewal of a port mapping.
+type Service struct {
+ id protocol.DeviceID
+ cfg *config.Wrapper
+ stop chan struct{}
+ immediate chan chan struct{}
+ timer *time.Timer
+ announce *stdsync.Once
+
+ mappings []*Mapping
+ mut sync.RWMutex
+}
+
+func NewService(id protocol.DeviceID, cfg *config.Wrapper) *Service {
+ return &Service{
+ id: id,
+ cfg: cfg,
+
+ immediate: make(chan chan struct{}),
+ timer: time.NewTimer(time.Second),
+
+ mut: sync.NewRWMutex(),
+ }
+}
+
+func (s *Service) Serve() {
+ s.timer.Reset(0)
+ s.stop = make(chan struct{})
+ s.announce = &stdsync.Once{}
+
+ for {
+ select {
+ case result := <-s.immediate:
+ s.process()
+ close(result)
+ case <-s.timer.C:
+ s.process()
+ case <-s.stop:
+ s.timer.Stop()
+ return
+ }
+ }
+}
+
+func (s *Service) process() {
+ // toRenew are mappings which are due for renewal
+ // 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
+ // actually need to perform a renewal.
+ var toRenew, toUpdate []*Mapping
+
+ renewIn := time.Duration(s.cfg.Options().NATRenewalM) * time.Minute
+ if renewIn == 0 {
+ // We always want to do renewal so lets just pick a nice sane number.
+ renewIn = 30 * time.Minute
+ }
+
+ s.mut.RLock()
+ for _, mapping := range s.mappings {
+ if mapping.expires.Before(time.Now()) {
+ toRenew = append(toRenew, mapping)
+ } else {
+ toUpdate = append(toUpdate, mapping)
+ mappingRenewIn := mapping.expires.Sub(time.Now())
+ if mappingRenewIn < renewIn {
+ renewIn = mappingRenewIn
+ }
+ }
+ }
+ s.mut.RUnlock()
+
+ s.timer.Reset(renewIn)
+
+ // Don't do anything, unless we really need to renew
+ if len(toRenew) == 0 {
+ return
+ }
+
+ 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 {
+ s.updateMapping(mapping, nats, true)
+ }
+
+ for _, mapping := range toUpdate {
+ s.updateMapping(mapping, nats, false)
+ }
+}
+
+func (s *Service) Stop() {
+ close(s.stop)
+}
+
+func (s *Service) NewMapping(protocol Protocol, ip net.IP, port int) *Mapping {
+ mapping := &Mapping{
+ protocol: protocol,
+ address: Address{
+ IP: ip,
+ Port: port,
+ },
+ extAddresses: make(map[string]Address),
+ mut: sync.NewRWMutex(),
+ }
+
+ s.mut.Lock()
+ s.mappings = append(s.mappings, mapping)
+ s.mut.Unlock()
+
+ return mapping
+}
+
+// Sync forces the service to recheck all mappings.
+func (s *Service) Sync() {
+ wait := make(chan struct{})
+ s.immediate <- wait
+ <-wait
+}
+
+// updateMapping compares the addresses of the existing mapping versus the natds
+// discovered, and removes any addresses of natds that do not exist, or tries to
+// acquire mappings for natds which the mapping was unaware of before.
+// Optionally takes renew flag which indicates whether or not we should renew
+// mappings with existing natds
+func (s *Service) updateMapping(mapping *Mapping, nats map[string]Device, renew bool) {
+ var added, removed []Address
+
+ renewalTime := time.Duration(s.cfg.Options().NATRenewalM) * time.Minute
+ mapping.expires = time.Now().Add(renewalTime)
+
+ newAdded, newRemoved := s.verifyExistingMappings(mapping, nats, renew)
+ added = append(added, newAdded...)
+ removed = append(removed, newRemoved...)
+
+ newAdded, newRemoved = s.acquireNewMappings(mapping, nats)
+ added = append(added, newAdded...)
+ removed = append(removed, newRemoved...)
+
+ if len(added) > 0 || len(removed) > 0 {
+ mapping.notify(added, removed)
+ }
+}
+
+func (s *Service) verifyExistingMappings(mapping *Mapping, nats map[string]Device, renew bool) ([]Address, []Address) {
+ var added, removed []Address
+
+ leaseTime := time.Duration(s.cfg.Options().NATLeaseM) * time.Minute
+
+ for id, address := range mapping.addressMap() {
+ // Delete addresses for NATDevice's that do not exist anymore
+ nat, ok := nats[id]
+ if !ok {
+ mapping.removeAddress(id)
+ removed = append(removed, address)
+ continue
+ } else if renew {
+ // Only perform renewals on the nat's that have the right local IP
+ // address
+ localIP := nat.GetLocalIPAddress()
+ if !mapping.validGateway(localIP) {
+ l.Debugf("Skipping %s for %s because of IP mismatch. %s != %s", id, mapping, mapping.address.IP, localIP)
+ continue
+ }
+
+ l.Debugf("Renewing %s -> %s mapping on %s", mapping, address, id)
+
+ addr, err := s.tryNATDevice(nat, mapping.address.Port, address.Port, leaseTime)
+ if err != nil {
+ l.Debugf("Failed to renew %s -> mapping on %s", mapping, address, id)
+ mapping.removeAddress(id)
+ removed = append(removed, address)
+ continue
+ }
+
+ l.Debugf("Renewed %s -> %s mapping on %s", mapping, address, id)
+
+ if !addr.Equal(address) {
+ mapping.removeAddress(id)
+ mapping.setAddress(id, addr)
+ removed = append(removed, address)
+ added = append(added, address)
+ }
+ }
+ }
+
+ return added, removed
+}
+
+func (s *Service) acquireNewMappings(mapping *Mapping, nats map[string]Device) ([]Address, []Address) {
+ var added, removed []Address
+
+ leaseTime := time.Duration(s.cfg.Options().NATLeaseM) * time.Minute
+ addrMap := mapping.addressMap()
+
+ for id, nat := range nats {
+ if _, ok := addrMap[id]; ok {
+ continue
+ }
+
+ // Only perform mappings on the nat's that have the right local IP
+ // address
+ localIP := nat.GetLocalIPAddress()
+ if !mapping.validGateway(localIP) {
+ l.Debugf("Skipping %s for %s because of IP mismatch. %s != %s", id, mapping, mapping.address.IP, localIP)
+ continue
+ }
+
+ l.Debugf("Acquiring %s mapping on %s", mapping, id)
+
+ addr, err := s.tryNATDevice(nat, mapping.address.Port, 0, leaseTime)
+ if err != nil {
+ l.Debugf("Failed to acquire %s mapping on %s", mapping, id)
+ continue
+ }
+
+ l.Debugf("Acquired %s -> %s mapping on %s", mapping, addr, id)
+
+ mapping.setAddress(id, addr)
+ added = append(added, addr)
+ }
+
+ return added, removed
+}
+
+// tryNATDevice tries to acquire a port mapping for the given internal address to
+// the given external port. If external port is 0, picks a pseudo-random port.
+func (s *Service) tryNATDevice(natd Device, intPort, extPort int, leaseTime time.Duration) (Address, error) {
+ var err error
+
+ // Generate a predictable random which is based on device ID + local port
+ // number so that the ports we'd try to acquire for the mapping would always
+ // be the same.
+ predictableRand := rand.New(rand.NewSource(int64(s.id.Short()) + int64(intPort)))
+
+ if extPort != 0 {
+ // First try renewing our existing mapping, if we have one.
+ name := fmt.Sprintf("syncthing-%d", extPort)
+ port, err := natd.AddPortMapping(TCP, intPort, extPort, name, leaseTime)
+ if err == nil {
+ extPort = port
+ goto findIP
+ }
+ l.Debugln("Error extending lease on", natd.ID(), err)
+ }
+
+ for i := 0; i < 10; i++ {
+ // Then try up to ten random ports.
+ extPort = 1024 + predictableRand.Intn(65535-1024)
+ name := fmt.Sprintf("syncthing-%d", extPort)
+ port, err := natd.AddPortMapping(TCP, intPort, extPort, name, leaseTime)
+ if err == nil {
+ extPort = port
+ goto findIP
+ }
+ l.Debugln("Error getting new lease on", natd.ID(), err)
+ }
+
+ return Address{}, err
+
+findIP:
+ ip, err := natd.GetExternalIPAddress()
+ if err != nil {
+ l.Debugln("Error getting external ip on", natd.ID(), err)
+ ip = nil
+ }
+ return Address{
+ IP: ip,
+ Port: extPort,
+ }, nil
+}
diff --git a/lib/nat/structs.go b/lib/nat/structs.go
new file mode 100644
index 000000000..bc9278c26
--- /dev/null
+++ b/lib/nat/structs.go
@@ -0,0 +1,129 @@
+// 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 nat
+
+import (
+ "fmt"
+ "net"
+ "time"
+
+ "github.com/syncthing/syncthing/lib/sync"
+)
+
+type MappingChangeSubscriber func(*Mapping, []Address, []Address)
+
+type Mapping struct {
+ protocol Protocol
+ address Address
+
+ extAddresses map[string]Address // NAT ID -> Address
+ expires time.Time
+ subscribers []MappingChangeSubscriber
+ mut sync.RWMutex
+}
+
+func (m *Mapping) setAddress(id string, address Address) {
+ m.mut.Lock()
+ if existing, ok := m.extAddresses[id]; !ok || !existing.Equal(address) {
+ l.Infof("New NAT port mapping: external %s address %s to local address %s.", m.protocol, address, m.address)
+ m.extAddresses[id] = address
+ }
+ m.mut.Unlock()
+}
+
+func (m *Mapping) removeAddress(id string) {
+ m.mut.Lock()
+ addr, ok := m.extAddresses[id]
+ if ok {
+ l.Infof("Removing NAT port mapping: external %s address %s, NAT %s is no longer available.", m.protocol, addr, id)
+ delete(m.extAddresses, id)
+ }
+ m.mut.Unlock()
+}
+
+func (m *Mapping) notify(added, removed []Address) {
+ m.mut.RLock()
+ for _, subscriber := range m.subscribers {
+ subscriber(m, added, removed)
+ }
+ m.mut.RUnlock()
+}
+
+func (m *Mapping) addressMap() map[string]Address {
+ m.mut.RLock()
+ addrMap := m.extAddresses
+ m.mut.RUnlock()
+ return addrMap
+}
+
+func (m *Mapping) Protocol() Protocol {
+ return m.protocol
+}
+
+func (m *Mapping) Address() Address {
+ return m.address
+}
+
+func (m *Mapping) ExternalAddresses() []Address {
+ m.mut.RLock()
+ addrs := make([]Address, 0, len(m.extAddresses))
+ for _, addr := range m.extAddresses {
+ addrs = append(addrs, addr)
+ }
+ m.mut.RUnlock()
+ return addrs
+}
+
+func (m *Mapping) OnChanged(subscribed MappingChangeSubscriber) {
+ m.mut.Lock()
+ m.subscribers = append(m.subscribers, subscribed)
+ m.mut.Unlock()
+}
+
+func (m *Mapping) String() string {
+ return fmt.Sprintf("%s %s", m.protocol, m.address)
+}
+
+func (m *Mapping) GoString() string {
+ return m.String()
+}
+
+// Checks if the mappings local IP address matches the IP address of the gateway
+// For example, if we are explicitly listening on 192.168.0.12, there is no
+// point trying to acquire a mapping on a gateway to which the local IP is
+// 10.0.0.1. Fallback to true if any of the IPs is not there.
+func (m *Mapping) validGateway(ip net.IP) bool {
+ if m.address.IP == nil || ip == nil || m.address.IP.IsUnspecified() || ip.IsUnspecified() {
+ return true
+ }
+ return m.address.IP.Equal(ip)
+}
+
+// Address is essentially net.TCPAddr yet is more general, and has a few helper
+// methods which reduce boilerplate code.
+type Address struct {
+ IP net.IP
+ Port int
+}
+
+func (a Address) Equal(b Address) bool {
+ return a.Port == b.Port && a.IP.Equal(b.IP)
+}
+
+func (a Address) String() string {
+ var ipStr string
+ if a.IP == nil {
+ ipStr = net.IPv4zero.String()
+ } else {
+ ipStr = a.IP.String()
+ }
+ return net.JoinHostPort(ipStr, fmt.Sprintf("%d", a.Port))
+}
+
+func (a Address) GoString() string {
+ return a.String()
+}
diff --git a/lib/nat/structs_test.go b/lib/nat/structs_test.go
new file mode 100644
index 000000000..3c2e201a8
--- /dev/null
+++ b/lib/nat/structs_test.go
@@ -0,0 +1,54 @@
+// 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 nat
+
+import (
+ "net"
+ "testing"
+)
+
+func TestMappingValidGateway(t *testing.T) {
+ a := net.ParseIP("10.0.0.1")
+ b := net.ParseIP("192.168.0.1")
+ tests := []struct {
+ mappingLocalIP net.IP
+ gatewayLocalIP net.IP
+ expected bool
+ }{
+ // Any of the IPs is nil or unspecified implies correct
+ {nil, nil, true},
+ {net.IPv4zero, net.IPv4zero, true},
+ {nil, net.IPv4zero, true},
+ {net.IPv4zero, nil, true},
+ {a, nil, true},
+ {b, nil, true},
+ {a, net.IPv4zero, true},
+ {b, net.IPv4zero, true},
+ {nil, a, true},
+ {nil, b, true},
+ {net.IPv4zero, a, true},
+ {net.IPv4zero, b, true},
+ // IPs are the same implies correct
+ {a, a, true},
+ {b, b, true},
+ // IPs are specified and different, implies incorrect
+ {a, b, false},
+ {b, a, false},
+ }
+
+ for _, test := range tests {
+ m := Mapping{
+ address: Address{
+ IP: test.mappingLocalIP,
+ },
+ }
+ result := m.validGateway(test.gatewayLocalIP)
+ if result != test.expected {
+ t.Errorf("Incorrect: local %s gateway %s result %t expected %t", test.mappingLocalIP, test.gatewayLocalIP, result, test.expected)
+ }
+ }
+}
diff --git a/lib/upnp/igd.go b/lib/upnp/igd.go
index 695015213..af8ab87e3 100644
--- a/lib/upnp/igd.go
+++ b/lib/upnp/igd.go
@@ -13,6 +13,9 @@ import (
"net"
"net/url"
"strings"
+ "time"
+
+ "github.com/syncthing/syncthing/lib/nat"
)
// An IGD is a UPnP InternetGatewayDevice.
@@ -24,7 +27,7 @@ type IGD struct {
localIPAddress net.IP
}
-func (n *IGD) UUID() string {
+func (n *IGD) ID() string {
return n.uuid
}
@@ -47,14 +50,14 @@ func (n *IGD) URL() *url.URL {
// if action is fails for _any_ of the relevant services. For this reason, it
// is generally better to configure port mapping for each individual service
// instead.
-func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
+func (n *IGD) AddPortMapping(protocol nat.Protocol, externalPort, internalPort int, description string, duration time.Duration) (int, error) {
for _, service := range n.services {
- err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout)
+ err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, duration)
if err != nil {
- return err
+ return externalPort, err
}
}
- return nil
+ return externalPort, nil
}
// DeletePortMapping deletes a port mapping from all relevant services on the
@@ -62,7 +65,7 @@ func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int,
// if action is fails for _any_ of the relevant services. For this reason, it
// is generally better to configure port mapping for each individual service
// instead.
-func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error {
+func (n *IGD) DeletePortMapping(protocol nat.Protocol, externalPort int) error {
for _, service := range n.services {
err := service.DeletePortMapping(protocol, externalPort)
if err != nil {
diff --git a/lib/upnp/igd_service.go b/lib/upnp/igd_service.go
index c9a1133be..a50f1aa5a 100644
--- a/lib/upnp/igd_service.go
+++ b/lib/upnp/igd_service.go
@@ -13,6 +13,9 @@ import (
"encoding/xml"
"fmt"
"net"
+ "time"
+
+ "github.com/syncthing/syncthing/lib/nat"
)
// An IGDService is a specific service provided by an IGD.
@@ -23,7 +26,7 @@ type IGDService struct {
}
// AddPortMapping adds a port mapping to the specified IGD service.
-func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
+func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol nat.Protocol, externalPort, internalPort int, description string, duration time.Duration) error {
tpl := `
%d
@@ -34,10 +37,10 @@ func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, ex
%s
%d
`
- body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
+ body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, duration/time.Second)
response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body)
- if err != nil && timeout > 0 {
+ if err != nil && duration > 0 {
// Try to repair error code 725 - OnlyPermanentLeasesSupported
envelope := &soapErrorResponse{}
if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil {
@@ -52,7 +55,7 @@ func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, ex
}
// DeletePortMapping deletes a port mapping from the specified IGD service.
-func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) error {
+func (s *IGDService) DeletePortMapping(protocol nat.Protocol, externalPort int) error {
tpl := `
%d
diff --git a/lib/upnp/service.go b/lib/upnp/service.go
deleted file mode 100644
index 7f0af4760..000000000
--- a/lib/upnp/service.go
+++ /dev/null
@@ -1,132 +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 upnp
-
-import (
- "fmt"
- "time"
-
- "github.com/syncthing/syncthing/lib/config"
- "github.com/syncthing/syncthing/lib/events"
- "github.com/syncthing/syncthing/lib/sync"
- "github.com/syncthing/syncthing/lib/util"
-)
-
-// Service runs a loop for discovery of IGDs (Internet Gateway Devices) and
-// setup/renewal of a port mapping.
-type Service struct {
- cfg *config.Wrapper
- localPort int
- extPort int
- extPortMut sync.Mutex
- stop chan struct{}
-}
-
-func NewUPnPService(cfg *config.Wrapper, localPort int) *Service {
- return &Service{
- cfg: cfg,
- localPort: localPort,
- extPortMut: sync.NewMutex(),
- }
-}
-
-func (s *Service) Serve() {
- foundIGD := true
- s.stop = make(chan struct{})
-
- for {
- igds := Discover(time.Duration(s.cfg.Options().UPnPTimeoutS) * time.Second)
- if len(igds) > 0 {
- foundIGD = true
- s.extPortMut.Lock()
- oldExtPort := s.extPort
- s.extPortMut.Unlock()
-
- newExtPort := s.tryIGDs(igds, oldExtPort)
-
- s.extPortMut.Lock()
- s.extPort = newExtPort
- s.extPortMut.Unlock()
- } else if foundIGD {
- // Only print a notice if we've previously found an IGD or this is
- // the first time around.
- foundIGD = false
- l.Infof("No UPnP device detected")
- }
-
- d := time.Duration(s.cfg.Options().UPnPRenewalM) * time.Minute
- if d == 0 {
- // We always want to do renewal so lets just pick a nice sane number.
- d = 30 * time.Minute
- }
-
- select {
- case <-s.stop:
- return
- case <-time.After(d):
- }
- }
-}
-
-func (s *Service) Stop() {
- close(s.stop)
-}
-
-func (s *Service) ExternalPort() int {
- s.extPortMut.Lock()
- port := s.extPort
- s.extPortMut.Unlock()
- return port
-}
-
-func (s *Service) tryIGDs(igds []IGD, prevExtPort int) int {
- // Lets try all the IGDs we found and use the first one that works.
- // TODO: Use all of them, and sort out the resulting mess to the
- // discovery announcement code...
- for _, igd := range igds {
- extPort, err := s.tryIGD(igd, prevExtPort)
- if err != nil {
- l.Warnf("Failed to set UPnP port mapping: external port %d on device %s.", extPort, igd.FriendlyIdentifier())
- continue
- }
-
- if extPort != prevExtPort {
- l.Infof("New UPnP port mapping: external port %d to local port %d.", extPort, s.localPort)
- events.Default.Log(events.ExternalPortMappingChanged, map[string]int{"port": extPort})
- }
- l.Debugf("Created/updated UPnP port mapping for external port %d on device %s.", extPort, igd.FriendlyIdentifier())
- return extPort
- }
-
- return 0
-}
-
-func (s *Service) tryIGD(igd IGD, suggestedPort int) (int, error) {
- var err error
- leaseTime := s.cfg.Options().UPnPLeaseM * 60
-
- if suggestedPort != 0 {
- // First try renewing our existing mapping.
- name := fmt.Sprintf("syncthing-%d", suggestedPort)
- err = igd.AddPortMapping(TCP, suggestedPort, s.localPort, name, leaseTime)
- if err == nil {
- return suggestedPort, nil
- }
- }
-
- for i := 0; i < 10; i++ {
- // Then try up to ten random ports.
- extPort := 1024 + util.PredictableRandom.Intn(65535-1024)
- name := fmt.Sprintf("syncthing-%d", extPort)
- err = igd.AddPortMapping(TCP, extPort, s.localPort, name, leaseTime)
- if err == nil {
- return extPort, nil
- }
- }
-
- return 0, err
-}
diff --git a/lib/upnp/upnp.go b/lib/upnp/upnp.go
index 7d1528297..cdc36b13e 100644
--- a/lib/upnp/upnp.go
+++ b/lib/upnp/upnp.go
@@ -26,15 +26,13 @@ import (
"time"
"github.com/syncthing/syncthing/lib/dialer"
+ "github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/sync"
)
-type Protocol string
-
-const (
- TCP Protocol = "TCP"
- UDP = "UDP"
-)
+func init() {
+ nat.Register(Discover)
+}
type upnpService struct {
ID string `xml:"serviceId"`
@@ -55,8 +53,8 @@ type upnpRoot struct {
// Discover discovers UPnP InternetGatewayDevices.
// The order in which the devices appear in the results list is not deterministic.
-func Discover(timeout time.Duration) []IGD {
- var results []IGD
+func Discover(renewal, timeout time.Duration) []nat.Device {
+ var results []nat.Device
interfaces, err := net.Interfaces()
if err != nil {
@@ -91,7 +89,7 @@ func Discover(timeout time.Duration) []IGD {
nextResult:
for result := range resultChan {
for _, existingResult := range results {
- if existingResult.uuid == result.uuid {
+ if existingResult.ID() == result.ID() {
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
for _, service := range result.services {
l.Debugf("* [%s] %s", service.ID, service.URL)
@@ -100,7 +98,7 @@ nextResult:
}
}
- results = append(results, result)
+ results = append(results, &result)
l.Debugf("UPnP discovery result %s with services:", result.uuid)
for _, service := range result.services {
l.Debugf("* [%s] %s", service.ID, service.URL)
diff --git a/lib/util/random.go b/lib/util/random.go
index ec086d2cc..fe77ecf4c 100644
--- a/lib/util/random.go
+++ b/lib/util/random.go
@@ -17,16 +17,6 @@ import (
// randomCharset contains the characters that can make up a randomString().
const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
-// PredictableRandom is an RNG that will always have the same sequence. It
-// will be seeded with the device ID during startup, so that the sequence is
-// predictable but varies between instances.
-var PredictableRandom = mathRand.New(mathRand.NewSource(42))
-
-func init() {
- // The default RNG should be seeded with something good.
- mathRand.Seed(RandomInt64())
-}
-
// RandomString returns a string of random characters (taken from
// randomCharset) of the specified length.
func RandomString(l int) string {
diff --git a/lib/util/random_test.go b/lib/util/random_test.go
index a63ab5ae9..8d6b5f710 100644
--- a/lib/util/random_test.go
+++ b/lib/util/random_test.go
@@ -6,26 +6,7 @@
package util
-import (
- "runtime"
- "sync"
- "testing"
-)
-
-var predictableRandomTest sync.Once
-
-func TestPredictableRandom(t *testing.T) {
- if runtime.GOARCH != "amd64" {
- t.Skip("Test only for 64 bit platforms; but if it works there, it should work on 32 bit")
- }
- predictableRandomTest.Do(func() {
- // predictable random sequence is predictable
- e := int64(3440579354231278675)
- if v := int64(PredictableRandom.Int()); v != e {
- t.Errorf("Unexpected random value %d != %d", v, e)
- }
- })
-}
+import "testing"
func TestSeedFromBytes(t *testing.T) {
// should always return the same seed for the same bytes