lib/connections: Refactor address listing into connection service
This commit is contained in:
committed by
Jakob Borg
parent
690837dbe5
commit
29913dd1e4
@@ -23,6 +23,7 @@ import (
|
||||
"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/thejerf/suture"
|
||||
)
|
||||
@@ -42,9 +43,9 @@ type Model interface {
|
||||
IsPaused(remoteID protocol.DeviceID) bool
|
||||
}
|
||||
|
||||
// The connection connectionService listens on TLS and dials configured unconnected
|
||||
// devices. Successful connections are handed to the model.
|
||||
type connectionService struct {
|
||||
// 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
|
||||
@@ -52,6 +53,7 @@ type connectionService struct {
|
||||
tlsCfg *tls.Config
|
||||
discoverer discover.Finder
|
||||
conns chan model.IntermediateConnection
|
||||
upnpService *upnp.Service
|
||||
relayService relay.Service
|
||||
bepProtocolName string
|
||||
tlsDefaultCommonName string
|
||||
@@ -66,15 +68,16 @@ type connectionService struct {
|
||||
relaysEnabled bool
|
||||
}
|
||||
|
||||
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relayService relay.Service,
|
||||
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) suture.Service {
|
||||
service := &connectionService{
|
||||
Supervisor: suture.NewSimple("connectionService"),
|
||||
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, upnpService *upnp.Service,
|
||||
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,
|
||||
upnpService: upnpService,
|
||||
relayService: relayService,
|
||||
conns: make(chan model.IntermediateConnection),
|
||||
bepProtocolName: bepProtocolName,
|
||||
@@ -100,7 +103,7 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
|
||||
// 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 connectionService which handles incoming requests to connect
|
||||
// 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
|
||||
@@ -137,7 +140,7 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *connectionService) handle() {
|
||||
func (s *Service) handle() {
|
||||
next:
|
||||
for c := range s.conns {
|
||||
cs := c.Conn.ConnectionState()
|
||||
@@ -259,7 +262,7 @@ next:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionService) connect() {
|
||||
func (s *Service) connect() {
|
||||
delay := time.Second
|
||||
for {
|
||||
l.Debugln("Reconnect loop")
|
||||
@@ -342,7 +345,7 @@ func (s *connectionService) connect() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionService) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
|
||||
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 {
|
||||
@@ -358,7 +361,7 @@ func (s *connectionService) resolveAddresses(deviceID protocol.DeviceID, inAddrs
|
||||
return
|
||||
}
|
||||
|
||||
func (s *connectionService) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
|
||||
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)
|
||||
@@ -381,7 +384,7 @@ func (s *connectionService) connectDirect(deviceID protocol.DeviceID, addr strin
|
||||
return conn
|
||||
}
|
||||
|
||||
func (s *connectionService) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.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)
|
||||
@@ -420,7 +423,7 @@ func (s *connectionService) connectViaRelay(deviceID protocol.DeviceID, addr dis
|
||||
return tc
|
||||
}
|
||||
|
||||
func (s *connectionService) acceptRelayConns() {
|
||||
func (s *Service) acceptRelayConns() {
|
||||
for {
|
||||
conn := s.relayService.Accept()
|
||||
s.conns <- model.IntermediateConnection{
|
||||
@@ -430,7 +433,7 @@ func (s *connectionService) acceptRelayConns() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionService) shouldLimit(addr net.Addr) bool {
|
||||
func (s *Service) shouldLimit(addr net.Addr) bool {
|
||||
if s.cfg.Options().LimitBandwidthInLan {
|
||||
return true
|
||||
}
|
||||
@@ -447,11 +450,11 @@ func (s *connectionService) shouldLimit(addr net.Addr) bool {
|
||||
return !tcpaddr.IP.IsLoopback()
|
||||
}
|
||||
|
||||
func (s *connectionService) VerifyConfiguration(from, to config.Configuration) error {
|
||||
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *connectionService) CommitConfiguration(from, to config.Configuration) bool {
|
||||
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
s.mut.Lock()
|
||||
s.relaysEnabled = to.Options.RelaysEnabled
|
||||
s.mut.Unlock()
|
||||
@@ -472,6 +475,106 @@ func (s *connectionService) CommitConfiguration(from, to config.Configuration) b
|
||||
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("tcp", 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, tcpAddr(addr.String()))
|
||||
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
|
||||
// A public address; include as is.
|
||||
addrs = append(addrs, tcpAddr(addr.String()))
|
||||
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
|
||||
// A private IPv4 address.
|
||||
addrs = append(addrs, tcpAddr(addr.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))
|
||||
}
|
||||
}
|
||||
|
||||
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 tcpAddr(host string) string {
|
||||
u := url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: host,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// serviceFunc wraps a function to create a suture.Service without stop
|
||||
// functionality.
|
||||
type serviceFunc func()
|
||||
|
||||
132
lib/upnp/service.go
Normal file
132
lib/upnp/service.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// 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
|
||||
}
|
||||
59
lib/util/random.go
Normal file
59
lib/util/random.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
cryptoRand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
mathRand "math/rand"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
bs := make([]byte, l)
|
||||
for i := range bs {
|
||||
bs[i] = randomCharset[mathRand.Intn(len(randomCharset))]
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
// RandomInt64 returns a strongly random int64, slowly
|
||||
func RandomInt64() int64 {
|
||||
var bs [8]byte
|
||||
_, err := io.ReadFull(cryptoRand.Reader, bs[:])
|
||||
if err != nil {
|
||||
panic("randomness failure: " + err.Error())
|
||||
}
|
||||
return SeedFromBytes(bs[:])
|
||||
}
|
||||
|
||||
// SeedFromBytes calculates a weak 64 bit hash from the given byte slice,
|
||||
// suitable for use a predictable random seed.
|
||||
func SeedFromBytes(bs []byte) int64 {
|
||||
h := md5.New()
|
||||
h.Write(bs)
|
||||
s := h.Sum(nil)
|
||||
// The MD5 hash of the byte slice is 16 bytes long. We interpret it as two
|
||||
// uint64s and XOR them together.
|
||||
return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
|
||||
}
|
||||
82
lib/util/random_test.go
Normal file
82
lib/util/random_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSeedFromBytes(t *testing.T) {
|
||||
// should always return the same seed for the same bytes
|
||||
tcs := []struct {
|
||||
bs []byte
|
||||
v int64
|
||||
}{
|
||||
{[]byte("hello world"), -3639725434188061933},
|
||||
{[]byte("hello worlx"), -2539100776074091088},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
if v := SeedFromBytes(tc.bs); v != tc.v {
|
||||
t.Errorf("Unexpected seed value %d != %d", v, tc.v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
for _, l := range []int{0, 1, 2, 3, 4, 8, 42} {
|
||||
s := RandomString(l)
|
||||
if len(s) != l {
|
||||
t.Errorf("Incorrect length %d != %d", len(s), l)
|
||||
}
|
||||
}
|
||||
|
||||
strings := make([]string, 1000)
|
||||
for i := range strings {
|
||||
strings[i] = RandomString(8)
|
||||
for j := range strings {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
if strings[i] == strings[j] {
|
||||
t.Errorf("Repeated random string %q", strings[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomInt64(t *testing.T) {
|
||||
ints := make([]int64, 1000)
|
||||
for i := range ints {
|
||||
ints[i] = RandomInt64()
|
||||
for j := range ints {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
if ints[i] == ints[j] {
|
||||
t.Errorf("Repeated random int64 %d", ints[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user