|
@@ -12,6 +12,7 @@ package connections
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
"crypto/tls"
|
|
"crypto/tls"
|
|
|
|
|
+ "crypto/x509"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
"math"
|
|
"math"
|
|
|
"net"
|
|
"net"
|
|
@@ -55,6 +56,13 @@ var (
|
|
|
errDisabled = fmt.Errorf("%w: disabled by configuration", errUnsupported)
|
|
errDisabled = fmt.Errorf("%w: disabled by configuration", errUnsupported)
|
|
|
errDeprecated = fmt.Errorf("%w: deprecated", errUnsupported)
|
|
errDeprecated = fmt.Errorf("%w: deprecated", errUnsupported)
|
|
|
errNotInBuild = fmt.Errorf("%w: disabled at build time", errUnsupported)
|
|
errNotInBuild = fmt.Errorf("%w: disabled at build time", errUnsupported)
|
|
|
|
|
+
|
|
|
|
|
+ // Various reasons to reject a connection
|
|
|
|
|
+ errNetworkNotAllowed = errors.New("network not allowed")
|
|
|
|
|
+ errDeviceAlreadyConnected = errors.New("already connected to this device")
|
|
|
|
|
+ errDeviceIgnored = errors.New("device is ignored")
|
|
|
|
|
+ errConnLimitReached = errors.New("connection limit reached")
|
|
|
|
|
+ errDevicePaused = errors.New("device is paused")
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
const (
|
|
@@ -128,6 +136,14 @@ type ConnectionStatusEntry struct {
|
|
|
Error *string `json:"error"`
|
|
Error *string `json:"error"`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type connWithHello struct {
|
|
|
|
|
+ c internalConn
|
|
|
|
|
+ hello protocol.Hello
|
|
|
|
|
+ err error
|
|
|
|
|
+ remoteID protocol.DeviceID
|
|
|
|
|
+ remoteCert *x509.Certificate
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
type service struct {
|
|
type service struct {
|
|
|
*suture.Supervisor
|
|
*suture.Supervisor
|
|
|
connectionStatusHandler
|
|
connectionStatusHandler
|
|
@@ -138,6 +154,7 @@ type service struct {
|
|
|
tlsCfg *tls.Config
|
|
tlsCfg *tls.Config
|
|
|
discoverer discover.Finder
|
|
discoverer discover.Finder
|
|
|
conns chan internalConn
|
|
conns chan internalConn
|
|
|
|
|
+ hellos chan *connWithHello
|
|
|
bepProtocolName string
|
|
bepProtocolName string
|
|
|
tlsDefaultCommonName string
|
|
tlsDefaultCommonName string
|
|
|
limiter *limiter
|
|
limiter *limiter
|
|
@@ -194,7 +211,8 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t
|
|
|
// incoming or outgoing.
|
|
// incoming or outgoing.
|
|
|
|
|
|
|
|
service.Add(svcutil.AsService(service.connect, fmt.Sprintf("%s/connect", service)))
|
|
service.Add(svcutil.AsService(service.connect, fmt.Sprintf("%s/connect", service)))
|
|
|
- service.Add(svcutil.AsService(service.handle, fmt.Sprintf("%s/handle", service)))
|
|
|
|
|
|
|
+ service.Add(svcutil.AsService(service.handleConns, fmt.Sprintf("%s/handleConns", service)))
|
|
|
|
|
+ service.Add(svcutil.AsService(service.handleHellos, fmt.Sprintf("%s/handleHellos", service)))
|
|
|
service.Add(service.natService)
|
|
service.Add(service.natService)
|
|
|
|
|
|
|
|
svcutil.OnSupervisorDone(service.Supervisor, func() {
|
|
svcutil.OnSupervisorDone(service.Supervisor, func() {
|
|
@@ -205,7 +223,7 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t
|
|
|
return service
|
|
return service
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (s *service) handle(ctx context.Context) error {
|
|
|
|
|
|
|
+func (s *service) handleConns(ctx context.Context) error {
|
|
|
var c internalConn
|
|
var c internalConn
|
|
|
for {
|
|
for {
|
|
|
select {
|
|
select {
|
|
@@ -245,8 +263,84 @@ func (s *service) handle(ctx context.Context) error {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if err := s.connectionCheckEarly(remoteID, c); err != nil {
|
|
|
|
|
+ l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type(), err)
|
|
|
|
|
+ c.Close()
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
_ = c.SetDeadline(time.Now().Add(20 * time.Second))
|
|
_ = c.SetDeadline(time.Now().Add(20 * time.Second))
|
|
|
- hello, err := protocol.ExchangeHello(c, s.model.GetHello(remoteID))
|
|
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ hello, err := protocol.ExchangeHello(c, s.model.GetHello(remoteID))
|
|
|
|
|
+ select {
|
|
|
|
|
+ case s.hellos <- &connWithHello{c, hello, err, remoteID, remoteCert}:
|
|
|
|
|
+ case <-ctx.Done():
|
|
|
|
|
+ }
|
|
|
|
|
+ }()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (s *service) connectionCheckEarly(remoteID protocol.DeviceID, c internalConn) error {
|
|
|
|
|
+ if s.cfg.IgnoredDevice(remoteID) {
|
|
|
|
|
+ return errDeviceIgnored
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if max := s.cfg.Options().ConnectionLimitMax; max > 0 && s.model.NumConnections() >= max {
|
|
|
|
|
+ // We're not allowed to accept any more connections.
|
|
|
|
|
+ return errConnLimitReached
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cfg, ok := s.cfg.Device(remoteID)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ // We do go ahead exchanging hello messages to get information about the device.
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if cfg.Paused {
|
|
|
|
|
+ return errDevicePaused
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if len(cfg.AllowedNetworks) > 0 && !IsAllowedNetwork(c.RemoteAddr().String(), cfg.AllowedNetworks) {
|
|
|
|
|
+ // The connection is not from an allowed network.
|
|
|
|
|
+ return errNetworkNotAllowed
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Lower priority is better, just like nice etc.
|
|
|
|
|
+ if ct, ok := s.model.Connection(remoteID); ok {
|
|
|
|
|
+ if ct.Priority() > c.priority || time.Since(ct.Statistics().StartedAt) > minConnectionReplaceAge {
|
|
|
|
|
+ l.Debugf("Switching connections %s (existing: %s new: %s)", remoteID, ct, c)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 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...
|
|
|
|
|
+ return errDeviceAlreadyConnected
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (s *service) handleHellos(ctx context.Context) error {
|
|
|
|
|
+ var c internalConn
|
|
|
|
|
+ var hello protocol.Hello
|
|
|
|
|
+ var err error
|
|
|
|
|
+ var remoteID protocol.DeviceID
|
|
|
|
|
+ var remoteCert *x509.Certificate
|
|
|
|
|
+ for {
|
|
|
|
|
+ select {
|
|
|
|
|
+ case <-ctx.Done():
|
|
|
|
|
+ return ctx.Err()
|
|
|
|
|
+ case withHello := <-s.hellos:
|
|
|
|
|
+ c = withHello.c
|
|
|
|
|
+ hello = withHello.hello
|
|
|
|
|
+ err = withHello.err
|
|
|
|
|
+ remoteID = withHello.remoteID
|
|
|
|
|
+ remoteCert = withHello.remoteCert
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
if protocol.IsVersionMismatch(err) {
|
|
if protocol.IsVersionMismatch(err) {
|
|
|
// The error will be a relatively user friendly description
|
|
// The error will be a relatively user friendly description
|
|
@@ -279,25 +373,6 @@ func (s *service) handle(ctx context.Context) error {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // If we have a relay connection, and the new incoming connection is
|
|
|
|
|
- // not a relay connection, we should drop that, and prefer this one.
|
|
|
|
|
- ct, connected := s.model.Connection(remoteID)
|
|
|
|
|
-
|
|
|
|
|
- // Lower priority is better, just like nice etc.
|
|
|
|
|
- if connected && (ct.Priority() > c.priority || time.Since(ct.Statistics().StartedAt) > minConnectionReplaceAge) {
|
|
|
|
|
- l.Debugf("Switching connections %s (existing: %s new: %s)", remoteID, ct, c)
|
|
|
|
|
- } else if connected {
|
|
|
|
|
- // 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 (existing: %s new: %s)", remoteID, ct, c)
|
|
|
|
|
- c.Close()
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
deviceCfg, ok := s.cfg.Device(remoteID)
|
|
deviceCfg, ok := s.cfg.Device(remoteID)
|
|
|
if !ok {
|
|
if !ok {
|
|
|
l.Infof("Device %s removed from config during connection attempt at %s", remoteID, c)
|
|
l.Infof("Device %s removed from config during connection attempt at %s", remoteID, c)
|
|
@@ -346,7 +421,6 @@ func (s *service) handle(ctx context.Context) error {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
func (s *service) connect(ctx context.Context) error {
|
|
func (s *service) connect(ctx context.Context) error {
|
|
|
// Map of when to earliest dial each given device + address again
|
|
// Map of when to earliest dial each given device + address again
|
|
|
nextDialAt := make(nextDialRegistry)
|
|
nextDialAt := make(nextDialRegistry)
|