Archived
Merge remote-tracking branch 'syncthing/pr/1995'
* syncthing/pr/1995: Add switch to disable relays Do not start relay service unless explicitly asked for, or global announcement server is running Add dynamic relay lookup (DDoS relays.syncthing.net!) Discovery clients now take an announcer, global discovery is delayed Expose connection type and relay status in the UI Add dependencies (fixes #1364) Check relays for available devices Add incoming connection relay service Add unsubscribe to config Connections have types Large refactoring/feature commit
This commit is contained in:
commit
c87faace6b
37 files changed
+2493
-457
No files matched your search
Generated
+8
@@ -41,6 +41,14 @@
|
|||||||
"ImportPath": "github.com/syncthing/protocol",
|
"ImportPath": "github.com/syncthing/protocol",
|
||||||
"Rev": "ebcdea63c07327a342f65415bbadc497462b8f1f"
|
"Rev": "ebcdea63c07327a342f65415bbadc497462b8f1f"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/syncthing/relaysrv/client",
|
||||||
|
"Rev": "7c6a31017968e7c1a69148db1ca3dea71eba8236"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/syncthing/relaysrv/protocol",
|
||||||
|
"Rev": "7c6a31017968e7c1a69148db1ca3dea71eba8236"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||||
"Rev": "b743d92d3215f11c9b5ce8830fafe1f16786adf4"
|
"Rev": "b743d92d3215f11c9b5ce8830fafe1f16786adf4"
|
||||||
|
|||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
syncthingprotocol "github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/relaysrv/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtocolClient struct {
|
||||||
|
URI *url.URL
|
||||||
|
Invitations chan protocol.SessionInvitation
|
||||||
|
|
||||||
|
closeInvitationsOnFinish bool
|
||||||
|
|
||||||
|
config *tls.Config
|
||||||
|
|
||||||
|
timeout time.Duration
|
||||||
|
|
||||||
|
stop chan struct{}
|
||||||
|
stopped chan struct{}
|
||||||
|
|
||||||
|
conn *tls.Conn
|
||||||
|
|
||||||
|
mut sync.RWMutex
|
||||||
|
connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient {
|
||||||
|
closeInvitationsOnFinish := false
|
||||||
|
if invitations == nil {
|
||||||
|
closeInvitationsOnFinish = true
|
||||||
|
invitations = make(chan protocol.SessionInvitation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProtocolClient{
|
||||||
|
URI: uri,
|
||||||
|
Invitations: invitations,
|
||||||
|
|
||||||
|
closeInvitationsOnFinish: closeInvitationsOnFinish,
|
||||||
|
|
||||||
|
config: configForCerts(certs),
|
||||||
|
|
||||||
|
timeout: time.Minute * 2,
|
||||||
|
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
stopped: make(chan struct{}),
|
||||||
|
|
||||||
|
mut: sync.NewRWMutex(),
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) Serve() {
|
||||||
|
c.stop = make(chan struct{})
|
||||||
|
c.stopped = make(chan struct{})
|
||||||
|
defer close(c.stopped)
|
||||||
|
|
||||||
|
if err := c.connect(); err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Relay connect:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
l.Debugln(c, "connected", c.conn.RemoteAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.join(); err != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
l.Infoln("Relay join:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.conn.SetDeadline(time.Time{}); err != nil {
|
||||||
|
l.Infoln("Relay set deadline:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
l.Debugln(c, "joined", c.conn.RemoteAddr(), "via", c.conn.LocalAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer c.cleanup()
|
||||||
|
c.mut.Lock()
|
||||||
|
c.connected = true
|
||||||
|
c.mut.Unlock()
|
||||||
|
|
||||||
|
messages := make(chan interface{})
|
||||||
|
errors := make(chan error, 1)
|
||||||
|
|
||||||
|
go messageReader(c.conn, messages, errors)
|
||||||
|
|
||||||
|
timeout := time.NewTimer(c.timeout)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-messages:
|
||||||
|
timeout.Reset(c.timeout)
|
||||||
|
if debug {
|
||||||
|
log.Printf("%s received message %T", c, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := message.(type) {
|
||||||
|
case protocol.Ping:
|
||||||
|
if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil {
|
||||||
|
l.Infoln("Relay write:", err)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
l.Debugln(c, "sent pong")
|
||||||
|
}
|
||||||
|
|
||||||
|
case protocol.SessionInvitation:
|
||||||
|
ip := net.IP(msg.Address)
|
||||||
|
if len(ip) == 0 || ip.IsUnspecified() {
|
||||||
|
msg.Address = c.conn.RemoteAddr().(*net.TCPAddr).IP[:]
|
||||||
|
}
|
||||||
|
c.Invitations <- msg
|
||||||
|
|
||||||
|
default:
|
||||||
|
l.Infoln("Relay: protocol error: unexpected message %v", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-c.stop:
|
||||||
|
if debug {
|
||||||
|
l.Debugln(c, "stopping")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case err := <-errors:
|
||||||
|
l.Infoln("Relay received:", err)
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-timeout.C:
|
||||||
|
if debug {
|
||||||
|
l.Debugln(c, "timed out")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) Stop() {
|
||||||
|
if c.stop == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(c.stop)
|
||||||
|
<-c.stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) StatusOK() bool {
|
||||||
|
c.mut.RLock()
|
||||||
|
con := c.connected
|
||||||
|
c.mut.RUnlock()
|
||||||
|
return con
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) String() string {
|
||||||
|
return fmt.Sprintf("ProtocolClient@%p", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) connect() error {
|
||||||
|
if c.URI.Scheme != "relay" {
|
||||||
|
return fmt.Errorf("Unsupported relay schema:", c.URI.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.Dial("tcp", c.URI.Host, c.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := performHandshakeAndValidation(conn, c.URI); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn = conn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) cleanup() {
|
||||||
|
if c.closeInvitationsOnFinish {
|
||||||
|
close(c.Invitations)
|
||||||
|
c.Invitations = make(chan protocol.SessionInvitation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
l.Debugln(c, "cleaning up")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mut.Lock()
|
||||||
|
c.connected = false
|
||||||
|
c.mut.Unlock()
|
||||||
|
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtocolClient) join() error {
|
||||||
|
if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := protocol.ReadMessage(c.conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := message.(type) {
|
||||||
|
case protocol.Response:
|
||||||
|
if msg.Code != 0 {
|
||||||
|
return fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("protocol error: expecting response got %v", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func performHandshakeAndValidation(conn *tls.Conn, uri *url.URL) error {
|
||||||
|
if err := conn.Handshake(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := conn.ConnectionState()
|
||||||
|
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != protocol.ProtocolName {
|
||||||
|
return fmt.Errorf("protocol negotiation error")
|
||||||
|
}
|
||||||
|
|
||||||
|
q := uri.Query()
|
||||||
|
relayIDs := q.Get("id")
|
||||||
|
if relayIDs != "" {
|
||||||
|
relayID, err := syncthingprotocol.DeviceIDFromString(relayIDs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("relay address contains invalid verification id: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certs := cs.PeerCertificates
|
||||||
|
if cl := len(certs); cl != 1 {
|
||||||
|
return fmt.Errorf("unexpected certificate count: %d", cl)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteID := syncthingprotocol.NewDeviceID(certs[0].Raw)
|
||||||
|
if remoteID != relayID {
|
||||||
|
return fmt.Errorf("relay id does not match. Expected %v got %v", relayID, remoteID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
|
||||||
|
for {
|
||||||
|
msg, err := protocol.ReadMessage(conn)
|
||||||
|
if err != nil {
|
||||||
|
errors <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messages <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/calmh/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debug = strings.Contains(os.Getenv("STTRACE"), "relay") || os.Getenv("STTRACE") == "all"
|
||||||
|
l = logger.DefaultLogger
|
||||||
|
)
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
syncthingprotocol "github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/relaysrv/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetInvitationFromRelay(uri *url.URL, id syncthingprotocol.DeviceID, certs []tls.Certificate) (protocol.SessionInvitation, error) {
|
||||||
|
if uri.Scheme != "relay" {
|
||||||
|
return protocol.SessionInvitation{}, fmt.Errorf("Unsupported relay scheme:", uri.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs))
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return protocol.SessionInvitation{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := performHandshakeAndValidation(conn, uri); err != nil {
|
||||||
|
return protocol.SessionInvitation{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
request := protocol.ConnectRequest{
|
||||||
|
ID: id[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := protocol.WriteMessage(conn, request); err != nil {
|
||||||
|
return protocol.SessionInvitation{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := protocol.ReadMessage(conn)
|
||||||
|
if err != nil {
|
||||||
|
return protocol.SessionInvitation{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := message.(type) {
|
||||||
|
case protocol.Response:
|
||||||
|
return protocol.SessionInvitation{}, fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
|
||||||
|
case protocol.SessionInvitation:
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Received invitation", msg, "via", conn.LocalAddr())
|
||||||
|
}
|
||||||
|
ip := net.IP(msg.Address)
|
||||||
|
if len(ip) == 0 || ip.IsUnspecified() {
|
||||||
|
msg.Address = conn.RemoteAddr().(*net.TCPAddr).IP[:]
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
default:
|
||||||
|
return protocol.SessionInvitation{}, fmt.Errorf("protocol error: unexpected message %v", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JoinSession(invitation protocol.SessionInvitation) (net.Conn, error) {
|
||||||
|
addr := net.JoinHostPort(net.IP(invitation.Address).String(), strconv.Itoa(int(invitation.Port)))
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request := protocol.JoinSessionRequest{
|
||||||
|
Key: invitation.Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
err = protocol.WriteMessage(conn, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := protocol.ReadMessage(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Time{})
|
||||||
|
|
||||||
|
switch msg := message.(type) {
|
||||||
|
case protocol.Response:
|
||||||
|
if msg.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("protocol error: expecting response got %v", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configForCerts(certs []tls.Certificate) *tls.Config {
|
||||||
|
return &tls.Config{
|
||||||
|
Certificates: certs,
|
||||||
|
NextProtos: []string{protocol.ProtocolName},
|
||||||
|
ClientAuth: tls.RequestClientCert,
|
||||||
|
SessionTicketsDisabled: true,
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||||
|
|
||||||
|
//go:generate -command genxdr go run ../../syncthing/Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||||
|
//go:generate genxdr -o packets_xdr.go packets.go
|
||||||
|
|
||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
syncthingprotocol "github.com/syncthing/protocol"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
messageTypePing int32 = iota
|
||||||
|
messageTypePong
|
||||||
|
messageTypeJoinRelayRequest
|
||||||
|
messageTypeJoinSessionRequest
|
||||||
|
messageTypeResponse
|
||||||
|
messageTypeConnectRequest
|
||||||
|
messageTypeSessionInvitation
|
||||||
|
)
|
||||||
|
|
||||||
|
type header struct {
|
||||||
|
magic uint32
|
||||||
|
messageType int32
|
||||||
|
messageLength int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ping struct{}
|
||||||
|
type Pong struct{}
|
||||||
|
type JoinRelayRequest struct{}
|
||||||
|
|
||||||
|
type JoinSessionRequest struct {
|
||||||
|
Key []byte // max:32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Code int32
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectRequest struct {
|
||||||
|
ID []byte // max:32
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionInvitation struct {
|
||||||
|
From []byte // max:32
|
||||||
|
Key []byte // max:32
|
||||||
|
Address []byte // max:32
|
||||||
|
Port uint16
|
||||||
|
ServerSocket bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i SessionInvitation) String() string {
|
||||||
|
return fmt.Sprintf("%s@%s", syncthingprotocol.DeviceIDFromBytes(i.From), i.AddressString())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i SessionInvitation) GoString() string {
|
||||||
|
return i.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i SessionInvitation) AddressString() string {
|
||||||
|
return fmt.Sprintf("%s:%d", net.IP(i.Address), i.Port)
|
||||||
|
}
|
||||||
+567
@@ -0,0 +1,567 @@
|
|||||||
|
// ************************************************************
|
||||||
|
// This file is automatically generated by genxdr. Do not edit.
|
||||||
|
// ************************************************************
|
||||||
|
|
||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/calmh/xdr"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
header 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| magic |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| message Type |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| message Length |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct header {
|
||||||
|
unsigned int magic;
|
||||||
|
int messageType;
|
||||||
|
int messageLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o header) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o header) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o header) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o header) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o header) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
xw.WriteUint32(o.magic)
|
||||||
|
xw.WriteUint32(uint32(o.messageType))
|
||||||
|
xw.WriteUint32(uint32(o.messageLength))
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *header) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *header) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *header) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
o.magic = xr.ReadUint32()
|
||||||
|
o.messageType = int32(xr.ReadUint32())
|
||||||
|
o.messageLength = int32(xr.ReadUint32())
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Ping 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct Ping {
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o Ping) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Ping) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Ping) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Ping) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Ping) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Ping) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Ping) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Ping) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Pong 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct Pong {
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o Pong) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Pong) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Pong) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Pong) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Pong) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Pong) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Pong) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Pong) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
JoinRelayRequest 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct JoinRelayRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o JoinRelayRequest) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinRelayRequest) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinRelayRequest) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinRelayRequest) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinRelayRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *JoinRelayRequest) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *JoinRelayRequest) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *JoinRelayRequest) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
JoinSessionRequest 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Key |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Key (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct JoinSessionRequest {
|
||||||
|
opaque Key<32>;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o JoinSessionRequest) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinSessionRequest) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinSessionRequest) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinSessionRequest) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o JoinSessionRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
if l := len(o.Key); l > 32 {
|
||||||
|
return xw.Tot(), xdr.ElementSizeExceeded("Key", l, 32)
|
||||||
|
}
|
||||||
|
xw.WriteBytes(o.Key)
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *JoinSessionRequest) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *JoinSessionRequest) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *JoinSessionRequest) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
o.Key = xr.ReadBytesMax(32)
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Response 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Code |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Message |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Message (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct Response {
|
||||||
|
int Code;
|
||||||
|
string Message<>;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o Response) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Response) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Response) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Response) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Response) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
xw.WriteUint32(uint32(o.Code))
|
||||||
|
xw.WriteString(o.Message)
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Response) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Response) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Response) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
o.Code = int32(xr.ReadUint32())
|
||||||
|
o.Message = xr.ReadString()
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
ConnectRequest 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of ID |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ ID (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct ConnectRequest {
|
||||||
|
opaque ID<32>;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o ConnectRequest) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o ConnectRequest) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o ConnectRequest) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o ConnectRequest) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o ConnectRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
if l := len(o.ID); l > 32 {
|
||||||
|
return xw.Tot(), xdr.ElementSizeExceeded("ID", l, 32)
|
||||||
|
}
|
||||||
|
xw.WriteBytes(o.ID)
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ConnectRequest) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ConnectRequest) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ConnectRequest) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
o.ID = xr.ReadBytesMax(32)
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
SessionInvitation 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of From |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ From (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Key |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Key (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Address |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Address (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| 0x0000 | Port |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Server Socket (V=0 or 1) |V|
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct SessionInvitation {
|
||||||
|
opaque From<32>;
|
||||||
|
opaque Key<32>;
|
||||||
|
opaque Address<32>;
|
||||||
|
unsigned int Port;
|
||||||
|
bool ServerSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o SessionInvitation) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o SessionInvitation) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o SessionInvitation) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o SessionInvitation) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o SessionInvitation) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
if l := len(o.From); l > 32 {
|
||||||
|
return xw.Tot(), xdr.ElementSizeExceeded("From", l, 32)
|
||||||
|
}
|
||||||
|
xw.WriteBytes(o.From)
|
||||||
|
if l := len(o.Key); l > 32 {
|
||||||
|
return xw.Tot(), xdr.ElementSizeExceeded("Key", l, 32)
|
||||||
|
}
|
||||||
|
xw.WriteBytes(o.Key)
|
||||||
|
if l := len(o.Address); l > 32 {
|
||||||
|
return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 32)
|
||||||
|
}
|
||||||
|
xw.WriteBytes(o.Address)
|
||||||
|
xw.WriteUint16(o.Port)
|
||||||
|
xw.WriteBool(o.ServerSocket)
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SessionInvitation) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SessionInvitation) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SessionInvitation) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
o.From = xr.ReadBytesMax(32)
|
||||||
|
o.Key = xr.ReadBytesMax(32)
|
||||||
|
o.Address = xr.ReadBytesMax(32)
|
||||||
|
o.Port = xr.ReadUint16()
|
||||||
|
o.ServerSocket = xr.ReadBool()
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||||
|
|
||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
magic = 0x9E79BC40
|
||||||
|
ProtocolName = "bep-relay"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ResponseSuccess = Response{0, "success"}
|
||||||
|
ResponseNotFound = Response{1, "not found"}
|
||||||
|
ResponseAlreadyConnected = Response{2, "already connected"}
|
||||||
|
ResponseInternalError = Response{99, "internal error"}
|
||||||
|
ResponseUnexpectedMessage = Response{100, "unexpected message"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteMessage(w io.Writer, message interface{}) error {
|
||||||
|
header := header{
|
||||||
|
magic: magic,
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch msg := message.(type) {
|
||||||
|
case Ping:
|
||||||
|
payload, err = msg.MarshalXDR()
|
||||||
|
header.messageType = messageTypePing
|
||||||
|
case Pong:
|
||||||
|
payload, err = msg.MarshalXDR()
|
||||||
|
header.messageType = messageTypePong
|
||||||
|
case JoinRelayRequest:
|
||||||
|
payload, err = msg.MarshalXDR()
|
||||||
|
header.messageType = messageTypeJoinRelayRequest
|
||||||
|
case JoinSessionRequest:
|
||||||
|
payload, err = msg.MarshalXDR()
|
||||||
|
header.messageType = messageTypeJoinSessionRequest
|
||||||
|
case Response:
|
||||||
|
payload, err = msg.MarshalXDR()
|
||||||
|
header.messageType = messageTypeResponse
|
||||||
|
case ConnectRequest:
|
||||||
|
payload, err = msg.MarshalXDR()
|
||||||
|
header.messageType = messageTypeConnectRequest
|
||||||
|
case SessionInvitation:
|
||||||
|
payload, err = msg.MarshalXDR()
|
||||||
|
header.messageType = messageTypeSessionInvitation
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Unknown message type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header.messageLength = int32(len(payload))
|
||||||
|
|
||||||
|
headerpayload, err := header.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(append(headerpayload, payload...))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadMessage(r io.Reader) (interface{}, error) {
|
||||||
|
var header header
|
||||||
|
if err := header.DecodeXDR(r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if header.magic != magic {
|
||||||
|
return nil, fmt.Errorf("magic mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch header.messageType {
|
||||||
|
case messageTypePing:
|
||||||
|
var msg Ping
|
||||||
|
err := msg.DecodeXDR(r)
|
||||||
|
return msg, err
|
||||||
|
case messageTypePong:
|
||||||
|
var msg Pong
|
||||||
|
err := msg.DecodeXDR(r)
|
||||||
|
return msg, err
|
||||||
|
case messageTypeJoinRelayRequest:
|
||||||
|
var msg JoinRelayRequest
|
||||||
|
err := msg.DecodeXDR(r)
|
||||||
|
return msg, err
|
||||||
|
case messageTypeJoinSessionRequest:
|
||||||
|
var msg JoinSessionRequest
|
||||||
|
err := msg.DecodeXDR(r)
|
||||||
|
return msg, err
|
||||||
|
case messageTypeResponse:
|
||||||
|
var msg Response
|
||||||
|
err := msg.DecodeXDR(r)
|
||||||
|
return msg, err
|
||||||
|
case messageTypeConnectRequest:
|
||||||
|
var msg ConnectRequest
|
||||||
|
err := msg.DecodeXDR(r)
|
||||||
|
return msg, err
|
||||||
|
case messageTypeSessionInvitation:
|
||||||
|
var msg SessionInvitation
|
||||||
|
err := msg.DecodeXDR(r)
|
||||||
|
return msg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unknown message type")
|
||||||
|
}
|
||||||
@@ -21,11 +21,11 @@ func main() {
|
|||||||
|
|
||||||
var server string
|
var server string
|
||||||
|
|
||||||
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22026", "Announce server")
|
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22027", "Announce server")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(flag.Args()) != 1 || server == "" {
|
if len(flag.Args()) != 1 || server == "" {
|
||||||
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22026\"] <device>", os.Args[0])
|
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22027\"] <device>", os.Args[0])
|
||||||
os.Exit(64)
|
os.Exit(64)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +35,13 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil)
|
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil, nil)
|
||||||
discoverer.StartGlobal([]string{server}, 1)
|
discoverer.StartGlobal([]string{server}, 1)
|
||||||
for _, addr := range discoverer.Lookup(id) {
|
addresses, relays := discoverer.Lookup(id)
|
||||||
log.Println(addr)
|
for _, addr := range addresses {
|
||||||
|
log.Println("address:", addr)
|
||||||
|
}
|
||||||
|
for _, addr := range relays {
|
||||||
|
log.Println("relay:", addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+193
-146
@@ -11,16 +11,28 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"net/url"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/protocol"
|
||||||
|
"github.com/syncthing/relaysrv/client"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
|
||||||
"github.com/thejerf/suture"
|
"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)
|
||||||
|
)
|
||||||
|
|
||||||
// The connection service listens on TLS and dials configured unconnected
|
// The connection service listens on TLS and dials configured unconnected
|
||||||
// devices. Successful connections are handed to the model.
|
// devices. Successful connections are handed to the model.
|
||||||
type connectionSvc struct {
|
type connectionSvc struct {
|
||||||
@@ -29,48 +41,36 @@ type connectionSvc struct {
|
|||||||
myID protocol.DeviceID
|
myID protocol.DeviceID
|
||||||
model *model.Model
|
model *model.Model
|
||||||
tlsCfg *tls.Config
|
tlsCfg *tls.Config
|
||||||
conns chan *tls.Conn
|
conns chan model.IntermediateConnection
|
||||||
|
|
||||||
|
lastRelayCheck map[protocol.DeviceID]time.Time
|
||||||
|
|
||||||
|
mut sync.RWMutex
|
||||||
|
connType map[protocol.DeviceID]model.ConnectionType
|
||||||
|
relaysEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.Model, tlsCfg *tls.Config) *connectionSvc {
|
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config) *connectionSvc {
|
||||||
svc := &connectionSvc{
|
svc := &connectionSvc{
|
||||||
Supervisor: suture.NewSimple("connectionSvc"),
|
Supervisor: suture.NewSimple("connectionSvc"),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
myID: myID,
|
myID: myID,
|
||||||
model: model,
|
model: mdl,
|
||||||
tlsCfg: tlsCfg,
|
tlsCfg: tlsCfg,
|
||||||
conns: make(chan *tls.Conn),
|
conns: make(chan model.IntermediateConnection),
|
||||||
|
|
||||||
|
connType: make(map[protocol.DeviceID]model.ConnectionType),
|
||||||
|
relaysEnabled: cfg.Options().RelaysEnabled,
|
||||||
|
lastRelayCheck: make(map[protocol.DeviceID]time.Time),
|
||||||
}
|
}
|
||||||
|
cfg.Subscribe(svc)
|
||||||
|
|
||||||
// There are several moving parts here; one routine per listening address
|
// There are several moving parts here; one routine per listening address
|
||||||
// to handle incoming connections, one routine to periodically attempt
|
// to handle incoming connections, one routine to periodically attempt
|
||||||
// outgoing connections, and lastly one routine to the the common handling
|
// outgoing connections, one routine to the the common handling
|
||||||
// regardless of whether the connection was incoming or outgoing. It ends
|
// regardless of whether the connection was incoming or outgoing.
|
||||||
// up as in the diagram below. We embed a Supervisor to manage the
|
// Furthermore, a relay service which handles incoming requests to connect
|
||||||
// routines (i.e. log and restart if they crash or exit, etc).
|
// via the relays.
|
||||||
//
|
|
||||||
// +-----------------+
|
|
||||||
// Incoming | +---------------+-+ +-----------------+
|
|
||||||
// Connections | | | | | Outgoing
|
|
||||||
// -------------->| | svc.listen | | | Connections
|
|
||||||
// | | (1 per listen | | svc.connect |-------------->
|
|
||||||
// | | address) | | |
|
|
||||||
// +-+ | | |
|
|
||||||
// +-----------------+ +-----------------+
|
|
||||||
// v v
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// +------------+-----------+
|
|
||||||
// |
|
|
||||||
// | svc.conns
|
|
||||||
// v
|
|
||||||
// +-----------------+
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | svc.handle |------> model.AddConnection()
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// +-----------------+
|
|
||||||
//
|
//
|
||||||
// TODO: Clean shutdown, and/or handling config changes on the fly. We
|
// 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
|
// partly do this now - new devices and addresses will be picked up, but
|
||||||
@@ -79,11 +79,25 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.
|
|||||||
|
|
||||||
svc.Add(serviceFunc(svc.connect))
|
svc.Add(serviceFunc(svc.connect))
|
||||||
for _, addr := range svc.cfg.Options().ListenAddress {
|
for _, addr := range svc.cfg.Options().ListenAddress {
|
||||||
addr := addr
|
uri, err := url.Parse(addr)
|
||||||
listener := serviceFunc(func() {
|
if err != nil {
|
||||||
svc.listen(addr)
|
l.Infoln("Failed to parse listen address:", addr, err)
|
||||||
})
|
continue
|
||||||
svc.Add(listener)
|
}
|
||||||
|
|
||||||
|
listener, ok := listeners[uri.Scheme]
|
||||||
|
if !ok {
|
||||||
|
l.Infoln("Unknown listen address scheme:", uri.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugNet {
|
||||||
|
l.Debugln("listening on", uri.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.Add(serviceFunc(func() {
|
||||||
|
listener(uri, svc.tlsCfg, svc.conns)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
svc.Add(serviceFunc(svc.handle))
|
svc.Add(serviceFunc(svc.handle))
|
||||||
|
|
||||||
@@ -92,15 +106,15 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.
|
|||||||
|
|
||||||
func (s *connectionSvc) handle() {
|
func (s *connectionSvc) handle() {
|
||||||
next:
|
next:
|
||||||
for conn := range s.conns {
|
for c := range s.conns {
|
||||||
cs := conn.ConnectionState()
|
cs := c.Conn.ConnectionState()
|
||||||
|
|
||||||
// We should have negotiated the next level protocol "bep/1.0" as part
|
// 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,
|
// of the TLS handshake. Unfortunately this can't be a hard error,
|
||||||
// because there are implementations out there that don't support
|
// because there are implementations out there that don't support
|
||||||
// protocol negotiation (iOS for one...).
|
// protocol negotiation (iOS for one...).
|
||||||
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != bepProtocolName {
|
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != bepProtocolName {
|
||||||
l.Infof("Peer %s did not negotiate bep/1.0", conn.RemoteAddr())
|
l.Infof("Peer %s did not negotiate bep/1.0", c.Conn.RemoteAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should have received exactly one certificate from the other
|
// We should have received exactly one certificate from the other
|
||||||
@@ -108,8 +122,8 @@ next:
|
|||||||
// connection.
|
// connection.
|
||||||
certs := cs.PeerCertificates
|
certs := cs.PeerCertificates
|
||||||
if cl := len(certs); cl != 1 {
|
if cl := len(certs); cl != 1 {
|
||||||
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, conn.RemoteAddr())
|
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.Conn.RemoteAddr())
|
||||||
conn.Close()
|
c.Conn.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
remoteCert := certs[0]
|
remoteCert := certs[0]
|
||||||
@@ -120,19 +134,29 @@ next:
|
|||||||
// clients between the same NAT gateway, and global discovery.
|
// clients between the same NAT gateway, and global discovery.
|
||||||
if remoteID == myID {
|
if remoteID == myID {
|
||||||
l.Infof("Connected to myself (%s) - should not happen", remoteID)
|
l.Infof("Connected to myself (%s) - should not happen", remoteID)
|
||||||
conn.Close()
|
c.Conn.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should not already be connected to the other party. TODO: This
|
// If we have a relay connection, and the new incoming connection is
|
||||||
// could use some better handling. If the old connection is dead but
|
// not a relay connection, we should drop that, and prefer the this one.
|
||||||
// hasn't timed out yet we may want to drop *that* connection and keep
|
s.mut.RLock()
|
||||||
// this one. But in case we are two devices connecting to each other
|
ct, ok := s.connType[remoteID]
|
||||||
// in parallel we don't want to do that or we end up with no
|
s.mut.RUnlock()
|
||||||
// connections still established...
|
if ok && !ct.IsDirect() && c.ConnType.IsDirect() {
|
||||||
if s.model.ConnectedTo(remoteID) {
|
if debugNet {
|
||||||
|
l.Debugln("Switching connections", remoteID)
|
||||||
|
}
|
||||||
|
s.model.Close(remoteID, fmt.Errorf("switching connections"))
|
||||||
|
} 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)
|
l.Infof("Connected to already connected device (%s)", remoteID)
|
||||||
conn.Close()
|
c.Conn.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,35 +174,42 @@ next:
|
|||||||
// Incorrect certificate name is something the user most
|
// Incorrect certificate name is something the user most
|
||||||
// likely wants to know about, since it's an advanced
|
// likely wants to know about, since it's an advanced
|
||||||
// config. Warn instead of Info.
|
// config. Warn instead of Info.
|
||||||
l.Warnf("Bad certificate from %s (%v): %v", remoteID, conn.RemoteAddr(), err)
|
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.Conn.RemoteAddr(), err)
|
||||||
conn.Close()
|
c.Conn.Close()
|
||||||
continue next
|
continue next
|
||||||
}
|
}
|
||||||
|
|
||||||
// If rate limiting is set, and based on the address we should
|
// If rate limiting is set, and based on the address we should
|
||||||
// limit the connection, then we wrap it in a limiter.
|
// limit the connection, then we wrap it in a limiter.
|
||||||
|
|
||||||
limit := s.shouldLimit(conn.RemoteAddr())
|
limit := s.shouldLimit(c.Conn.RemoteAddr())
|
||||||
|
|
||||||
wr := io.Writer(conn)
|
wr := io.Writer(c.Conn)
|
||||||
if limit && writeRateLimit != nil {
|
if limit && writeRateLimit != nil {
|
||||||
wr = &limitedWriter{conn, writeRateLimit}
|
wr = &limitedWriter{c.Conn, writeRateLimit}
|
||||||
}
|
}
|
||||||
|
|
||||||
rd := io.Reader(conn)
|
rd := io.Reader(c.Conn)
|
||||||
if limit && readRateLimit != nil {
|
if limit && readRateLimit != nil {
|
||||||
rd = &limitedReader{conn, readRateLimit}
|
rd = &limitedReader{c.Conn, readRateLimit}
|
||||||
}
|
}
|
||||||
|
|
||||||
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
name := fmt.Sprintf("%s-%s (%s)", c.Conn.LocalAddr(), c.Conn.RemoteAddr(), c.ConnType)
|
||||||
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
||||||
|
|
||||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||||
if debugNet {
|
if debugNet {
|
||||||
l.Debugf("cipher suite: %04X in lan: %t", conn.ConnectionState().CipherSuite, !limit)
|
l.Debugf("cipher suite: %04X in lan: %t", c.Conn.ConnectionState().CipherSuite, !limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.model.AddConnection(conn, protoConn)
|
s.model.AddConnection(model.Connection{
|
||||||
|
c.Conn,
|
||||||
|
protoConn,
|
||||||
|
c.ConnType,
|
||||||
|
})
|
||||||
|
s.mut.Lock()
|
||||||
|
s.connType[remoteID] = c.ConnType
|
||||||
|
s.mut.Unlock()
|
||||||
continue next
|
continue next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,54 +217,14 @@ next:
|
|||||||
if !s.cfg.IgnoredDevice(remoteID) {
|
if !s.cfg.IgnoredDevice(remoteID) {
|
||||||
events.Default.Log(events.DeviceRejected, map[string]string{
|
events.Default.Log(events.DeviceRejected, map[string]string{
|
||||||
"device": remoteID.String(),
|
"device": remoteID.String(),
|
||||||
"address": conn.RemoteAddr().String(),
|
"address": c.Conn.RemoteAddr().String(),
|
||||||
})
|
})
|
||||||
l.Infof("Connection from %s with unknown device ID %s", conn.RemoteAddr(), remoteID)
|
l.Infof("Connection from %s (%s) with unknown device ID %s", c.Conn.RemoteAddr(), c.ConnType, remoteID)
|
||||||
} else {
|
} else {
|
||||||
l.Infof("Connection from %s with ignored device ID %s", conn.RemoteAddr(), remoteID)
|
l.Infof("Connection from %s (%s) with ignored device ID %s", c.Conn.RemoteAddr(), c.ConnType, remoteID)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Close()
|
c.Conn.Close()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) listen(addr string) {
|
|
||||||
if debugNet {
|
|
||||||
l.Debugln("listening on", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
tcaddr, err := net.ResolveTCPAddr("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln("listen (BEP):", err)
|
|
||||||
}
|
|
||||||
listener, err := net.ListenTCP("tcp", tcaddr)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln("listen (BEP):", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Accepting connection:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if debugNet {
|
|
||||||
l.Debugln("connect from", conn.RemoteAddr())
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpConn := conn.(*net.TCPConn)
|
|
||||||
s.setTCPOptions(tcpConn)
|
|
||||||
|
|
||||||
tc := tls.Server(conn, s.tlsCfg)
|
|
||||||
err = tc.Handshake()
|
|
||||||
if err != nil {
|
|
||||||
l.Infoln("TLS handshake:", err)
|
|
||||||
tc.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.conns <- tc
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,19 +237,24 @@ func (s *connectionSvc) connect() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.model.ConnectedTo(deviceID) {
|
connected := s.model.ConnectedTo(deviceID)
|
||||||
|
|
||||||
|
s.mut.RLock()
|
||||||
|
ct, ok := s.connType[deviceID]
|
||||||
|
relaysEnabled := s.relaysEnabled
|
||||||
|
s.mut.RUnlock()
|
||||||
|
if connected && ok && ct.IsDirect() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var addrs []string
|
var addrs []string
|
||||||
|
var relays []string
|
||||||
for _, addr := range deviceCfg.Addresses {
|
for _, addr := range deviceCfg.Addresses {
|
||||||
if addr == "dynamic" {
|
if addr == "dynamic" {
|
||||||
if discoverer != nil {
|
if discoverer != nil {
|
||||||
t := discoverer.Lookup(deviceID)
|
t, r := discoverer.Lookup(deviceID)
|
||||||
if len(t) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addrs = append(addrs, t...)
|
addrs = append(addrs, t...)
|
||||||
|
relays = append(relays, r...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addrs = append(addrs, addr)
|
addrs = append(addrs, addr)
|
||||||
@@ -266,45 +262,108 @@ func (s *connectionSvc) connect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
host, port, err := net.SplitHostPort(addr)
|
uri, err := url.Parse(addr)
|
||||||
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
|
if err != nil {
|
||||||
// addr is on the form "1.2.3.4"
|
l.Infoln("Failed to parse connection url:", addr, err)
|
||||||
addr = net.JoinHostPort(addr, "22000")
|
continue
|
||||||
} else if err == nil && port == "" {
|
|
||||||
// addr is on the form "1.2.3.4:"
|
|
||||||
addr = net.JoinHostPort(host, "22000")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialer, ok := dialers[uri.Scheme]
|
||||||
|
if !ok {
|
||||||
|
l.Infoln("Unknown address schema", uri.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if debugNet {
|
if debugNet {
|
||||||
l.Debugln("dial", deviceCfg.DeviceID, addr)
|
l.Debugln("dial", deviceCfg.DeviceID, uri.String())
|
||||||
}
|
}
|
||||||
|
conn, err := dialer(uri, s.tlsCfg)
|
||||||
raddr, err := net.ResolveTCPAddr("tcp", addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debugNet {
|
if debugNet {
|
||||||
l.Debugln(err)
|
l.Debugln("dial failed", deviceCfg.DeviceID, uri.String(), err)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.DialTCP("tcp", nil, raddr)
|
if connected {
|
||||||
|
s.model.Close(deviceID, fmt.Errorf("switching connections"))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conns <- model.IntermediateConnection{
|
||||||
|
conn, model.ConnectionTypeBasicDial,
|
||||||
|
}
|
||||||
|
continue nextDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
continue nextDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
reconIntv := time.Duration(s.cfg.Options().RelayReconnectIntervalM) * time.Minute
|
||||||
|
if last, ok := s.lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv {
|
||||||
|
if debugNet {
|
||||||
|
l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last)
|
||||||
|
}
|
||||||
|
continue nextDevice
|
||||||
|
} else if debugNet {
|
||||||
|
l.Debugln("Trying relay connections to", deviceID, relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastRelayCheck[deviceID] = time.Now()
|
||||||
|
|
||||||
|
for _, addr := range relays {
|
||||||
|
uri, err := url.Parse(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debugNet {
|
l.Infoln("Failed to parse relay connection url:", addr, err)
|
||||||
l.Debugln(err)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
s.setTCPOptions(conn)
|
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates)
|
||||||
|
if err != nil {
|
||||||
|
if debugNet {
|
||||||
|
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if debugNet {
|
||||||
|
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
|
||||||
|
}
|
||||||
|
|
||||||
tc := tls.Client(conn, s.tlsCfg)
|
conn, err := client.JoinSession(inv)
|
||||||
|
if err != nil {
|
||||||
|
if debugNet {
|
||||||
|
l.Debugf("Failed to join relay session %s: %v", inv, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if debugNet {
|
||||||
|
l.Debugln("Sucessfully joined relay session", inv)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tc *tls.Conn
|
||||||
|
|
||||||
|
if inv.ServerSocket {
|
||||||
|
tc = tls.Server(conn, s.tlsCfg)
|
||||||
|
} else {
|
||||||
|
tc = tls.Client(conn, s.tlsCfg)
|
||||||
|
}
|
||||||
err = tc.Handshake()
|
err = tc.Handshake()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Infoln("TLS handshake:", err)
|
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
|
||||||
tc.Close()
|
tc.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
s.conns <- model.IntermediateConnection{
|
||||||
s.conns <- tc
|
tc, model.ConnectionTypeRelayDial,
|
||||||
|
}
|
||||||
continue nextDevice
|
continue nextDevice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,22 +376,6 @@ func (s *connectionSvc) connect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*connectionSvc) setTCPOptions(conn *net.TCPConn) {
|
|
||||||
var err error
|
|
||||||
if err = conn.SetLinger(0); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
if err = conn.SetNoDelay(false); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
if err = conn.SetKeepAlive(true); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
||||||
if s.cfg.Options().LimitBandwidthInLan {
|
if s.cfg.Options().LimitBandwidthInLan {
|
||||||
return true
|
return true
|
||||||
@@ -355,6 +398,10 @@ func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
|
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
|
||||||
|
s.mut.Lock()
|
||||||
|
s.relaysEnabled = to.Options.RelaysEnabled
|
||||||
|
s.mut.Unlock()
|
||||||
|
|
||||||
// We require a restart if a device as been removed.
|
// We require a restart if a device as been removed.
|
||||||
|
|
||||||
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dialers["tcp"] = tcpDialer
|
||||||
|
listeners["tcp"] = tcpListener
|
||||||
|
}
|
||||||
|
|
||||||
|
func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
raddr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
|
if err != nil {
|
||||||
|
if debugNet {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialTCP("tcp", nil, raddr)
|
||||||
|
if err != nil {
|
||||||
|
if debugNet {
|
||||||
|
l.Debugln(err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = osutil.SetTCPOptions(conn)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc := tls.Client(conn, tlsCfg)
|
||||||
|
err = tc.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
tc.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) {
|
||||||
|
tcaddr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("listen (BEP/tcp):", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listener, err := net.ListenTCP("tcp", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugNet {
|
||||||
|
l.Debugln("connect from", conn.RemoteAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = osutil.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{
|
||||||
|
tc, model.ConnectionTypeBasicAccept,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -628,6 +628,9 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
if cfg.Options().GlobalAnnEnabled && discoverer != nil {
|
if cfg.Options().GlobalAnnEnabled && discoverer != nil {
|
||||||
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
||||||
}
|
}
|
||||||
|
if relaySvc != nil {
|
||||||
|
res["relayClientStatus"] = relaySvc.ClientStatus()
|
||||||
|
}
|
||||||
cpuUsageLock.RLock()
|
cpuUsageLock.RLock()
|
||||||
var cpusum float64
|
var cpusum float64
|
||||||
for _, p := range cpuUsagePercent {
|
for _, p := range cpuUsagePercent {
|
||||||
|
|||||||
+31
-12
@@ -34,8 +34,10 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay"
|
||||||
"github.com/syncthing/syncthing/lib/symlinks"
|
"github.com/syncthing/syncthing/lib/symlinks"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
@@ -110,6 +112,7 @@ var (
|
|||||||
readRateLimit *ratelimit.Bucket
|
readRateLimit *ratelimit.Bucket
|
||||||
stop = make(chan int)
|
stop = make(chan int)
|
||||||
discoverer *discover.Discoverer
|
discoverer *discover.Discoverer
|
||||||
|
relaySvc *relay.Svc
|
||||||
cert tls.Certificate
|
cert tls.Certificate
|
||||||
lans []*net.IPNet
|
lans []*net.IPNet
|
||||||
)
|
)
|
||||||
@@ -627,7 +630,7 @@ func syncthingMain() {
|
|||||||
m.StartDeadlockDetector(time.Duration(it) * time.Second)
|
m.StartDeadlockDetector(time.Duration(it) * time.Second)
|
||||||
}
|
}
|
||||||
} else if !IsRelease || IsBeta {
|
} else if !IsRelease || IsBeta {
|
||||||
m.StartDeadlockDetector(20 * 60 * time.Second)
|
m.StartDeadlockDetector(20 * time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out old indexes for other devices. Otherwise we'll start up and
|
// Clear out old indexes for other devices. Otherwise we'll start up and
|
||||||
@@ -658,15 +661,30 @@ func syncthingMain() {
|
|||||||
|
|
||||||
// The default port we announce, possibly modified by setupUPnP next.
|
// The default port we announce, possibly modified by setupUPnP next.
|
||||||
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp", opts.ListenAddress[0])
|
uri, err := url.Parse(opts.ListenAddress[0])
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Bad listen address:", err)
|
l.Fatalln("Bad listen address:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the relevant services
|
||||||
|
|
||||||
|
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg)
|
||||||
|
mainSvc.Add(connectionSvc)
|
||||||
|
|
||||||
|
if opts.RelaysEnabled && (opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn) {
|
||||||
|
relaySvc = relay.NewSvc(cfg, tlsCfg, connectionSvc.conns)
|
||||||
|
connectionSvc.Add(relaySvc)
|
||||||
|
}
|
||||||
|
|
||||||
// Start discovery
|
// Start discovery
|
||||||
|
|
||||||
localPort := addr.Port
|
localPort := addr.Port
|
||||||
discoverer = discovery(localPort)
|
discoverer = discovery(localPort, relaySvc)
|
||||||
|
|
||||||
// Start UPnP. The UPnP service will restart global discovery if the
|
// Start UPnP. The UPnP service will restart global discovery if the
|
||||||
// external port changes.
|
// external port changes.
|
||||||
@@ -676,10 +694,6 @@ func syncthingMain() {
|
|||||||
mainSvc.Add(upnpSvc)
|
mainSvc.Add(upnpSvc)
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg)
|
|
||||||
cfg.Subscribe(connectionSvc)
|
|
||||||
mainSvc.Add(connectionSvc)
|
|
||||||
|
|
||||||
if cpuProfile {
|
if cpuProfile {
|
||||||
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
|
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -900,18 +914,23 @@ func shutdown() {
|
|||||||
stop <- exitSuccess
|
stop <- exitSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
func discovery(extPort int) *discover.Discoverer {
|
func discovery(extPort int, relaySvc *relay.Svc) *discover.Discoverer {
|
||||||
opts := cfg.Options()
|
opts := cfg.Options()
|
||||||
disc := discover.NewDiscoverer(myID, opts.ListenAddress)
|
disc := discover.NewDiscoverer(myID, opts.ListenAddress, relaySvc)
|
||||||
|
|
||||||
if opts.LocalAnnEnabled {
|
if opts.LocalAnnEnabled {
|
||||||
l.Infoln("Starting local discovery announcements")
|
l.Infoln("Starting local discovery announcements")
|
||||||
disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr)
|
disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.GlobalAnnEnabled {
|
if opts.GlobalAnnEnabled {
|
||||||
l.Infoln("Starting global discovery announcements")
|
go func() {
|
||||||
disc.StartGlobal(opts.GlobalAnnServers, uint16(extPort))
|
// Defer starting global announce server, giving time to connect
|
||||||
|
// to relay servers.
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
l.Infoln("Starting global discovery announcements")
|
||||||
|
disc.StartGlobal(opts.GlobalAnnServers, uint16(extPort))
|
||||||
|
}()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return disc
|
return disc
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
|||||||
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
|
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
|
||||||
case events.DeviceConnected:
|
case events.DeviceConnected:
|
||||||
data := ev.Data.(map[string]string)
|
data := ev.Data.(map[string]string)
|
||||||
return fmt.Sprintf("Connected to device %v at %v", data["id"], data["addr"])
|
return fmt.Sprintf("Connected to device %v at %v (type %s)", data["id"], data["addr"], data["type"])
|
||||||
case events.DeviceDisconnected:
|
case events.DeviceDisconnected:
|
||||||
data := ev.Data.(map[string]string)
|
data := ev.Data.(map[string]string)
|
||||||
return fmt.Sprintf("Disconnected from device %v", data["id"])
|
return fmt.Sprintf("Disconnected from device %v", data["id"])
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"Edit Folder": "Edit Folder",
|
"Edit Folder": "Edit Folder",
|
||||||
"Editing": "Editing",
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Enable UPnP",
|
"Enable UPnP": "Enable UPnP",
|
||||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.",
|
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||||
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"External File Versioning": "External File Versioning",
|
"External File Versioning": "External File Versioning",
|
||||||
@@ -120,6 +120,8 @@
|
|||||||
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "RAM Utilization",
|
"RAM Utilization": "RAM Utilization",
|
||||||
"Random": "Random",
|
"Random": "Random",
|
||||||
|
"Relayed via": "Relayed via",
|
||||||
|
"Relays": "Relays",
|
||||||
"Release Notes": "Release Notes",
|
"Release Notes": "Release Notes",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
"Rescan": "Rescan",
|
"Rescan": "Rescan",
|
||||||
|
|||||||
+18
-1
@@ -385,6 +385,19 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="system.relayClientStatus != undefined && relayClientsTotal > 0">
|
||||||
|
<th><span class="fa fa-fw fa-sitemap"></span> <span translate>Relays</span></th>
|
||||||
|
<td class="text-right">
|
||||||
|
<span ng-if="relayClientsFailed.length == 0" class="data text-success">
|
||||||
|
<span>OK</span>
|
||||||
|
</span>
|
||||||
|
<span ng-if="relayClientsFailed.length != 0" class="data" ng-class="{'text-danger': relayClientsFailed.length == relayClientsTotal}">
|
||||||
|
<span popover data-trigger="hover" data-placement="bottom" data-content="{{relayClientsFailed.join('\n')}}">
|
||||||
|
{{relayClientsTotal-relayClientsFailed.length}}/{{relayClientsTotal}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="fa fa-fw fa-clock-o"></span> <span translate>Uptime</span></th>
|
<th><span class="fa fa-fw fa-clock-o"></span> <span translate>Uptime</span></th>
|
||||||
<td class="text-right">{{system.uptime | duration:"m"}}</td>
|
<td class="text-right">{{system.uptime | duration:"m"}}</td>
|
||||||
@@ -430,7 +443,11 @@
|
|||||||
<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>
|
||||||
<th><span class="fa fa-fw fa-link"></span> <span translate>Address</span></th>
|
<th>
|
||||||
|
<span class="fa fa-fw fa-link"></span>
|
||||||
|
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('basic') == 0" >Address</span>
|
||||||
|
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span>
|
||||||
|
</th>
|
||||||
<td class="text-right">{{deviceAddr(deviceCfg)}}</td>
|
<td class="text-right">{{deviceAddr(deviceCfg)}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="deviceCfg.compression != 'metadata'">
|
<tr ng-if="deviceCfg.compression != 'metadata'">
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ angular.module('syncthing.core')
|
|||||||
outbps: 0,
|
outbps: 0,
|
||||||
inBytesTotal: 0,
|
inBytesTotal: 0,
|
||||||
outBytesTotal: 0,
|
outBytesTotal: 0,
|
||||||
|
type: arg.data.type,
|
||||||
address: arg.data.addr
|
address: arg.data.addr
|
||||||
};
|
};
|
||||||
$scope.completion[arg.data.id] = {
|
$scope.completion[arg.data.id] = {
|
||||||
@@ -346,14 +347,24 @@ angular.module('syncthing.core')
|
|||||||
$http.get(urlbase + '/system/status').success(function (data) {
|
$http.get(urlbase + '/system/status').success(function (data) {
|
||||||
$scope.myID = data.myID;
|
$scope.myID = data.myID;
|
||||||
$scope.system = data;
|
$scope.system = data;
|
||||||
|
|
||||||
$scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
|
$scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
|
||||||
var failed = [];
|
var failedAnnounce = [];
|
||||||
for (var server in data.extAnnounceOK) {
|
for (var server in data.extAnnounceOK) {
|
||||||
if (!data.extAnnounceOK[server]) {
|
if (!data.extAnnounceOK[server]) {
|
||||||
failed.push(server);
|
failedAnnounce.push(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$scope.announceServersFailed = failed;
|
$scope.announceServersFailed = failedAnnounce;
|
||||||
|
|
||||||
|
$scope.relayClientsTotal = data.relayClientStatus ? Object.keys(data.relayClientStatus).length : 0;
|
||||||
|
var failedRelays = [];
|
||||||
|
for (var relay in data.relayClientStatus) {
|
||||||
|
if (!data.relayClientStatus[relay]) {
|
||||||
|
failedRelays.push(relay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.relayClientsFailed = failedRelays;
|
||||||
|
|
||||||
|
|
||||||
console.log("refreshSystem", data);
|
console.log("refreshSystem", data);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="addresses">Addresses</label>
|
<label translate for="addresses">Addresses</label>
|
||||||
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
||||||
<p translate class="help-block">Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
|
<p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate>Compression</label>
|
<label translate>Compression</label>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AssetsBuildDate = "Tue, 18 Aug 2015 11:29:36 GMT"
|
AssetsBuildDate = "Wed, 19 Aug 2015 20:10:12 GMT"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Assets() map[string][]byte {
|
func Assets() map[string][]byte {
|
||||||
@@ -24,7 +24,7 @@ func Assets() map[string][]byte {
|
|||||||
assets["assets/lang/lang-de.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-de.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/lang/lang-el.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-el.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/lang/lang-en-GB.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-en-GB.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/lang/lang-en.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-en.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/lang/lang-es-ES.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-es-ES.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/lang/lang-es.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-es.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/lang/lang-fi.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["assets/lang/lang-fi.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
@@ -50,7 +50,7 @@ func Assets() map[string][]byte {
|
|||||||
assets["assets/lang/lang-zh-TW.json"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/8R7W3MUR5b/+3yK+hNB/KUIRsvM7syDH9bhscczhAebtWAndoOIjVJXtlRLd1VPVTWyRqGNFqC7Wg22JJCQ0QUhGgldAIOELhCxH2W369JP8xX2nDxZWVnVLY3Xa+/6wbQyT95OnsvvnJM1+DMN/jv3kWaxXt0zbzLNKhd7mKPZec3QB1zNsJlr/X9PK+o3mOYyy2Vd5z7QzjUOKn5lKnq5Ei7e8defBXMHjYONxkHVvzse3h39z8qtcxeSmfth9L/ajnaTOa5pW/DXgGbZntbDtJxdLMG6PQWm9Zten1Zy2E3TLrsxrctX+59PEu/myiXtMzbA54SfzbF7zXt7srPHLnvY1by/FMwfy+ach3NgR/BNtXGyJDsMAxv9tUf+5IrSqH0Cy+dY0hc9/jY82VEpPrULBnMUildjwfyCv/5OJcIz5znhh3zx+T0glpQfKqQOc/n+GidV/9tKpoO17bqpWznG9y9/Z/q0j20rb/aWHR3Pr1JmeuJxhYL2ie7p/FQj9ebtOu1V6bf7tY8s2xoo4uVcc/Vepn3JSrbjmVbvhzRuOKq/8Kff+3erIFmNk/fhLE4T1Sf9h4/8lRd/OV5O5iv16T3MM3N85Pb9YLfaXBn1D2uSwtLYVx5zLL2AMlLULUPrg/8VmKt5fSwWD1i8S7vkQRc025rDijboARLkTRCpvGMX+V/uAD88XQnJ5U+6gDzFKRzjxz6dVcn4Ac3gEunCJuniYBVQId3STMtzbKOcA40nGtAguEdQKt0wgAq26/WZbtwJB+hnhQKZgKOjxsmcv1T3j+f8kSdRZYTkPJioRPVt+AG7ohZ/byxYug0mo3HyWkjx2Ktgez2jF2XPLoJE5bRyqdfRDZLbaGzTn5rzq2Phd4sx5W9A5XWH5cuF/4ck6p8xRbmXjw7rU/5xrTm9G22ux30fX7mmXfPMgvlnKdjYRAyURHCNvaxgcyYHD7+DbQf3n0Sb05KgwIB9YDA0Pe+RKmebJKXtMmFWmvMTspnkhc8/PdY4Wlc7mOVd0Pr7mKWVXbgF3SP58HTHQ8usawXT4nNG9aPo6eO/HE+Hc2+i4wO4B7gQ4HS0Og3sb85PNVe2g4VbytwltAni3P7jb8KDnaTTshg3dtpvHcfmZ2pWHof7ixkG2iUTxRTFlhVcBvt0yNq9e+SPP4kW5qL1sejxSTt62zF7TVAXzq927cmYAWjq87R/f6b98uIvfkXqYqMNAdFHC+Q5Zg+IjON+ICbLDGgcTPnj96OXa2H1CITzL8dSfj5hBebRjsc3mwvr6XZ+JfFP2cPF/9InnOdcbKPtB3DYcO04S2PA5Zl5MyelS6XPEH+uF1lCgppcf5EhGTxPund+SOsYPK+TTT8/1Kn165bH7UmO7q1Li50GDfgwmVgbHKS2IZxkUEwyBJMEt19FG8N0y+C14bdQUNrQ/RnF4NKG3GRa2WG6Yg/EvGB+H6aTvXaujAKd8GNzM3gwE8yD0L9JiPqtgq0b2pc63QzcXnQ80awshzNjWSJaxd9/SUTZbmEag+3HXBdUkt8aJvfw4X49Ot5VWxWnTZ3pI3KSxGsLkqzXRqrU6pl1LB0xyrUr1hV+gLllsDj0p6QAw0GuBMBWSQcPCxpy/ZxZ+gCN/vVzmh47dVAZ6DAGLL1o5qADBKHEnLztFDVdWlIDbwa80ABaDdQgMZwbcLIXjaMnfnXSP1lrVu5HC8t+tdJcnAXbQauCZAQzT/zlFViBIATYmmB8Prp3n4an9gBzNSsvwfYQKToAbr6DmbXwu7X0Ic1ey3aYBsANHah7ARwSwxNw06bsz39/Es49CeqPwZDj2rs1nP+gEtZ3FJiZGKyUqfpt7J0/RUf7j9Idc/avzwNGCTZng9XxcGI8WHoezDz1x6VIfqrDGAP8NitymU/9HdPgtFfK4DG/cIRoBLW7fm1eTAsmOIVI2mzjjA0gMXCkaHKDrfWYoO/g6ATrDHIQBdu+gRYRbl7Lca/ldgFSYOjgP/3oKkEMd8DFbXO2/hTTKjumuRDhcPTQ5XoxChfAhuZ3WKmgI9qxEXlwY6v1DGjdAIEAcABgirf6o813+h4N0DJ0rsUS/C1nNwEg/ejb/0mWajlZybE9bo3JuYr7g5DJ4PdngyWI8R4oHnjRFhIV9MWIEKI/jwAhg025XmxTcoUyiAEhYn/kTeNoXniP4UVQgGD2bbQzCaEhQED/5EVz+b0/vifBHxJwBQDVbpyMgsmUXY2DSTQgO5P+9DwgyGhtK1w4blaGYbhfW2s+HG0cbGP0yVcETfbH1bgzsdXiV6o99uSxAW9x5oLssu4KcNc4OGox94Loiu71pWfb3/Xf3UtTuSmKuO931y5hO/6TtGiAhPsQRBCEgPldt992DEGpRZvVaHvC3x1V9ttmGCirkx4iYoTKSBpoIMEfTDiopaVCRmwPHz6Ohk/SwePvmMUc4abD2bVwViKE3xXsHjC2n8ReR4SC/vJy2gNk6bRu5twU8bBC3jg+DJaq/kI9M67bE6sTcThVCUamYprfs0IpQRmy1S6ir+nlw5I/RO8lbvn4jNzbpNvxgrmPSgjIHWXJpE1VKZ9PNhfuSkoLfDuaVYQ5wPQioIqOz8zf/I3bKdA2eDwCPTAKfIHsVSZwHNBtGcrRZWM2xNCLGB8O2GUnNiBABNeK9h0iU4dZ2CYNB5gdG/wAYhKuuz/Z3HLvcazJ2RNHjUl3nNoRdqXXvAkiCcsgqoLmDrOLdWmGzdM+7CswOwbjbPNr1ehkIny94e8sBmvfAqzUOvzqK0D8YHYQ0s58DShf8vAzxkqxE6bcyPtvw7mFYHvfr70UjnjuIKb+g+70orH71HRcjh2DpYq//hQtz51Nf2Q8oeNEBQzQcwy2bsTE76YBQgWzb6StSw1xGbMSSjBxYe0dRGzN+W8SMmGDwnoVSGSzncvqGuzcX9pL6xqRSZUhkrTKKCRax1Xb0wucre2axYjLPBV3jeJ0LrljVWAKBcoJ0VdmsVzUPuoVK1eac/uC16kDXmaeDo5R176wCmQzbo+gcX97nM4gXTYtPuGnDmN48Btad0knzH5aVzySJ1zAddkllK4/lVmZzNfTI3AnzcVJiBSbK7fCLYnVL5cLnqkV2E1WQAdo5HTH0DoAVef60E1ibwnu2jBRYWwwYZyU1NhfX2wuzvgv1qO7z8Hg+tv3/ZHbIJK13WB31t8DTi1iZ+wMJFc/Z7ERfPcoWNpMmvuV6AQYnA5NsDvxdtid9TJAkZVgIGuR4M9tvvbdjaShHW4O76yejlk/t714n6/qwR1pIr/4jA9dOwQVlW35fGta5As4SGav0fhky16/KMmkbPwz7ilzYIKWiPcqf7ZSJOie566ng+0n4cQzJS4F2l77bIM9dniWwf4CrtQxAVRRRomyUic7Qe0hAaOYDkFEDK6EgeUwjAESz1F6sVRGlKX9UcCxnMN4dGjmNdgU5umFYQRX3qVdhZEeyC1DaOfoOQy5Ov6tU8vpFg6mtBLYbs3tg8AyByzJUwAlpQexGDcWzYdH0cgGYjGOazBNMPVEkiHjAD0t1f0JzPL5R4fh1hTQBK/Wgteb4fMNCCr5yiD+oAyNkyWRmIknQLNY3YeJlQwN5wbPKyUgGTZaLhgciHo8TukoMB3UmhVL3gCPU5BbBsvroJzt0LRpKdztJLh65yVsXrX88phaB5wyvHUYPjsEbAqHa67cjerbMs8anx4xaGq1RK8VOHoF9upipcJycXcEoakNLg2urofleThMITxKnC4KHiIbSvtd3ofol4wt5UT9iWq0NQWa4dduN+df0CEoDdy6fL9O6Q8YkfYlV7Biwvq5SMMxN04yHanUc0KVSjqvvPDvSXH+h7KZu6H1llHyQardcglHwqWVVCwFBnF2N6h9R3AKlfz9FihTMD3mV+/HU3350eVsvjaqPwhG3zS3ZtNJ2y8BidhFvr3FevBMwtIvBac/R05zBVS4lBBhkIa94pdsd0FnhI/DNPDM7aBWS/dqHxUKSdnjDLpLmPq4SelPlQycYXNxViHGTG9C5M8t+1NzmW7tc8ZEKqwJRnJj+AzaVFKqPR1aBDo9/yXau3XiCSmKbIWzWKk506ftBobnPKGPVHlAIejD2JRHlnF8a3p9XK6bwwfBN2NwBn/kReMQZGABq4uKhSCXp0i0sgRNpizBi4BKBPuDl/CQcRS51bcVz9WNy1D+eBTmSjWrtT3e2+KOVTIXzDlsNnHv0fiYiJ75Nk8ZrAxyxSg4UOPwoTo2PUTh+4fJ5jDajpdQUr18iMGXiTe1uZedFRRaM0W2mznSAovylXa57PLqLK9c6EWeVtDBdYl0QSwZXSmnA/cQ7q2Gy9sUkQN0ggvx3480V8bRxfC43x95GkxUgqUJeWk8VTAaPjwAF65eYJ/dLyL95upuuH7YEuYjBRbBYEO6gaBAJvpjbxHvFtTIK7uJ99UNMPie6VJaJ5VR4X4VohiOS8B1xy7JAiaIytnj4OH7TBmBdigPSQgd3AucB6422HkdzI8hktwjL/s4+HobMyE8+5Ew4mQJXBYV58G6gszzFCQ6Lor5M9z5YWcvlwyOPARcwXMpup6wxkVsUmB5j3z0Dzo7Isznd8AVAyD2V7f83S1wxZQOomgDzitTRiAWxBl0Ja3nLXsGHLkVc8Y9GpbH4tIQkfg708G4DN+7TexvC4r3Vv35ndNxcTfQwaDT4gm0WDwJmMQTNsRCFE3M77SPJjaRYfM7qhkjQlQQHmhIFNpdBM1riWL3ai24uhsCfLjCj20K6/yZZf/plKownt7by9A2tGEC5uqm5sCTn8EH7rl+49j9IjtFbiis1ABOKJkejDoBx5o5bt6Uv2S/XSrJOpA/vBRsy4i3m8AGPyWHF7IDEf8Vx/bsnF1om/KiEKBZWURAU50Fk98uAYbzpLLine truncated
|
assets["assets/lang/lang-zh-TW.json"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["assets/lang/prettyprint.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1xSUY7TMBD95xRWvrISuQASEiQsCxSVKpuF79nGm5g442hst9oijsNJuBgzdtSW/eq892bGb15zAFIWcNiRDuF5IYNBvVW/iseheFPU0Q5ABrB4XeyBiQYC2BW+O4DVuDdXvCq/rxzeSI8X6aT3I4NeM7jTNKdxbQWR1pMAZHCLgzV+TLC6qy+MKh/QBN2rjcGhd7Ns1rL5fgFcJ3x1e39hVMmFSRaeDNMfDa6NTySQ2GJGVfP+TKiyAYQeZGyMTH+KeL7eBCY+85EZ/pSjvwA/p71mPLlq0zK1caQlhvQri6zMfTVhjOwsjeIjM1tHRz0woWo3zX//WBEkkg8xJGuI/3Vtn9GRl6wW6dq5NaolVHWbCApxiGxGlTXBydibrO66F2quIenkqvYb662bkztVrlUSJYI2ep9t+4Pke9R9fjhIkF2kKcM4MXyYiFPP7aexarby748G07tSpL2sdD+ulQ7MUb6X36/+AQAA//8BAAD//5YJ/N+MAgAA")
|
assets["assets/lang/prettyprint.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1xSUY7TMBD95xRWvrISuQASEiQsCxSVKpuF79nGm5g442hst9oijsNJuBgzdtSW/eq892bGb15zAFIWcNiRDuF5IYNBvVW/iseheFPU0Q5ABrB4XeyBiQYC2BW+O4DVuDdXvCq/rxzeSI8X6aT3I4NeM7jTNKdxbQWR1pMAZHCLgzV+TLC6qy+MKh/QBN2rjcGhd7Ns1rL5fgFcJ3x1e39hVMmFSRaeDNMfDa6NTySQ2GJGVfP+TKiyAYQeZGyMTH+KeL7eBCY+85EZ/pSjvwA/p71mPLlq0zK1caQlhvQri6zMfTVhjOwsjeIjM1tHRz0woWo3zX//WBEkkg8xJGuI/3Vtn9GRl6wW6dq5NaolVHWbCApxiGxGlTXBydibrO66F2quIenkqvYb662bkztVrlUSJYI2ep9t+4Pke9R9fjhIkF2kKcM4MXyYiFPP7aexarby748G07tSpL2sdD+ulQ7MUb6X36/+AQAA//8BAAD//5YJ/N+MAgAA")
|
||||||
assets["assets/lang/valid-langs.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/yTMvQ6CMBTF8d2nIJ29j2CiEOOgUVNJHIxDgQqVppC2dPDpvacsv38/kpOUL5Kyprso14diV7xE04utaFVmz3/atSbfAtNpRlvgMnQq0ZCh44P7McBnqDpwh4UxkfliaZzoLLkWL64BWHRYnHGaI5Vy7b3m+onkDcVOSEzE/DIyv4Gq69r6Kd6bPwAAAP//AQAA//8bXi5E0gAAAA==")
|
assets["assets/lang/valid-langs.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/yTMvQ6CMBTF8d2nIJ29j2CiEOOgUVNJHIxDgQqVppC2dPDpvacsv38/kpOUL5Kyprso14diV7xE04utaFVmz3/atSbfAtNpRlvgMnQq0ZCh44P7McBnqDpwh4UxkfliaZzoLLkWL64BWHRYnHGaI5Vy7b3m+onkDcVOSEzE/DIyv4Gq69r6Kd6bPwAAAP//AQAA//8bXi5E0gAAAA==")
|
||||||
assets["index.html"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["index.html"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||||
assets["modal.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3RST0/cPhC9/z7F/HLogkQ2gDjRXaoWqVIlKiHBpceJPUlcHDuyJ9BtlO/eibNNYbUcEv97783Mm9lo8wzKYozbrPUaLVSoKQPG0jhNv7ZZfpGBq3NkDrlGxrxE9aSD77bZMAjVR4JPwKEnuIZVZGSjVjCOB6wn2pUeg06sL95bQneS2KcT+OY/gM3/eS5LUcCt73bB1A3Dye0pXJ5fXMFjQ/Cwc4ob42r43HPjQ1wn+Mx5bEyEB98HRcLXBF99aEHuYl/+JMXAHlhEmEIbwVfp8N3/NtYi3PelNWoWujOKXKQzeF7D5fp8Dd8qQFCS0sK6v4MXjOA8gzaRgyl7Jg0vhhsBSMzKWDqb5X74HhQ68CWjkcURIEPD3F0XRTvHX/tQF6JaSLxiKirPkyGHzcm1QevrZO3+fljNL7ZeXYPFUNNs5jG28o7J8f79GKIh6X4AtBR4/ufDMPW0j+O48ITZXL0lsmFLrwACiZ1ULYmaapsZCZ39ZVQoQya60+Wkuikm6GvuMCS9cfwXsGiulrQLyfv9Gkqvd8khDuiisr2m95n7/NIgZm91Ku9lVl4XLV1m74B3HW2z+bBwSnYgX66pwt5y2sc2gzT8MiStWZSP2PTGGTYtxcWWD66M3ccZliqyyHRzOyV8aNymmJM6Uu6y3W/2yx8AAAD//wEAAP//qvxG6f8DAAA=")
|
assets["modal.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3RST0/cPhC9/z7F/HLogkQ2gDjRXaoWqVIlKiHBpceJPUlcHDuyJ9BtlO/eibNNYbUcEv97783Mm9lo8wzKYozbrPUaLVSoKQPG0jhNv7ZZfpGBq3NkDrlGxrxE9aSD77bZMAjVR4JPwKEnuIZVZGSjVjCOB6wn2pUeg06sL95bQneS2KcT+OY/gM3/eS5LUcCt73bB1A3Dye0pXJ5fXMFjQ/Cwc4ob42r43HPjQ1wn+Mx5bEyEB98HRcLXBF99aEHuYl/+JMXAHlhEmEIbwVfp8N3/NtYi3PelNWoWujOKXKQzeF7D5fp8Dd8qQFCS0sK6v4MXjOA8gzaRgyl7Jg0vhhsBSMzKWDqb5X74HhQ68CWjkcURIEPD3F0XRTvHX/tQF6JaSLxiKirPkyGHzcm1QevrZO3+fljNL7ZeXYPFUNNs5jG28o7J8f79GKIh6X4AtBR4/ufDMPW0j+O48ITZXL0lsmFLrwACiZ1ULYmaapsZCZ39ZVQoQya60+Wkuikm6GvuMCS9cfwXsGiulrQLyfv9Gkqvd8khDuiisr2m95n7/NIgZm91Ku9lVl4XLV1m74B3HW2z+bBwSnYgX66pwt5y2sc2gzT8MiStWZSP2PTGGTYtxcWWD66M3ccZliqyyHRzOyV8aNymmJM6Uu6y3W/2yx8AAAD//wEAAP//qvxG6f8DAAA=")
|
||||||
assets["syncthing/app.js"], _ = base64.StdEncoding.DecodeString("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")
|
assets["syncthing/app.js"], _ = base64.StdEncoding.DecodeString("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")
|
||||||
assets["syncthing/core/aboutModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2ENqWktOglwHhFDvTgpSgT/+/yw/F8fMTmxIQ2asYX5ErxPF0fokCE1lSuxAgl7pVgB3TlkPaXAMrbnkQlOKpm7M842uEdQs8af4lLMKed0Z2EP7NymGKzvFo3BZN4NuNei+/06EdztpYNiul75amr56AQAA//8BAAD//xLf4CHFAAAA")
|
assets["syncthing/core/aboutModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2ENqWktOglwHhFDvTgpSgT/+/yw/F8fMTmxIQ2asYX5ErxPF0fokCE1lSuxAgl7pVgB3TlkPaXAMrbnkQlOKpm7M842uEdQs8af4lLMKed0Z2EP7NymGKzvFo3BZN4NuNei+/06EdztpYNiul75amr56AQAA//8BAAD//xLf4CHFAAAA")
|
||||||
@@ -79,13 +79,13 @@ func Assets() map[string][]byte {
|
|||||||
assets["syncthing/core/selectOnClickDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3SQQWvEIBCF7/srPBRiIEjPXXooe2+hPZYeRKeprJkpo2YpJf+9atLdJWzeSZ/z+Z5q7JPXrAayyYNswg+a+OWwV4YYmnYnspR1DCa6sQyAz8sXPHhnjk0nPlMGHKGQdyeHlk6t+K1QEUNMjFdGkXd4fLjigqFv6ES+dwCMndAxcmhXUNEyoQhlY1bx8hZQNGoWc+cS9iiWlqqH+PZvy3a/yXL+IcicJZNqumHQEV6LvcVVRs2pz2ThQBgzGuTygvf7jw3y3FQxDDTCk/c1KWxFXQBt7Vyqpt8Yn1bedN5N+105/AMAAP//AQAA//+SF+4JDAIAAA==")
|
assets["syncthing/core/selectOnClickDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3SQQWvEIBCF7/srPBRiIEjPXXooe2+hPZYeRKeprJkpo2YpJf+9atLdJWzeSZ/z+Z5q7JPXrAayyYNswg+a+OWwV4YYmnYnspR1DCa6sQyAz8sXPHhnjk0nPlMGHKGQdyeHlk6t+K1QEUNMjFdGkXd4fLjigqFv6ES+dwCMndAxcmhXUNEyoQhlY1bx8hZQNGoWc+cS9iiWlqqH+PZvy3a/yXL+IcicJZNqumHQEV6LvcVVRs2pz2ThQBgzGuTygvf7jw3y3FQxDDTCk/c1KWxFXQBt7Vyqpt8Yn1bedN5N+105/AMAAP//AQAA//+SF+4JDAIAAA==")
|
||||||
assets["syncthing/core/shutdownDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOwQqDQAxE737F3qIg672eCv2F9r6sqQZitsRspRT/vdqC1DnOTDIvSJ85qB9TlxlLmF4SbSDpfUyKUBVule9IMRo9t8KQrUuzXChw6qF297xeUBJXVu79rW9StKzyZ/zMyZSinRycoT5EhuODg+FVeU13imajaI6bN8LZDzYy7B+WtliqtvgAAAD//wEAAP//3qFOo80AAAA=")
|
assets["syncthing/core/shutdownDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOwQqDQAxE737F3qIg672eCv2F9r6sqQZitsRspRT/vdqC1DnOTDIvSJ85qB9TlxlLmF4SbSDpfUyKUBVule9IMRo9t8KQrUuzXChw6qF297xeUBJXVu79rW9StKzyZ/zMyZSinRycoT5EhuODg+FVeU13imajaI6bN8LZDzYy7B+WtliqtvgAAAD//wEAAP//3qFOo80AAAA=")
|
||||||
assets["syncthing/core/shutdownDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0TOTaqDMBAH8PXzFMNsXL16gZhNj+AJ0jjWQJwJzgQp1ru3lkK3P/h/uEXGkCGNPepcbZSNEdSCVX1LjZFUEVIU7rHIRuu/TBOCJcvU4763wzcFV1lKJqMWnmBrYM3B6DjQN3+u/MQPD442J77DHBRuRAznMpwlF9cV37juc8o3LwAAAP//AQAA//99X8KxnQAAAA==")
|
assets["syncthing/core/shutdownDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0TOTaqDMBAH8PXzFMNsXL16gZhNj+AJ0jjWQJwJzgQp1ru3lkK3P/h/uEXGkCGNPepcbZSNEdSCVX1LjZFUEVIU7rHIRuu/TBOCJcvU4763wzcFV1lKJqMWnmBrYM3B6DjQN3+u/MQPD442J77DHBRuRAznMpwlF9cV37juc8o3LwAAAP//AQAA//99X8KxnQAAAA==")
|
||||||
assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("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 truncated
|
assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/9x9+3fbNtLo7/0rGN3cSq4VOekjZ9eut8f1o+uvSZwTJ+13jzebQ0mQxYYiVT6seJvcv/3O4EESxAAEZae79+PpaWRyMBgMgJnBYDAIk+syDrPJKp2XMRsN89tkViyj5HoySzM23PkigAd+J4voerQo4WOUJqOHcToL8dfLLL2J5izbCf7gkPgYHyfLYhV/9zyds1GRlWxnsgzz5cuMLaIPo+GD4c4BL/qprqvI0jhm2Wh4qag5rl4Ox4EiIxg9zGfpmo2Dh8uiWI/rmsfBM/gVs0uW3UQzADi9YUmRN6kcljkL8iKLZsXw4Ivq9d5esM6im7Bge0sWr1kWzIHOJEKseQ12E2YAx25OADA4DB4faF+S8Ca6BkKS66NNeAvfF2GcMx0mTeIoYfS3jOVFmGH5+nsFULUeqar5Mmo2Dh+NA5OwLNJj3ovi/WjnQIPOWXGeFAAcxpKrE+ggIGQ5Dp48hqcFLxg64XQ2cX3SWVlO42i2x/HRjJR1zdLVOma8WYfBH58OzO9IuuvbeYKDBSBwiFEwCeNsy2kkLMvSDL9dvTW+wdxgMV1sdXt+Al+GQ+PLnCHfLRjFx1fsNydNizSG2dMFtc7SIp2l8fES5jKbmyOq6s51mhUwYEMaj/gO0/ImYhsrFkGThZScseQUGUmzpFxfZ+GcnSeLFACSMo4tjLkswsLJEwcAsOMahq3l6w3Qbh1kCWNzzkCix8S312kRxvp8174fl1kG8+JleI0z+4kFCj9fRv/iICamRRjFnAqq9fxbVy0a1BlnGMCVCZ+AbG6Bb1HVBkLRGiPfmWKCLm6mtwXL94PHY+3tAjDj21o+NDGPNlEyTzc7kyn8OxpO2QJ0TpnEaTjXxHxbtBnyVZ/zn3ZM8h+C1hpU+kHMlctyBjM0H7iq0qUo6AwpPitUMOjDbLYc7UxiwLnjpgL7/6iezFpNw2w13A+GJywejtsf5lEmvwUj+L3ThkCtjQAoAtvfirScLfHjm/Ucem/Y7AqCvPOZg7giA839KLUSaPmuyAtBNmwezaJsFrNH6SPo+sRKbZiDNory90N66NS9KjXRxYtn5y9OXV0ZLYKR1Lpffhk8qJVsGxCfjBVllug6r6Hb8EFGpTHOjOvR8M35BUc9bPa6QMS16OUttGfV1rryo1DM9o9SdXGpZ4E6qQWnBeKslpyjNpHcfppcs2JUZvE0BMtoNxju5ZzmPSkzhzuTXEyYUc1jGFEhxT5D3CJgi5s7Qukqc4OtouLvr1+/5ArEk8Cb2Z5QW9sQpynEz0OfYKDUe9vQqKtMB5H2YW/Fpitgga3VrMpI1QUsPpSNqlU5Gv6vhBWbNHvPOQatBzsKrMvhMpqz4Y4JXWPshs2XZcHFhxXSpgSUuDg785EXLU3z8WPwQPDknmTGYqGEBsl2gq9IVYfssrJ+p01bF7OG1YDX9DFDHo6DMDOqB5v/FOZJAQbMZsmSIAwQQwAqMw7QzMgnwa8swKVXscTlV1iUOTBlDn+nMMhu28jgLUjGMmNBWhaAMiyGebDJUliffvHndlaTE9jwe+0xBAGkE8mQw0MwMSk4yZWjQHZvwCf/OEjSIggTwWv+iiypOhYlWWsetJrDmREwaEqbtL8dBt8+fowKtPHy+8Pgu7/+1UnwNUtK5FAXib2kRrOUr/RolumWIu0SKOadc4pzrucsk11x+fro9em7478fvfjp9MRvuqGzYC4UGHaHqR+w95qr2CsEkYuotw5NYQLzvmZSCU2K1Gy2vSiTa0L+kv/R0jP4wDA5X4DAEIWCZZgHvEdBlKD9CB07RqEBMiW5DQAmiKO8CNIFhUiu5qM8SKc4hQvG5c4KpALoaKgkgSWuLK+AwVa5pXDlS7AS4luoOy2vlxNy7kqe8Ik7lNQObfNBW3VpjGotPdWz5Xh6dnF89Ozd+YuT0/9+9+blCQwuz1FFG4yetZ6c/nJ+fPru5Pzy+OLFi9Nj71ph1EA/EQ6bKzW0J9H8bR/j14/SnmRyad5BpGNi2YoYK66qvmS6NtfV6gGt6PocJT/ispyv2F04PKCK2zXbr6TMBP+kAcP5HP0vDVh8Yw5qqwCpfYF+HHpXCMKfPH7cVYuHxaOcpGAnu50QDdEqPJCTdM37dVJmR2Dmr1FyWTW5kHZljj4csRBBSxqF3pSBjEtYBLIuC0KFKEnREz1DK2Nu8h2wbViwCZMCLaYwf8/NKzCzMvx7Fb5nIPNmyxR9GMGPaEmxYA5LuoKXodBBsWl5jWhWwbzMkDjuvA1jdBWX63GQp4gl5+J0lqbvIyAAiCaRATFFtGIobfH3IspA9N5EeVSAXEaZXoBlp7CA1F7DCGI0YWEyr/EB6CrN0JgE62eRlqA24H95EF6nY6ROcoLC83sJBgP6b0xNhFqVk/gLUoh6K52VK6BnIijElWMczthob/TDPvz3z4+Trw7+kX+1UxeCv/5xCP8bXf3z4O1XO5OvHu58/Cf8f28cDB4+GRBGA5crNQKb8miRAsQN6kKHA1h24pbEJEk3MGB3g8HBKvzwCAYZ//TN4+Cr4Otv4X/fPH382GryWQQRELjbYMv3zZoeBQor/IP7BTb68UFDqnRaUJyY+9KDUs6/Ov2vHmLe4qivJZL48lZYXt465+zi2cnpq+1oaW8H1LRIkwm6+xF28x1pBIV4dv7Tu8ujX3wJLLlnkXtGpStLkdDLTyLE6F6UcH/hFt6S1k4Q50HzXT8Hj+/wuvj1xbOLo5N3L19d/PTq9PLS33TP5TYGbbuLPUZyNwOfBWiEERdVovejROCj+KPQXNWGZhudjhJM0wrhlWO5ULUEMGrAV4iCsGVVgYyBdprzvQYUGflEvgB7e8L1ub0odEDE5mdZurrIousoaSAxPvVCdwrCbwNal5EY668eSNdlHGvNky88iwqvWrMsvukoDEoN1jhFdovKO0tLUJTlukKHdkERxCzMUWGvWTaDkYlKnKtekOebtIRF1ZQ1waaRUNPTmJkLH3xQKagavg+eoF9AtnSXZOyu2Xu7aix8fxjYnQj4NDjjqzJ4sdbgF0PTak7iIyjal//Sdi4+7cbsG298ylbs2ade2jEITu/Lf91wwLd99cMOOW2sA/JJ/VdHiZM0YVUB/IOGJySOTaXjQwi41h6vzZX2YFQXqWBtI6thx4vtWbVjeigrdg1JuQB9AQVlnf7WTO1f26p6zh4lqFuM6RLZDUZJBEZJV9FeLVfPNGPhezsIPXf9jUB8zBAA9fPA6tgdnKQbvuP8UoIOxsYw62veXb55/vzo1f+5Jx+e1a+mTJy8XK3C7LbtGgFrbPZM37Xv4R2RTTm+eP7y2enr84sX9+iRfGCu9Jv2qtPCI0sQ9gw5MizFSa7WsC0bFpsIahjDQEw5N0sKPT4En0qSzVbrxmzt0XzkGhZGf8JAODwGthmKAXRR0t6vM3mCD7Zj97CLPVAzYcphU3cNXdyH7xPREtxfBDr2EGPf8Xn66tXFqx4mt89Moz2ztfM6dxOprSNwS0hbvozlrtc4WLIQQ6nGgViiWFZ9YrdG2376A/HsB01s+22s+230+/LfT00RcKBHNM7ZFMzGGegg+L9ac1QQVTs0//CI1lKI7j3D2JyBBo7rU1GCEAt6/VdQnpwMBBj2j3zbsRXOWUuuQufTPcHEHwSBh0N4zRLcnXzz6vwYhjBYNkmhGtxnfdrqUyHK9aFl14ydkrz9aOpN5/5YMn8cyNU5Vd53YVzBixDRMY8SsO4x40P0nCV4tOKp6VugpwsOuGWYH6tA0QdRfrpaF7cXU/SX6J5awyvRCjIVP0jZ0HL1vsNNJJYcCZ/3ZYHznYbUACe/pVEygqlsxDbQtVzH6TSMj5KEcw+j0WBmu2ojC2i12r1duYFVvidJld9AUGan4WzZnAz8y/GCjlEwFYMClyjPT+7H5U9zWJGdp1mhKAVawqw9gI24V/Hrebhuef8lRKu4GH4TGOn5SEe1Q7DMbvGTMte6hSKrUCJm205qVHxcddTI7CglVSjZ0JYH7aHHBX81cR1DRSpCbX/GHtBiV1sqEq+tbl2eSaEXtnFJygBxbjzgb2uXiZoqZWADC/UprWJxhXHyoVAz/uLn4Adt7JkAO5OYJdfFMthvm6v48HU3t4NUCcvmdGXb5pwiNG+Juhxr7wcm+JXA5Vy+6sRN1mW+HIliW+4n2Jl8piKy9TrtnZSxOLw9jiM0VLUeany4FKEzRC8ZQL499QoLtk8cGP3E0VfdZFbW2VVGkSv+xqO3BIGir3iZu3dVk9WtfhK1QS+ZKxfCQBJiYUAaRt47BaTUIaw30zT1jLLHh46055W2o+05Ua1xWrvVGktRbpESHUiTJRx9jYWjNGjfCkAeTWD0rAUVp9mK6gy/biHkDXU1tnjTePQOh+Br64bU3jJo0Lq2qE2dH0SFfH0h6waYL+991WFztdjdDI1+MgtYNo2sU9PEcOXrYsHH6WbBh3S14OPhbnGzoI+3hVNi9bjQDMLH7nlxOl1Uw03HC12VFT/pe/GSlPXcQmmpza6xg9f3KVLpgxF9LLpGHFYvs44f70zxfFwd/WAJmEJNNELYR9VR0R1gNF8q08NuTvRA45Qp4CIAcMPPEisiohJRyvIoMkDxPCyWk1X4YQSr9ZH2vQ4BA3rNYLXXBhi2pZhbNHgDtQhRc9StxZ85KtfgrLV/CmZhMVsGIzLq28IWQojYWkGFzdhnnd6EOnqWb+Kajq36LEqzrEXCRXNlxXXbbLDAutjggew1y4pbKOrc39lKnlmHoWrZVTR/ax+I8mvHSDCgnKNQq9k+DtVnn7r9BiFnU+dAJBljGYp0UyzAPexn/WA07YiklUBV7nPYzLwODtpTrjMZnN97oV4dACf9/C5WCDI/GxfU4cCe2g3K9GKD6We1eIgtowFKbMeCLine truncated
|
||||||
assets["syncthing/core/uniqueFolderDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/7SSwW4yMQyE7zyFD7+0IK3CHc5/b1V74h5tvKzVkIDtQFHFuzdhUaGwtJWqzgVp4vkYW2vDMnnLZhVd8jiuZB8a7SgsTRMZq8kIsowjxkZpmwdSoE3Ch+gdclVDm/I8xQDjCbwdh4sYNXG4MHpzkzJnBlVYPkaHvqo/vXsKL7MLoDRxjTWgX9VgVVlqaJT95ApbVHzzb21ZkMWkIB21Oj6jtoS7hfUJh8JF1J7+z6Ajzev/fyUpv/cCRdMp7BCki8m7UClsrSdnFe8G+pqCuiiTpPubcyrnjvNBwCFfQvCiaXsMiemsPO3CM2ePM/K86zfdtUPoGYBlWwHrGa3b/6J/a3PHrxf4aScS6OF/dM5B9/ThftzwNnu44p05h/moPL4DAAD//wEAAP//r65WJ1IDAAA=")
|
assets["syncthing/core/uniqueFolderDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/7SSwW4yMQyE7zyFD7+0IK3CHc5/b1V74h5tvKzVkIDtQFHFuzdhUaGwtJWqzgVp4vkYW2vDMnnLZhVd8jiuZB8a7SgsTRMZq8kIsowjxkZpmwdSoE3Ch+gdclVDm/I8xQDjCbwdh4sYNXG4MHpzkzJnBlVYPkaHvqo/vXsKL7MLoDRxjTWgX9VgVVlqaJT95ApbVHzzb21ZkMWkIB21Oj6jtoS7hfUJh8JF1J7+z6Ajzev/fyUpv/cCRdMp7BCki8m7UClsrSdnFe8G+pqCuiiTpPubcyrnjvNBwCFfQvCiaXsMiemsPO3CM2ePM/K86zfdtUPoGYBlWwHrGa3b/6J/a3PHrxf4aScS6OF/dM5B9/ThftzwNnu44p05h/moPL4DAAD//wEAAP//r65WJ1IDAAA=")
|
||||||
assets["syncthing/core/upgradingDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1yOwQrCMBBE7/2K3LaFkt7tSfAX9B7SNV3YJmW7UUT676YKRZ3jzNudcTFkdmKnNGTGGpZH9DpSDNYnQWgqU2QHEvRKtwLkOYgbCnAixylAa665nFCKpm7M881vEtQs8cv4mIsKeT0YOEL7EylOMzvFs3BJ9xndNqP7K70Q3u2oE8P+Yu2rtemrFwAAAP//AQAA//99zQ2GzwAAAA==")
|
assets["syncthing/core/upgradingDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1yOwQrCMBBE7/2K3LaFkt7tSfAX9B7SNV3YJmW7UUT676YKRZ3jzNudcTFkdmKnNGTGGpZH9DpSDNYnQWgqU2QHEvRKtwLkOYgbCnAixylAa665nFCKpm7M881vEtQs8cv4mIsKeT0YOEL7EylOMzvFs3BJ9xndNqP7K70Q3u2oE8P+Yu2rtemrFwAAAP//AQAA//99zQ2GzwAAAA==")
|
||||||
assets["syncthing/core/upgradingDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1zOTarDMAwE4PXLKYQ2WSW5gO0zPCg9gHDcVODaxlIIJc3dm5b+QLfDN8yYSx4pAo8W5zJVGjlNCKKks1jkdMoI7HOySLXmpfNcfQzdXBCUNQaL69oe380WbqCVkkTSsG3omj9TnJFC6Zu7wzV5Pe8cWOCz2pvh4Rz88v8YSAIsxPoifb/j4hozPN+75g4AAP//AQAA///kaeW6xgAAAA==")
|
assets["syncthing/core/upgradingDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1zOTarDMAwE4PXLKYQ2WSW5gO0zPCg9gHDcVODaxlIIJc3dm5b+QLfDN8yYSx4pAo8W5zJVGjlNCKKks1jkdMoI7HOySLXmpfNcfQzdXBCUNQaL69oe380WbqCVkkTSsG3omj9TnJFC6Zu7wzV5Pe8cWOCz2pvh4Rz88v8YSAIsxPoifb/j4hozPN+75g4AAP//AQAA///kaeW6xgAAAA==")
|
||||||
assets["syncthing/core/validDeviceidDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/5STQW/qMAzH73yKHJBS9Kr0Dnp6l7fjrtyzxG2jhbSznTI08d2XAAMGlDFfKtn+/23/ourQRK9RrTobPRSSNsFw60KjTIcgZxORQlmHYNgNqWHQ3tn/MDgDzspS1DEJXBdEMW2Z+5n42ElyIHDEcJbYJ99icpsLGZrnzoKX5be6d+F1fuZKpuuhFOBXpdDMSKUwjH52YZsj59W010iApGKg1tVcnKwGB+ul9hFuiXO4+jBPgXWcIDy9O8rfMUGOqhJrENR20dsgWewAaYZRwX5NAl7mTseba6iMacnFTYdtQkFwZ5/dM6gGuIjoX3Tq/SNkRYOp7GHAP2f/ypQ98VAUjQGiM1gI1N87+4tX7lOA2OFP3Q/eXut038jxD0P4xbw7rI/jRqvbsVe6mT38EEfu19pLv5PPdjHJxU8AAAD//wEAAP//7ceZ97ADAAA=")
|
assets["syncthing/core/validDeviceidDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/5STQW/qMAzH73yKHJBS9Kr0Dnp6l7fjrtyzxG2jhbSznTI08d2XAAMGlDFfKtn+/23/ourQRK9RrTobPRSSNsFw60KjTIcgZxORQlmHYNgNqWHQ3tn/MDgDzspS1DEJXBdEMW2Z+5n42ElyIHDEcJbYJ99icpsLGZrnzoKX5be6d+F1fuZKpuuhFOBXpdDMSKUwjH52YZsj59W010iApGKg1tVcnKwGB+ul9hFuiXO4+jBPgXWcIDy9O8rfMUGOqhJrENR20dsgWewAaYZRwX5NAl7mTseba6iMacnFTYdtQkFwZ5/dM6gGuIjoX3Tq/SNkRYOp7GHAP2f/ypQ98VAUjQGiM1gI1N87+4tX7lOA2OFP3Q/eXut038jxD0P4xbw7rI/jRqvbsVe6mT38EEfu19pLv5PPdjHJxU8AAAD//wEAAP//7ceZ97ADAAA=")
|
||||||
assets["syncthing/device/editDeviceModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1yOMQ+CMBCFd35Ft4OElF0mE1dH3Zv2hEuOYo4rxhj+uxUSor7xfe/uPRe7xE7sMIbEWML0jF57ip0NOJNHqAqTZQMJeqU5RzCQnlZ4HoNjqM0t5SMaoykr81rzHwlqkvhlbOakQl4PBo5Q/yDF4c5O8SKc6T6k2YY0f7VXwoftdWDYnyxtsVRt8QYAAP//AQAA//8AoJJL0wAAAA==")
|
assets["syncthing/device/editDeviceModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1yOMQ+CMBCFd35Ft4OElF0mE1dH3Zv2hEuOYo4rxhj+uxUSor7xfe/uPRe7xE7sMIbEWML0jF57ip0NOJNHqAqTZQMJeqU5RzCQnlZ4HoNjqM0t5SMaoykr81rzHwlqkvhlbOakQl4PBo5Q/yDF4c5O8SKc6T6k2YY0f7VXwoftdWDYnyxtsVRt8QYAAP//AQAA//8AoJJL0wAAAA==")
|
||||||
assets["syncthing/device/editDeviceModalView.html"], _ = base64.StdEncoding.DecodeString("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")
|
assets["syncthing/device/editDeviceModalView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/7RYTY/bNhO+v7+CEV4ku8DKRoKkh9Q2EGRTYIG2AboFciiKghJpizBFqiS1XsPxf+8MKcmSJa2cdnOxKZGcj2cezgy1YOKBCLaMOBPulj+IlEckldTaZZRrRiVZUwavHE2EYvxxGcWvo9X/CPEb2wtjJqjUGxIe5MavGlqXauW4ctX80IqMg07TLIAl2dvuCiec5K0FsMQWVBG1iW2md8voBfoj1ObTo7D4H63CgkrKmoJfMeN263QBc3OcrJY4Q5WV1PHVB8ZIwKReEP7G1V6iteAqFXJU6ScQ8bTWxTx724A3B/TGkUw027dxXGuTE6MlB2NgGBFFcxgzrw41a9NFtSURN8Qbo8siQoert4dXGbUxN0abV+9JW9AsPNzdzv4v1AOVgpGXL8dWMGHc/tjRDdolTbg8YUPAhNrYu9toFVAid7eLuV95tluoonRoqlgPEKLjOkjzp+D01HYaGQugEccfXZxrpSEeeE7cvgAJ+NYjApBzuYzS0hjgdzBudpJo+N+lMJwRj0UcJgAUCQaBZmFT/cDNPsbniMzPnGHUUZwJZp4tHvOxKwOk6MIJ7flqeMEp6L0S7IYyZri110Qo0oiO0M4S/DscQCc5Hvs2zWujzm0F0lQm9VCvcN1xKQn+xDY/x3V1OAxjeDx26F5pK2qZGZdFnEidbvuOdw9Zbd0IGQNZv34dI2thvDdg6e8ZrxYBC4nTBIzmhmTccJKCxgQ5WyqGyDpYG/nTvSL3kC0IkqJKmjpMa/gxlcAZuUc0LKGwnVGb4RDEhhBCYr4SG6WBT9ezfloa8Xk4N+KxvPqPUFxHqy8ZVwSohBIpUXxX7bkhW84LRCCHEgJ+Ugc/wtbA5SWwGoCCrXA4OkhYwTigqi/38Imo+hw1a07hRC46jy1EU2lvZyKp2j6jRR7i2zobXGSWZxn4cDKPaaAHGii13oYMMyN3jkDES8k8vOTdG6INefcDSTNqaIpEtc5guCDB2YoLek0kdzAXiKfKPIHxDdkJlxHbY2TC/aaKk4OoLOZFp6ScH+GRGnNJKcAU3pSBX+HhqUKAidNvGMrtlyXzoG8x9wJ7aagX9uEsRpZLku9bNaadtzAxKDgr1kEPhMFoalydQlIJ58WHjrrSzsgXAZnUHx/I2k5YoAXkoXYmgYBhzE6Zg/E1LaXzFXB2Fp5v8uTFd/GkLKCuBDdwGVrpBxXbG0ctEUjWtSM8L9z+3JPnJFpVIbmNsCkMw4mmAwopTSRn0zRAYp4U/Ht2/tUIuXfmEpoORO2Tr1+pznNKLC8gT2AkyFXk0uL9fC6K94U2Lroh1YtMWxdeXZNGPaaZiO0hcCKNMIwFN77zpKXTOXUiPbUZyAwMbrX5+wRx9VHnBYoH/g+HzXLJUzeM/Rje6UnoaKdV9VBU7ugeYtu6WwDXb6F9WszDygkBOXcUu622iF+qd+SzkvsL5SgOoLeFfF6vh7dCKveYPEM0WovSjKfbRD/2ARuIin8fDlTgf7N7NCYCY8bKFK+P59V4dddMjlTvIWpMn5kPat9kWiDNWmxKEzoZigmw1lnnr12TsFmd5E7NEGRq7IoHsnIP+vBiJBhG756KgpZxzuLXb/phuCSgp3id58m1lnBzt5j9sVf9KTxCZofmob7TjgV6AuX7cEIxV1RaEDvr9fjmpAVjH75pfEZRehu170tBN5awMPoZ+qar60FZFzK/i+jY7DefhHB6Oati8EcwdybYn3AyDofm8Xgcs2csUmGydxF7cmLw9dBtbormiznS8sLPH2utXfdDUlI6h1cMD2J4aCpu4uDC5lQMd5qcQmXCsc2rjx0i3S4jSx94gBdC3i3ynaa9/uAx8IWq8ynIR7H5EvRSJbb48Txp3YPO/hegYPo3Olb3fbVjWDzQhVw0kE1Z7ESOTdCTFn+U2j6XyTtqFN4wilLK2Pd6/agA+bnrxmXo08OEZ3A5LW2cCpNKPuHgbzzX00FpcbMZVoPq7x8AAAD//wEAAP///D/JJH0VAAA=")
|
||||||
assets["syncthing/device/idqrModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2KPEtJbcFFynCKHenUClCP74/7P8XBwSO7HTHBJjDcszeh0pDjbgSh6hqUyODSToldaMULjLaQ6OoTXXlHGao6kb8/qSnwhqkvhT7OWiQl4PBo7Q/k2K042d4lk4r0Wh2xW68vBC+LCjTgzlfOurremrNwAAAP//AQAA///xwJawxwAAAA==")
|
assets["syncthing/device/idqrModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2KPEtJbcFFynCKHenUClCP74/7P8XBwSO7HTHBJjDcszeh0pDjbgSh6hqUyODSToldaMULjLaQ6OoTXXlHGao6kb8/qSnwhqkvhT7OWiQl4PBo7Q/k2K042d4lk4r0Wh2xW68vBC+LCjTgzlfOurremrNwAAAP//AQAA///xwJawxwAAAA==")
|
||||||
assets["syncthing/device/idqrModalView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/zSQPXLCMBCFe59iZxug0PgCttPQ0OQOQlqbnegnSAsJo+jukQ00Gj3pvW/e7uCj1Q7Yjsj2mhCyaLnlpsIcEdjEMOI1mWgJQVgcjVjK7kh3NgQnS0F4ZqOFY9jBH0jSITstVCsoKMVuxk/taS8Xzs/c/nCoFcHptDTcgzKCcTG/7lMHMFi+tzedW5Mfcg7WQ2UPQr+ifAwxf+tWYJOmlaCEUyn+cTrWOvQtvVHYLxAWxfOI6xe+kc+EOrtovqCZlFxu/hw0O1z9OZl16P5jxY9vLPZTN/TbvqbuHwAA//8BAAD//2Nevvg4AQAA")
|
assets["syncthing/device/idqrModalView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/zSQPXLCMBCFe59iZxug0PgCttPQ0OQOQlqbnegnSAsJo+jukQ00Gj3pvW/e7uCj1Q7Yjsj2mhCyaLnlpsIcEdjEMOI1mWgJQVgcjVjK7kh3NgQnS0F4ZqOFY9jBH0jSITstVCsoKMVuxk/taS8Xzs/c/nCoFcHptDTcgzKCcTG/7lMHMFi+tzedW5Mfcg7WQ2UPQr+ifAwxf+tWYJOmlaCEUyn+cTrWOvQtvVHYLxAWxfOI6xe+kc+EOrtovqCZlFxu/hw0O1z9OZl16P5jxY9vLPZTN/TbvqbuHwAA//8BAAD//2Nevvg4AQAA")
|
||||||
assets["syncthing/device/module.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0rMSy/NSSzSy81PKc1J1VAvrsxLLsnIzEvXS0kty0xOVddRiI7VtOYCAAAA//8BAAD//23b1z0oAAAA")
|
assets["syncthing/device/module.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0rMSy/NSSzSy81PKc1J1VAvrsxLLsnIzEvXS0kty0xOVddRiI7VtOYCAAAA//8BAAD//23b1z0oAAAA")
|
||||||
|
|||||||
+46
-6
@@ -9,6 +9,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,7 +27,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
OldestHandledVersion = 5
|
OldestHandledVersion = 5
|
||||||
CurrentVersion = 11
|
CurrentVersion = 12
|
||||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -212,15 +213,19 @@ type FolderDeviceConfiguration struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OptionsConfiguration struct {
|
type OptionsConfiguration struct {
|
||||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"0.0.0.0:22000"`
|
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
||||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22026, udp6://announce-v6.syncthing.net:22026"`
|
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22027, udp6://announce-v6.syncthing.net:22027"`
|
||||||
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:"21025"`
|
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21025"`
|
||||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
|
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
|
||||||
|
RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net"`
|
||||||
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"`
|
||||||
|
RelayWithoutGlobalAnn bool `xml:"relayWithoutGlobalAnn" json:"relayWithoutGlobalAnn" default:"false"`
|
||||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
||||||
@@ -346,6 +351,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||||
|
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
||||||
|
|
||||||
if cfg.Version < OldestHandledVersion {
|
if cfg.Version < OldestHandledVersion {
|
||||||
l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)
|
l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)
|
||||||
}
|
}
|
||||||
@@ -369,6 +377,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
|||||||
if cfg.Version == 10 {
|
if cfg.Version == 10 {
|
||||||
convertV10V11(cfg)
|
convertV10V11(cfg)
|
||||||
}
|
}
|
||||||
|
if cfg.Version == 11 {
|
||||||
|
convertV11V12(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// Hash old cleartext passwords
|
// Hash old cleartext passwords
|
||||||
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
||||||
@@ -420,9 +431,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
|||||||
cfg.Options.ReconnectIntervalS = 5
|
cfg.Options.ReconnectIntervalS = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
|
||||||
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
|
||||||
|
|
||||||
if cfg.GUI.APIKey == "" {
|
if cfg.GUI.APIKey == "" {
|
||||||
cfg.GUI.APIKey = randomString(32)
|
cfg.GUI.APIKey = randomString(32)
|
||||||
}
|
}
|
||||||
@@ -467,6 +475,38 @@ func convertV10V11(cfg *Configuration) {
|
|||||||
cfg.Version = 11
|
cfg.Version = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertV11V12(cfg *Configuration) {
|
||||||
|
// Change listen address schema
|
||||||
|
for i, addr := range cfg.Options.ListenAddress {
|
||||||
|
if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") {
|
||||||
|
cfg.Options.ListenAddress[i] = fmt.Sprintf("tcp://%s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, device := range cfg.Devices {
|
||||||
|
for j, addr := range device.Addresses {
|
||||||
|
if addr != "dynamic" && addr != "" {
|
||||||
|
cfg.Devices[i].Addresses[j] = fmt.Sprintf("tcp://%s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use new discovery server
|
||||||
|
for i, addr := range cfg.Options.GlobalAnnServers {
|
||||||
|
if addr == "udp4://announce.syncthing.net:22026" {
|
||||||
|
cfg.Options.GlobalAnnServers[i] = "udp4://announce.syncthing.net:22027"
|
||||||
|
} else if addr == "udp6://announce-v6.syncthing.net:22026" {
|
||||||
|
cfg.Options.GlobalAnnServers[i] = "udp6://announce-v6.syncthing.net:22027"
|
||||||
|
} else if addr == "udp4://194.126.249.5:22026" {
|
||||||
|
cfg.Options.GlobalAnnServers[i] = "udp4://194.126.249.5:22027"
|
||||||
|
} else if addr == "udp6://[2001:470:28:4d6::5]:22026" {
|
||||||
|
cfg.Options.GlobalAnnServers[i] = "udp6://[2001:470:28:4d6::5]:22027"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = 12
|
||||||
|
}
|
||||||
|
|
||||||
func convertV9V10(cfg *Configuration) {
|
func convertV9V10(cfg *Configuration) {
|
||||||
// Enable auto normalization on existing folders.
|
// Enable auto normalization on existing folders.
|
||||||
for i := range cfg.Folders {
|
for i := range cfg.Folders {
|
||||||
|
|||||||
+20
-12
@@ -31,15 +31,19 @@ func init() {
|
|||||||
|
|
||||||
func TestDefaultValues(t *testing.T) {
|
func TestDefaultValues(t *testing.T) {
|
||||||
expected := OptionsConfiguration{
|
expected := OptionsConfiguration{
|
||||||
ListenAddress: []string{"0.0.0.0:22000"},
|
ListenAddress: []string{"tcp://0.0.0.0:22000"},
|
||||||
GlobalAnnServers: []string{"udp4://announce.syncthing.net:22026", "udp6://announce-v6.syncthing.net:22026"},
|
GlobalAnnServers: []string{"udp4://announce.syncthing.net:22027", "udp6://announce-v6.syncthing.net:22027"},
|
||||||
GlobalAnnEnabled: true,
|
GlobalAnnEnabled: true,
|
||||||
LocalAnnEnabled: true,
|
LocalAnnEnabled: true,
|
||||||
LocalAnnPort: 21025,
|
LocalAnnPort: 21025,
|
||||||
LocalAnnMCAddr: "[ff32::5222]:21026",
|
LocalAnnMCAddr: "[ff32::5222]:21026",
|
||||||
|
RelayServers: []string{"dynamic+https://relays.syncthing.net"},
|
||||||
MaxSendKbps: 0,
|
MaxSendKbps: 0,
|
||||||
MaxRecvKbps: 0,
|
MaxRecvKbps: 0,
|
||||||
ReconnectIntervalS: 60,
|
ReconnectIntervalS: 60,
|
||||||
|
RelaysEnabled: true,
|
||||||
|
RelayReconnectIntervalM: 10,
|
||||||
|
RelayWithoutGlobalAnn: false,
|
||||||
StartBrowser: true,
|
StartBrowser: true,
|
||||||
UPnPEnabled: true,
|
UPnPEnabled: true,
|
||||||
UPnPLeaseM: 60,
|
UPnPLeaseM: 60,
|
||||||
@@ -100,13 +104,13 @@ func TestDeviceConfig(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DeviceID: device1,
|
DeviceID: device1,
|
||||||
Name: "node one",
|
Name: "node one",
|
||||||
Addresses: []string{"a"},
|
Addresses: []string{"tcp://a"},
|
||||||
Compression: protocol.CompressMetadata,
|
Compression: protocol.CompressMetadata,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
DeviceID: device4,
|
DeviceID: device4,
|
||||||
Name: "node two",
|
Name: "node two",
|
||||||
Addresses: []string{"b"},
|
Addresses: []string{"tcp://b"},
|
||||||
Compression: protocol.CompressMetadata,
|
Compression: protocol.CompressMetadata,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -142,15 +146,19 @@ func TestNoListenAddress(t *testing.T) {
|
|||||||
|
|
||||||
func TestOverriddenValues(t *testing.T) {
|
func TestOverriddenValues(t *testing.T) {
|
||||||
expected := OptionsConfiguration{
|
expected := OptionsConfiguration{
|
||||||
ListenAddress: []string{":23000"},
|
ListenAddress: []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,
|
||||||
|
RelayWithoutGlobalAnn: true,
|
||||||
StartBrowser: false,
|
StartBrowser: false,
|
||||||
UPnPEnabled: false,
|
UPnPEnabled: false,
|
||||||
UPnPLeaseM: 90,
|
UPnPLeaseM: 90,
|
||||||
@@ -255,15 +263,15 @@ func TestDeviceAddressesStatic(t *testing.T) {
|
|||||||
expected := map[protocol.DeviceID]DeviceConfiguration{
|
expected := map[protocol.DeviceID]DeviceConfiguration{
|
||||||
device1: {
|
device1: {
|
||||||
DeviceID: device1,
|
DeviceID: device1,
|
||||||
Addresses: []string{"192.0.2.1", "192.0.2.2"},
|
Addresses: []string{"tcp://192.0.2.1", "tcp://192.0.2.2"},
|
||||||
},
|
},
|
||||||
device2: {
|
device2: {
|
||||||
DeviceID: device2,
|
DeviceID: device2,
|
||||||
Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"},
|
Addresses: []string{"tcp://192.0.2.3:6070", "tcp://[2001:db8::42]:4242"},
|
||||||
},
|
},
|
||||||
device3: {
|
device3: {
|
||||||
DeviceID: device3,
|
DeviceID: device3,
|
||||||
Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"},
|
Addresses: []string{"tcp://[2001:db8::44]:4444", "tcp://192.0.2.4:6090"},
|
||||||
},
|
},
|
||||||
device4: {
|
device4: {
|
||||||
DeviceID: device4,
|
DeviceID: device4,
|
||||||
@@ -330,12 +338,12 @@ func TestIssue1750(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options().ListenAddress[0] != ":23000" {
|
if cfg.Options().ListenAddress[0] != "tcp://:23000" {
|
||||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], ":23000")
|
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], "tcp://:23000")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options().ListenAddress[1] != ":23001" {
|
if cfg.Options().ListenAddress[1] != "tcp://:23001" {
|
||||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], ":23001")
|
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], "tcp://:23001")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" {
|
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" {
|
||||||
|
|||||||
+5
@@ -7,10 +7,15 @@
|
|||||||
<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>
|
||||||
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
||||||
|
<relaysEnabled>false</relaysEnabled>
|
||||||
|
<relayReconnectIntervalM>20</relayReconnectIntervalM>
|
||||||
|
<relayWithoutGlobalAnn>true</relayWithoutGlobalAnn>
|
||||||
<startBrowser>false</startBrowser>
|
<startBrowser>false</startBrowser>
|
||||||
<upnpEnabled>false</upnpEnabled>
|
<upnpEnabled>false</upnpEnabled>
|
||||||
<upnpLeaseMinutes>90</upnpLeaseMinutes>
|
<upnpLeaseMinutes>90</upnpLeaseMinutes>
|
||||||
|
|||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<configuration version="12">
|
||||||
|
<folder id="test" path="testdata" ro="true" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
|
||||||
|
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||||
|
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
|
||||||
|
<minDiskFreePct>1</minDiskFreePct>
|
||||||
|
</folder>
|
||||||
|
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
|
||||||
|
<address>tcp://a</address>
|
||||||
|
</device>
|
||||||
|
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
|
||||||
|
<address>tcp://b</address>
|
||||||
|
</device>
|
||||||
|
</configuration>
|
||||||
@@ -114,6 +114,21 @@ func (w *Wrapper) Subscribe(c Committer) {
|
|||||||
w.sMut.Unlock()
|
w.sMut.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unsubscribe de-registers the given handler from any future calls to
|
||||||
|
// configuration changes
|
||||||
|
func (w *Wrapper) Unsubscribe(c Committer) {
|
||||||
|
w.sMut.Lock()
|
||||||
|
for i := range w.subs {
|
||||||
|
if w.subs[i] == c {
|
||||||
|
copy(w.subs[i:], w.subs[i+1:])
|
||||||
|
w.subs[len(w.subs)-1] = nil
|
||||||
|
w.subs = w.subs[:len(w.subs)-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.sMut.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Raw returns the currently wrapped Configuration object.
|
// Raw returns the currently wrapped Configuration object.
|
||||||
func (w *Wrapper) Raw() Configuration {
|
func (w *Wrapper) Raw() Configuration {
|
||||||
return w.cfg
|
return w.cfg
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import (
|
|||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Factory func(*url.URL, *Announce) (Client, error)
|
type Announcer interface {
|
||||||
|
Announcement() Announce
|
||||||
|
}
|
||||||
|
|
||||||
|
type Factory func(*url.URL, Announcer) (Client, error)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
factories = make(map[string]Factory)
|
factories = make(map[string]Factory)
|
||||||
@@ -26,7 +30,7 @@ func Register(proto string, factory Factory) {
|
|||||||
factories[proto] = factory
|
factories[proto] = factory
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(addr string, pkt *Announce) (Client, error) {
|
func New(addr string, announcer Announcer) (Client, error) {
|
||||||
uri, err := url.Parse(addr)
|
uri, err := url.Parse(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -35,7 +39,7 @@ func New(addr string, pkt *Announce) (Client, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
|
return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
|
||||||
}
|
}
|
||||||
client, err := factory(uri, pkt)
|
client, err := factory(uri, announcer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -43,7 +47,7 @@ func New(addr string, pkt *Announce) (Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
Lookup(device protocol.DeviceID) []string
|
Lookup(device protocol.DeviceID) (Announce, error)
|
||||||
StatusOK() bool
|
StatusOK() bool
|
||||||
Address() string
|
Address() string
|
||||||
Stop()
|
Stop()
|
||||||
|
|||||||
+35
-15
@@ -24,6 +24,14 @@ func init() {
|
|||||||
device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
|
device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FakeAnnouncer struct {
|
||||||
|
pkt Announce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeAnnouncer) Announcement() Announce {
|
||||||
|
return f.pkt
|
||||||
|
}
|
||||||
|
|
||||||
func TestUDP4Success(t *testing.T) {
|
func TestUDP4Success(t *testing.T) {
|
||||||
conn, err := net.ListenUDP("udp4", nil)
|
conn, err := net.ListenUDP("udp4", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,18 +41,19 @@ func TestUDP4Success(t *testing.T) {
|
|||||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
port := conn.LocalAddr().(*net.UDPAddr).Port
|
||||||
|
|
||||||
address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
|
address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
|
||||||
pkt := &Announce{
|
pkt := Announce{
|
||||||
Magic: AnnouncementMagic,
|
Magic: AnnouncementMagic,
|
||||||
This: Device{
|
This: Device{
|
||||||
device[:],
|
device[:],
|
||||||
[]Address{{
|
[]string{"tcp://123.123.123.123:1234"},
|
||||||
IP: net.IPv4(123, 123, 123, 123),
|
nil,
|
||||||
Port: 1234,
|
|
||||||
}},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
ann := &FakeAnnouncer{
|
||||||
|
pkt: pkt,
|
||||||
|
}
|
||||||
|
|
||||||
client, err := New(address, pkt)
|
client, err := New(address, ann)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -101,7 +110,12 @@ func TestUDP4Success(t *testing.T) {
|
|||||||
wg := sync.NewWaitGroup()
|
wg := sync.NewWaitGroup()
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
addrs = client.Lookup(device)
|
pkt, err := client.Lookup(device)
|
||||||
|
if err == nil {
|
||||||
|
for _, addr := range pkt.This.Addresses {
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -117,7 +131,7 @@ func TestUDP4Success(t *testing.T) {
|
|||||||
// Wait for the lookup to arrive, verify that the number of answers is correct
|
// Wait for the lookup to arrive, verify that the number of answers is correct
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if len(addrs) != 1 || addrs[0] != "123.123.123.123:1234" {
|
if len(addrs) != 1 || addrs[0] != "tcp://123.123.123.123:1234" {
|
||||||
t.Fatal("Wrong number of answers")
|
t.Fatal("Wrong number of answers")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,18 +148,19 @@ func TestUDP4Failure(t *testing.T) {
|
|||||||
|
|
||||||
address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
|
address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
|
||||||
|
|
||||||
pkt := &Announce{
|
pkt := Announce{
|
||||||
Magic: AnnouncementMagic,
|
Magic: AnnouncementMagic,
|
||||||
This: Device{
|
This: Device{
|
||||||
device[:],
|
device[:],
|
||||||
[]Address{{
|
[]string{"tcp://123.123.123.123:1234"},
|
||||||
IP: net.IPv4(123, 123, 123, 123),
|
nil,
|
||||||
Port: 1234,
|
|
||||||
}},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
ann := &FakeAnnouncer{
|
||||||
|
pkt: pkt,
|
||||||
|
}
|
||||||
|
|
||||||
client, err := New(address, pkt)
|
client, err := New(address, ann)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -197,7 +212,12 @@ func TestUDP4Failure(t *testing.T) {
|
|||||||
wg := sync.NewWaitGroup()
|
wg := sync.NewWaitGroup()
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
addrs = client.Lookup(device)
|
pkt, err := client.Lookup(device)
|
||||||
|
if err == nil {
|
||||||
|
for _, addr := range pkt.This.Addresses {
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
+33
-30
@@ -20,12 +20,13 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
for _, proto := range []string{"udp", "udp4", "udp6"} {
|
for _, proto := range []string{"udp", "udp4", "udp6"} {
|
||||||
Register(proto, func(uri *url.URL, pkt *Announce) (Client, error) {
|
Register(proto, func(uri *url.URL, announcer Announcer) (Client, error) {
|
||||||
c := &UDPClient{
|
c := &UDPClient{
|
||||||
wg: sync.NewWaitGroup(),
|
announcer: announcer,
|
||||||
mut: sync.NewRWMutex(),
|
wg: sync.NewWaitGroup(),
|
||||||
|
mut: sync.NewRWMutex(),
|
||||||
}
|
}
|
||||||
err := c.Start(uri, pkt)
|
err := c.Start(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -37,22 +38,20 @@ func init() {
|
|||||||
type UDPClient struct {
|
type UDPClient struct {
|
||||||
url *url.URL
|
url *url.URL
|
||||||
|
|
||||||
id protocol.DeviceID
|
|
||||||
|
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
listenAddress *net.UDPAddr
|
listenAddress *net.UDPAddr
|
||||||
|
|
||||||
globalBroadcastInterval time.Duration
|
globalBroadcastInterval time.Duration
|
||||||
errorRetryInterval time.Duration
|
errorRetryInterval time.Duration
|
||||||
|
announcer Announcer
|
||||||
|
|
||||||
status bool
|
status bool
|
||||||
mut sync.RWMutex
|
mut sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UDPClient) Start(uri *url.URL, pkt *Announce) error {
|
func (d *UDPClient) Start(uri *url.URL) error {
|
||||||
d.url = uri
|
d.url = uri
|
||||||
d.id = protocol.DeviceIDFromBytes(pkt.This.ID)
|
|
||||||
d.stop = make(chan struct{})
|
d.stop = make(chan struct{})
|
||||||
|
|
||||||
params := uri.Query()
|
params := uri.Query()
|
||||||
@@ -79,11 +78,11 @@ func (d *UDPClient) Start(uri *url.URL, pkt *Announce) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.wg.Add(1)
|
d.wg.Add(1)
|
||||||
go d.broadcast(pkt.MustMarshalXDR())
|
go d.broadcast()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UDPClient) broadcast(pkt []byte) {
|
func (d *UDPClient) broadcast() {
|
||||||
defer d.wg.Done()
|
defer d.wg.Done()
|
||||||
|
|
||||||
conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress)
|
conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress)
|
||||||
@@ -126,7 +125,14 @@ func (d *UDPClient) broadcast(pkt []byte) {
|
|||||||
l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote)
|
l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := conn.WriteTo(pkt, remote)
|
ann := d.announcer.Announcement()
|
||||||
|
pkt, err := ann.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
timer.Reset(d.errorRetryInterval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.WriteTo(pkt, remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err)
|
l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err)
|
||||||
@@ -137,11 +143,13 @@ func (d *UDPClient) broadcast(pkt []byte) {
|
|||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
res := d.Lookup(d.id)
|
pkt, err := d.Lookup(protocol.DeviceIDFromBytes(ann.This.ID))
|
||||||
if debug {
|
if err != nil && debug {
|
||||||
l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, res)
|
l.Debugf("discover %s: broadcast: Self-lookup failed: %v", d.url, err)
|
||||||
|
} else if debug {
|
||||||
|
l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, pkt.This.Addresses)
|
||||||
}
|
}
|
||||||
ok = len(res) > 0
|
ok = len(pkt.This.Addresses) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mut.Lock()
|
d.mut.Lock()
|
||||||
@@ -157,13 +165,13 @@ func (d *UDPClient) broadcast(pkt []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
func (d *UDPClient) Lookup(device protocol.DeviceID) (Announce, error) {
|
||||||
extIP, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
extIP, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||||
}
|
}
|
||||||
return nil
|
return Announce{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP)
|
conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP)
|
||||||
@@ -171,7 +179,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
|||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||||
}
|
}
|
||||||
return nil
|
return Announce{}, err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -180,7 +188,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
|||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||||
}
|
}
|
||||||
return nil
|
return Announce{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
|
buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
|
||||||
@@ -189,7 +197,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
|||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||||
}
|
}
|
||||||
return nil
|
return Announce{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = make([]byte, 2048)
|
buf = make([]byte, 2048)
|
||||||
@@ -197,12 +205,12 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||||
// Expected if the server doesn't know about requested device ID
|
// Expected if the server doesn't know about requested device ID
|
||||||
return nil
|
return Announce{}, err
|
||||||
}
|
}
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||||
}
|
}
|
||||||
return nil
|
return Announce{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pkt Announce
|
var pkt Announce
|
||||||
@@ -211,18 +219,13 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
|||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n]))
|
l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n]))
|
||||||
}
|
}
|
||||||
return nil
|
return Announce{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var addrs []string
|
|
||||||
for _, a := range pkt.This.Addresses {
|
|
||||||
deviceAddr := net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port)))
|
|
||||||
addrs = append(addrs, deviceAddr)
|
|
||||||
}
|
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover %s: Lookup(%s) result: %v", d.url, device, addrs)
|
l.Debugf("discover %s: Lookup(%s) result: %v relays: %v", d.url, device, pkt.This.Addresses, pkt.This.Relays)
|
||||||
}
|
}
|
||||||
return addrs
|
return pkt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UDPClient) Stop() {
|
func (d *UDPClient) Stop() {
|
||||||
|
|||||||
+203
-86
@@ -10,21 +10,26 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/beacon"
|
"github.com/syncthing/syncthing/lib/beacon"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Discoverer struct {
|
type Discoverer struct {
|
||||||
myID protocol.DeviceID
|
myID protocol.DeviceID
|
||||||
listenAddrs []string
|
listenAddrs []string
|
||||||
|
relaySvc *relay.Svc
|
||||||
localBcastIntv time.Duration
|
localBcastIntv time.Duration
|
||||||
localBcastStart time.Time
|
localBcastStart time.Time
|
||||||
cacheLifetime time.Duration
|
cacheLifetime time.Duration
|
||||||
@@ -34,9 +39,10 @@ type Discoverer struct {
|
|||||||
localBcastTick <-chan time.Time
|
localBcastTick <-chan time.Time
|
||||||
forcedBcastTick chan time.Time
|
forcedBcastTick chan time.Time
|
||||||
|
|
||||||
registryLock sync.RWMutex
|
registryLock sync.RWMutex
|
||||||
registry map[protocol.DeviceID][]CacheEntry
|
addressRegistry map[protocol.DeviceID][]CacheEntry
|
||||||
lastLookup map[protocol.DeviceID]time.Time
|
relayRegistry map[protocol.DeviceID][]CacheEntry
|
||||||
|
lastLookup map[protocol.DeviceID]time.Time
|
||||||
|
|
||||||
clients []Client
|
clients []Client
|
||||||
mut sync.RWMutex
|
mut sync.RWMutex
|
||||||
@@ -51,17 +57,19 @@ var (
|
|||||||
ErrIncorrectMagic = errors.New("incorrect magic number")
|
ErrIncorrectMagic = errors.New("incorrect magic number")
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDiscoverer(id protocol.DeviceID, addresses []string) *Discoverer {
|
func NewDiscoverer(id protocol.DeviceID, addresses []string, relaySvc *relay.Svc) *Discoverer {
|
||||||
return &Discoverer{
|
return &Discoverer{
|
||||||
myID: id,
|
myID: id,
|
||||||
listenAddrs: addresses,
|
listenAddrs: addresses,
|
||||||
localBcastIntv: 30 * time.Second,
|
relaySvc: relaySvc,
|
||||||
cacheLifetime: 5 * time.Minute,
|
localBcastIntv: 30 * time.Second,
|
||||||
negCacheCutoff: 3 * time.Minute,
|
cacheLifetime: 5 * time.Minute,
|
||||||
registry: make(map[protocol.DeviceID][]CacheEntry),
|
negCacheCutoff: 3 * time.Minute,
|
||||||
lastLookup: make(map[protocol.DeviceID]time.Time),
|
addressRegistry: make(map[protocol.DeviceID][]CacheEntry),
|
||||||
registryLock: sync.NewRWMutex(),
|
relayRegistry: make(map[protocol.DeviceID][]CacheEntry),
|
||||||
mut: sync.NewRWMutex(),
|
lastLookup: make(map[protocol.DeviceID]time.Time),
|
||||||
|
registryLock: sync.NewRWMutex(),
|
||||||
|
mut: sync.NewRWMutex(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,14 +144,13 @@ func (d *Discoverer) StartGlobal(servers []string, extPort uint16) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.extPort = extPort
|
d.extPort = extPort
|
||||||
pkt := d.announcementPkt()
|
|
||||||
wg := sync.NewWaitGroup()
|
wg := sync.NewWaitGroup()
|
||||||
clients := make(chan Client, len(servers))
|
clients := make(chan Client, len(servers))
|
||||||
for _, address := range servers {
|
for _, address := range servers {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(addr string) {
|
go func(addr string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
client, err := New(addr, pkt)
|
client, err := New(addr, d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Infoln("Error creating discovery client", addr, err)
|
l.Infoln("Error creating discovery client", addr, err)
|
||||||
return
|
return
|
||||||
@@ -184,75 +191,108 @@ func (d *Discoverer) ExtAnnounceOK() map[string]bool {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
|
// Lookup returns a list of addresses the device is available at, as well as
|
||||||
|
// a list of relays the device is supposed to be available on sorted by the
|
||||||
|
// sum of latencies between this device, and the device in question.
|
||||||
|
func (d *Discoverer) Lookup(device protocol.DeviceID) ([]string, []string) {
|
||||||
d.registryLock.RLock()
|
d.registryLock.RLock()
|
||||||
cached := d.filterCached(d.registry[device])
|
cachedAddresses := d.filterCached(d.addressRegistry[device])
|
||||||
|
cachedRelays := d.filterCached(d.relayRegistry[device])
|
||||||
lastLookup := d.lastLookup[device]
|
lastLookup := d.lastLookup[device]
|
||||||
d.registryLock.RUnlock()
|
d.registryLock.RUnlock()
|
||||||
|
|
||||||
d.mut.RLock()
|
d.mut.RLock()
|
||||||
defer d.mut.RUnlock()
|
defer d.mut.RUnlock()
|
||||||
|
|
||||||
if len(cached) > 0 {
|
relays := make([]string, len(cachedRelays))
|
||||||
|
for i := range cachedRelays {
|
||||||
|
relays[i] = cachedRelays[i].Address
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cachedAddresses) > 0 {
|
||||||
// There are cached address entries.
|
// There are cached address entries.
|
||||||
addrs := make([]string, len(cached))
|
addrs := make([]string, len(cachedAddresses))
|
||||||
for i := range cached {
|
for i := range cachedAddresses {
|
||||||
addrs[i] = cached[i].Address
|
addrs[i] = cachedAddresses[i].Address
|
||||||
}
|
}
|
||||||
return addrs
|
return addrs, relays
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Since(lastLookup) < d.negCacheCutoff {
|
if time.Since(lastLookup) < d.negCacheCutoff {
|
||||||
// We have recently tried to lookup this address and failed. Lets
|
// We have recently tried to lookup this address and failed. Lets
|
||||||
// chill for a while.
|
// chill for a while.
|
||||||
return nil
|
return nil, relays
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
|
if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
|
||||||
// Only perform external lookups if we have at least one external
|
// Only perform external lookups if we have at least one external
|
||||||
// server client and one local announcement interval has passed. This is
|
// server client and one local announcement interval has passed. This is
|
||||||
// to avoid finding local peers on their remote address at startup.
|
// to avoid finding local peers on their remote address at startup.
|
||||||
results := make(chan []string, len(d.clients))
|
results := make(chan Announce, len(d.clients))
|
||||||
wg := sync.NewWaitGroup()
|
wg := sync.NewWaitGroup()
|
||||||
for _, client := range d.clients {
|
for _, client := range d.clients {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(c Client) {
|
go func(c Client) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
results <- c.Lookup(device)
|
ann, err := c.Lookup(device)
|
||||||
|
if err == nil {
|
||||||
|
results <- ann
|
||||||
|
}
|
||||||
|
|
||||||
}(client)
|
}(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(results)
|
close(results)
|
||||||
|
|
||||||
cached := []CacheEntry{}
|
cachedAddresses := []CacheEntry{}
|
||||||
seen := make(map[string]struct{})
|
availableRelays := []Relay{}
|
||||||
|
seenAddresses := make(map[string]struct{})
|
||||||
|
seenRelays := make(map[string]struct{})
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
var addrs []string
|
var addrs []string
|
||||||
for result := range results {
|
for result := range results {
|
||||||
for _, addr := range result {
|
for _, addr := range result.This.Addresses {
|
||||||
_, ok := seen[addr]
|
_, ok := seenAddresses[addr]
|
||||||
if !ok {
|
if !ok {
|
||||||
cached = append(cached, CacheEntry{
|
cachedAddresses = append(cachedAddresses, CacheEntry{
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Seen: now,
|
Seen: now,
|
||||||
})
|
})
|
||||||
seen[addr] = struct{}{}
|
seenAddresses[addr] = struct{}{}
|
||||||
addrs = append(addrs, addr)
|
addrs = append(addrs, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, relay := range result.This.Relays {
|
||||||
|
_, ok := seenRelays[relay.Address]
|
||||||
|
if !ok {
|
||||||
|
availableRelays = append(availableRelays, relay)
|
||||||
|
seenRelays[relay.Address] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relays = addressesSortedByLatency(availableRelays)
|
||||||
|
cachedRelays := make([]CacheEntry, len(relays))
|
||||||
|
for i := range relays {
|
||||||
|
cachedRelays[i] = CacheEntry{
|
||||||
|
Address: relays[i],
|
||||||
|
Seen: now,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
d.registryLock.Lock()
|
d.registryLock.Lock()
|
||||||
d.registry[device] = cached
|
d.addressRegistry[device] = cachedAddresses
|
||||||
|
d.relayRegistry[device] = cachedRelays
|
||||||
d.lastLookup[device] = time.Now()
|
d.lastLookup[device] = time.Now()
|
||||||
d.registryLock.Unlock()
|
d.registryLock.Unlock()
|
||||||
|
|
||||||
return addrs
|
return addrs, relays
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, relays
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) Hint(device string, addrs []string) {
|
func (d *Discoverer) Hint(device string, addrs []string) {
|
||||||
@@ -267,8 +307,8 @@ func (d *Discoverer) Hint(device string, addrs []string) {
|
|||||||
|
|
||||||
func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
|
func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
|
||||||
d.registryLock.RLock()
|
d.registryLock.RLock()
|
||||||
devices := make(map[protocol.DeviceID][]CacheEntry, len(d.registry))
|
devices := make(map[protocol.DeviceID][]CacheEntry, len(d.addressRegistry))
|
||||||
for device, addrs := range d.registry {
|
for device, addrs := range d.addressRegistry {
|
||||||
addrsCopy := make([]CacheEntry, len(addrs))
|
addrsCopy := make([]CacheEntry, len(addrs))
|
||||||
copy(addrsCopy, addrs)
|
copy(addrsCopy, addrs)
|
||||||
devices[device] = addrsCopy
|
devices[device] = addrsCopy
|
||||||
@@ -277,41 +317,36 @@ func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
|
|||||||
return devices
|
return devices
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) announcementPkt() *Announce {
|
func (d *Discoverer) Announcement() Announce {
|
||||||
var addrs []Address
|
return d.announcementPkt(true)
|
||||||
if d.extPort != 0 {
|
}
|
||||||
addrs = []Address{{Port: d.extPort}}
|
|
||||||
|
func (d *Discoverer) announcementPkt(allowExternal bool) Announce {
|
||||||
|
var addrs []string
|
||||||
|
if d.extPort != 0 && allowExternal {
|
||||||
|
addrs = []string{fmt.Sprintf("tcp://:%d", d.extPort)}
|
||||||
} else {
|
} else {
|
||||||
for _, astr := range d.listenAddrs {
|
addrs = resolveAddrs(d.listenAddrs)
|
||||||
addr, err := net.ResolveTCPAddr("tcp", astr)
|
}
|
||||||
if err != nil {
|
|
||||||
l.Warnln("discover: %v: not announcing %s", err, astr)
|
var relayAddrs []string
|
||||||
continue
|
if d.relaySvc != nil {
|
||||||
} else if debug {
|
status := d.relaySvc.ClientStatus()
|
||||||
l.Debugf("discover: resolved %s as %#v", astr, addr)
|
for uri, ok := range status {
|
||||||
}
|
if ok {
|
||||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
relayAddrs = append(relayAddrs, uri)
|
||||||
addrs = append(addrs, Address{Port: uint16(addr.Port)})
|
|
||||||
} else if bs := addr.IP.To4(); bs != nil {
|
|
||||||
addrs = append(addrs, Address{IP: bs, Port: uint16(addr.Port)})
|
|
||||||
} else if bs := addr.IP.To16(); bs != nil {
|
|
||||||
addrs = append(addrs, Address{IP: bs, Port: uint16(addr.Port)})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Announce{
|
|
||||||
|
return Announce{
|
||||||
Magic: AnnouncementMagic,
|
Magic: AnnouncementMagic,
|
||||||
This: Device{d.myID[:], addrs},
|
This: Device{d.myID[:], addrs, measureLatency(relayAddrs)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) sendLocalAnnouncements() {
|
func (d *Discoverer) sendLocalAnnouncements() {
|
||||||
var addrs = resolveAddrs(d.listenAddrs)
|
var pkt = d.announcementPkt(false)
|
||||||
|
|
||||||
var pkt = Announce{
|
|
||||||
Magic: AnnouncementMagic,
|
|
||||||
This: Device{d.myID[:], addrs},
|
|
||||||
}
|
|
||||||
msg := pkt.MustMarshalXDR()
|
msg := pkt.MustMarshalXDR()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -363,19 +398,32 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
|
|||||||
d.registryLock.Lock()
|
d.registryLock.Lock()
|
||||||
defer d.registryLock.Unlock()
|
defer d.registryLock.Unlock()
|
||||||
|
|
||||||
current := d.filterCached(d.registry[id])
|
current := d.filterCached(d.addressRegistry[id])
|
||||||
|
|
||||||
orig := current
|
orig := current
|
||||||
|
|
||||||
for _, a := range device.Addresses {
|
for _, deviceAddr := range device.Addresses {
|
||||||
var deviceAddr string
|
uri, err := url.Parse(deviceAddr)
|
||||||
if len(a.IP) > 0 {
|
if err != nil {
|
||||||
deviceAddr = net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port)))
|
if debug {
|
||||||
} else if addr != nil {
|
l.Debugf("discover: Failed to parse address %s: %s", deviceAddr, err)
|
||||||
ua := addr.(*net.UDPAddr)
|
}
|
||||||
ua.Port = int(a.Port)
|
continue
|
||||||
deviceAddr = ua.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(uri.Host)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugf("discover: Failed to split address host %s: %s", deviceAddr, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
uri.Host = net.JoinHostPort(addr.(*net.UDPAddr).IP.String(), port)
|
||||||
|
deviceAddr = uri.String()
|
||||||
|
}
|
||||||
|
|
||||||
for i := range current {
|
for i := range current {
|
||||||
if current[i].Address == deviceAddr {
|
if current[i].Address == deviceAddr {
|
||||||
current[i].Seen = time.Now()
|
current[i].Seen = time.Now()
|
||||||
@@ -393,7 +441,7 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
|
|||||||
l.Debugf("discover: Caching %s addresses: %v", id, current)
|
l.Debugf("discover: Caching %s addresses: %v", id, current)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.registry[id] = current
|
d.addressRegistry[id] = current
|
||||||
|
|
||||||
if len(current) > len(orig) {
|
if len(current) > len(orig) {
|
||||||
addrs := make([]string, len(current))
|
addrs := make([]string, len(current))
|
||||||
@@ -413,7 +461,7 @@ func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
|
|||||||
for i := 0; i < len(c); {
|
for i := 0; i < len(c); {
|
||||||
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
|
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("discover: Removing cached address %s - seen %v ago", c[i].Address, ago)
|
l.Debugf("discover: Removing cached entry %s - seen %v ago", c[i].Address, ago)
|
||||||
}
|
}
|
||||||
c[i] = c[len(c)-1]
|
c[i] = c[len(c)-1]
|
||||||
c = c[:len(c)-1]
|
c = c[:len(c)-1]
|
||||||
@@ -424,30 +472,99 @@ func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func addrToAddr(addr *net.TCPAddr) Address {
|
func addrToAddr(addr *net.TCPAddr) string {
|
||||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||||
return Address{Port: uint16(addr.Port)}
|
return fmt.Sprintf(":%d", addr.Port)
|
||||||
} else if bs := addr.IP.To4(); bs != nil {
|
} else if bs := addr.IP.To4(); bs != nil {
|
||||||
return Address{IP: bs, Port: uint16(addr.Port)}
|
return fmt.Sprintf("%s:%d", bs.String(), addr.Port)
|
||||||
} else if bs := addr.IP.To16(); bs != nil {
|
} else if bs := addr.IP.To16(); bs != nil {
|
||||||
return Address{IP: bs, Port: uint16(addr.Port)}
|
return fmt.Sprintf("[%s]:%d", bs.String(), addr.Port)
|
||||||
}
|
}
|
||||||
return Address{}
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAddrs(addrs []string) []Address {
|
func resolveAddrs(addrs []string) []string {
|
||||||
var raddrs []Address
|
var raddrs []string
|
||||||
for _, addrStr := range addrs {
|
for _, addrStr := range addrs {
|
||||||
addrRes, err := net.ResolveTCPAddr("tcp", addrStr)
|
uri, err := url.Parse(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
addr := addrToAddr(addrRes)
|
addr := addrToAddr(addrRes)
|
||||||
if len(addr.IP) > 0 {
|
if len(addr) > 0 {
|
||||||
raddrs = append(raddrs, addr)
|
uri.Host = addr
|
||||||
} else {
|
raddrs = append(raddrs, uri.String())
|
||||||
raddrs = append(raddrs, Address{Port: addr.Port})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return raddrs
|
return raddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func measureLatency(relayAdresses []string) []Relay {
|
||||||
|
relays := make([]Relay, 0, len(relayAdresses))
|
||||||
|
for i, addr := range relayAdresses {
|
||||||
|
relay := Relay{
|
||||||
|
Address: addr,
|
||||||
|
Latency: int32(time.Hour / time.Millisecond),
|
||||||
|
}
|
||||||
|
relays = append(relays, relay)
|
||||||
|
|
||||||
|
if latency, err := getLatencyForURL(addr); err == nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugf("Relay %s latency %s", addr, latency)
|
||||||
|
}
|
||||||
|
relays[i].Latency = int32(latency / time.Millisecond)
|
||||||
|
} else {
|
||||||
|
l.Debugf("Failed to get relay %s latency %s", addr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return relays
|
||||||
|
}
|
||||||
|
|
||||||
|
// addressesSortedByLatency adds local latency to the relay, and sorts them
|
||||||
|
// by sum latency, and returns the addresses.
|
||||||
|
func addressesSortedByLatency(input []Relay) []string {
|
||||||
|
relays := make([]Relay, len(input))
|
||||||
|
copy(relays, input)
|
||||||
|
for i, relay := range relays {
|
||||||
|
if latency, err := getLatencyForURL(relay.Address); err == nil {
|
||||||
|
relays[i].Latency += int32(latency / time.Millisecond)
|
||||||
|
} else {
|
||||||
|
relays[i].Latency += int32(time.Hour / time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(relayList(relays))
|
||||||
|
|
||||||
|
addresses := make([]string, 0, len(relays))
|
||||||
|
for _, relay := range relays {
|
||||||
|
addresses = append(addresses, relay.Address)
|
||||||
|
}
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLatencyForURL(addr string) (time.Duration, error) {
|
||||||
|
uri, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return osutil.TCPPing(uri.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
type relayList []Relay
|
||||||
|
|
||||||
|
func (l relayList) Len() int {
|
||||||
|
return len(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l relayList) Less(a, b int) bool {
|
||||||
|
return l[a].Latency < l[b].Latency
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l relayList) Swap(a, b int) {
|
||||||
|
l[a], l[b] = l[b], l[a]
|
||||||
|
}
|
||||||
@@ -18,15 +18,15 @@ import (
|
|||||||
type DummyClient struct {
|
type DummyClient struct {
|
||||||
url *url.URL
|
url *url.URL
|
||||||
lookups []protocol.DeviceID
|
lookups []protocol.DeviceID
|
||||||
lookupRet []string
|
lookupRet Announce
|
||||||
stops int
|
stops int
|
||||||
statusRet bool
|
statusRet bool
|
||||||
statusChecks int
|
statusChecks int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DummyClient) Lookup(device protocol.DeviceID) []string {
|
func (c *DummyClient) Lookup(device protocol.DeviceID) (Announce, error) {
|
||||||
c.lookups = append(c.lookups, device)
|
c.lookups = append(c.lookups, device)
|
||||||
return c.lookupRet
|
return c.lookupRet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DummyClient) StatusOK() bool {
|
func (c *DummyClient) StatusOK() bool {
|
||||||
@@ -45,34 +45,58 @@ func (c *DummyClient) Address() string {
|
|||||||
func TestGlobalDiscovery(t *testing.T) {
|
func TestGlobalDiscovery(t *testing.T) {
|
||||||
c1 := &DummyClient{
|
c1 := &DummyClient{
|
||||||
statusRet: false,
|
statusRet: false,
|
||||||
lookupRet: []string{"test.com:1234"},
|
lookupRet: Announce{
|
||||||
|
Magic: AnnouncementMagic,
|
||||||
|
This: Device{
|
||||||
|
ID: protocol.LocalDeviceID[:],
|
||||||
|
Addresses: []string{"test.com:1234"},
|
||||||
|
Relays: nil,
|
||||||
|
},
|
||||||
|
Extra: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
c2 := &DummyClient{
|
c2 := &DummyClient{
|
||||||
statusRet: true,
|
statusRet: true,
|
||||||
lookupRet: []string{},
|
lookupRet: Announce{
|
||||||
|
Magic: AnnouncementMagic,
|
||||||
|
This: Device{
|
||||||
|
ID: protocol.LocalDeviceID[:],
|
||||||
|
Addresses: nil,
|
||||||
|
Relays: nil,
|
||||||
|
},
|
||||||
|
Extra: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
c3 := &DummyClient{
|
c3 := &DummyClient{
|
||||||
statusRet: true,
|
statusRet: true,
|
||||||
lookupRet: []string{"best.com:2345"},
|
lookupRet: Announce{
|
||||||
|
Magic: AnnouncementMagic,
|
||||||
|
This: Device{
|
||||||
|
ID: protocol.LocalDeviceID[:],
|
||||||
|
Addresses: []string{"best.com:2345"},
|
||||||
|
Relays: nil,
|
||||||
|
},
|
||||||
|
Extra: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
clients := []*DummyClient{c1, c2}
|
clients := []*DummyClient{c1, c2}
|
||||||
|
|
||||||
Register("test1", func(uri *url.URL, pkt *Announce) (Client, error) {
|
Register("test1", func(uri *url.URL, ann Announcer) (Client, error) {
|
||||||
c := clients[0]
|
c := clients[0]
|
||||||
clients = clients[1:]
|
clients = clients[1:]
|
||||||
c.url = uri
|
c.url = uri
|
||||||
return c, nil
|
return c, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
Register("test2", func(uri *url.URL, pkt *Announce) (Client, error) {
|
Register("test2", func(uri *url.URL, ann Announcer) (Client, error) {
|
||||||
c3.url = uri
|
c3.url = uri
|
||||||
return c3, nil
|
return c3, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
d := NewDiscoverer(device, []string{})
|
d := NewDiscoverer(device, []string{}, nil)
|
||||||
d.localBcastStart = time.Time{}
|
d.localBcastStart = time.Time{}
|
||||||
servers := []string{
|
servers := []string{
|
||||||
"test1://123.123.123.123:1234",
|
"test1://123.123.123.123:1234",
|
||||||
@@ -93,7 +117,7 @@ func TestGlobalDiscovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs := d.Lookup(device)
|
addrs, _ := d.Lookup(device)
|
||||||
if len(addrs) != 2 {
|
if len(addrs) != 2 {
|
||||||
t.Fatal("Wrong number of addresses", addrs)
|
t.Fatal("Wrong number of addresses", addrs)
|
||||||
}
|
}
|
||||||
@@ -117,7 +141,7 @@ func TestGlobalDiscovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs = d.Lookup(device)
|
addrs, _ = d.Lookup(device)
|
||||||
if len(addrs) != 2 {
|
if len(addrs) != 2 {
|
||||||
t.Fatal("Wrong number of addresses", addrs)
|
t.Fatal("Wrong number of addresses", addrs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
package discover
|
package discover
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AnnouncementMagic = 0x9D79BC39
|
AnnouncementMagic = 0x9D79BC40
|
||||||
QueryMagic = 0x2CA856F5
|
QueryMagic = 0x2CA856F6
|
||||||
)
|
)
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -25,12 +25,13 @@ type Announce struct {
|
|||||||
Extra []Device // max:16
|
Extra []Device // max:16
|
||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Relay struct {
|
||||||
ID []byte // max:32
|
Address string // max:256
|
||||||
Addresses []Address // max:16
|
Latency int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type Address struct {
|
type Device struct {
|
||||||
IP []byte // max:16
|
ID []byte // max:32
|
||||||
Port uint16
|
Addresses []string // max:16
|
||||||
|
Relays []Relay // max:16
|
||||||
}
|
}
|
||||||
+106
-79
@@ -187,6 +187,80 @@ func (o *Announce) DecodeXDRFrom(xr *xdr.Reader) 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
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Address |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Address (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Latency |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
struct Relay {
|
||||||
|
string Address<256>;
|
||||||
|
int Latency;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (o Relay) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.EncodeXDRInto(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Relay) MarshalXDR() ([]byte, error) {
|
||||||
|
return o.AppendXDR(make([]byte, 0, 128))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Relay) MustMarshalXDR() []byte {
|
||||||
|
bs, err := o.MarshalXDR()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Relay) AppendXDR(bs []byte) ([]byte, error) {
|
||||||
|
var aw = xdr.AppendWriter(bs)
|
||||||
|
var xw = xdr.NewWriter(&aw)
|
||||||
|
_, err := o.EncodeXDRInto(xw)
|
||||||
|
return []byte(aw), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||||
|
if l := len(o.Address); l > 256 {
|
||||||
|
return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 256)
|
||||||
|
}
|
||||||
|
xw.WriteString(o.Address)
|
||||||
|
xw.WriteUint32(uint32(o.Latency))
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Relay) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Relay) UnmarshalXDR(bs []byte) error {
|
||||||
|
var br = bytes.NewReader(bs)
|
||||||
|
var xr = xdr.NewReader(br)
|
||||||
|
return o.DecodeXDRFrom(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||||
|
o.Address = xr.ReadStringMax(256)
|
||||||
|
o.Latency = int32(xr.ReadUint32())
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
Device Structure:
|
Device Structure:
|
||||||
|
|
||||||
0 1 2 3
|
0 1 2 3
|
||||||
@@ -200,15 +274,24 @@ Device Structure:
|
|||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| Number of Addresses |
|
| Number of Addresses |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Addresses |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
/ /
|
/ /
|
||||||
\ Zero or more Address Structures \
|
\ Addresses (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Number of Relays |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Zero or more Relay Structures \
|
||||||
/ /
|
/ /
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
struct Device {
|
struct Device {
|
||||||
opaque ID<32>;
|
opaque ID<32>;
|
||||||
Address Addresses<16>;
|
string Addresses<16>;
|
||||||
|
Relay Relays<16>;
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@@ -247,7 +330,14 @@ func (o Device) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
|||||||
}
|
}
|
||||||
xw.WriteUint32(uint32(len(o.Addresses)))
|
xw.WriteUint32(uint32(len(o.Addresses)))
|
||||||
for i := range o.Addresses {
|
for i := range o.Addresses {
|
||||||
_, err := o.Addresses[i].EncodeXDRInto(xw)
|
xw.WriteString(o.Addresses[i])
|
||||||
|
}
|
||||||
|
if l := len(o.Relays); l > 16 {
|
||||||
|
return xw.Tot(), xdr.ElementSizeExceeded("Relays", l, 16)
|
||||||
|
}
|
||||||
|
xw.WriteUint32(uint32(len(o.Relays)))
|
||||||
|
for i := range o.Relays {
|
||||||
|
_, err := o.Relays[i].EncodeXDRInto(xw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xw.Tot(), err
|
return xw.Tot(), err
|
||||||
}
|
}
|
||||||
@@ -275,83 +365,20 @@ func (o *Device) DecodeXDRFrom(xr *xdr.Reader) error {
|
|||||||
if _AddressesSize > 16 {
|
if _AddressesSize > 16 {
|
||||||
return xdr.ElementSizeExceeded("Addresses", _AddressesSize, 16)
|
return xdr.ElementSizeExceeded("Addresses", _AddressesSize, 16)
|
||||||
}
|
}
|
||||||
o.Addresses = make([]Address, _AddressesSize)
|
o.Addresses = make([]string, _AddressesSize)
|
||||||
for i := range o.Addresses {
|
for i := range o.Addresses {
|
||||||
(&o.Addresses[i]).DecodeXDRFrom(xr)
|
o.Addresses[i] = xr.ReadString()
|
||||||
|
}
|
||||||
|
_RelaysSize := int(xr.ReadUint32())
|
||||||
|
if _RelaysSize < 0 {
|
||||||
|
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
|
||||||
|
}
|
||||||
|
if _RelaysSize > 16 {
|
||||||
|
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
|
||||||
|
}
|
||||||
|
o.Relays = make([]Relay, _RelaysSize)
|
||||||
|
for i := range o.Relays {
|
||||||
|
(&o.Relays[i]).DecodeXDRFrom(xr)
|
||||||
}
|
}
|
||||||
return xr.Error()
|
return xr.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Address 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
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| Length of IP |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
/ /
|
|
||||||
\ IP (variable length) \
|
|
||||||
/ /
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| 0x0000 | Port |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
|
|
||||||
|
|
||||||
struct Address {
|
|
||||||
opaque IP<16>;
|
|
||||||
unsigned int Port;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
func (o Address) EncodeXDR(w io.Writer) (int, error) {
|
|
||||||
var xw = xdr.NewWriter(w)
|
|
||||||
return o.EncodeXDRInto(xw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Address) MarshalXDR() ([]byte, error) {
|
|
||||||
return o.AppendXDR(make([]byte, 0, 128))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Address) MustMarshalXDR() []byte {
|
|
||||||
bs, err := o.MarshalXDR()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return bs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Address) AppendXDR(bs []byte) ([]byte, error) {
|
|
||||||
var aw = xdr.AppendWriter(bs)
|
|
||||||
var xw = xdr.NewWriter(&aw)
|
|
||||||
_, err := o.EncodeXDRInto(xw)
|
|
||||||
return []byte(aw), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Address) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
|
||||||
if l := len(o.IP); l > 16 {
|
|
||||||
return xw.Tot(), xdr.ElementSizeExceeded("IP", l, 16)
|
|
||||||
}
|
|
||||||
xw.WriteBytes(o.IP)
|
|
||||||
xw.WriteUint16(o.Port)
|
|
||||||
return xw.Tot(), xw.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Address) DecodeXDR(r io.Reader) error {
|
|
||||||
xr := xdr.NewReader(r)
|
|
||||||
return o.DecodeXDRFrom(xr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Address) UnmarshalXDR(bs []byte) error {
|
|
||||||
var br = bytes.NewReader(bs)
|
|
||||||
var xr = xdr.NewReader(br)
|
|
||||||
return o.DecodeXDRFrom(xr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Address) DecodeXDRFrom(xr *xdr.Reader) error {
|
|
||||||
o.IP = xr.ReadBytesMax(16)
|
|
||||||
o.Port = xr.ReadUint16()
|
|
||||||
return xr.Error()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// 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/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntermediateConnection struct {
|
||||||
|
Conn *tls.Conn
|
||||||
|
ConnType ConnectionType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
net.Conn
|
||||||
|
protocol.Connection
|
||||||
|
Type ConnectionType
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectionTypeBasicAccept ConnectionType = iota
|
||||||
|
ConnectionTypeBasicDial
|
||||||
|
ConnectionTypeRelayAccept
|
||||||
|
ConnectionTypeRelayDial
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnectionType int
|
||||||
|
|
||||||
|
func (t ConnectionType) String() string {
|
||||||
|
switch t {
|
||||||
|
case ConnectionTypeBasicAccept:
|
||||||
|
return "basic-accept"
|
||||||
|
case ConnectionTypeBasicDial:
|
||||||
|
return "basic-dial"
|
||||||
|
case ConnectionTypeRelayAccept:
|
||||||
|
return "relay-accept"
|
||||||
|
case ConnectionTypeRelayDial:
|
||||||
|
return "relay-dial"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ConnectionType) IsDirect() bool {
|
||||||
|
return t == ConnectionTypeBasicAccept || t == ConnectionTypeBasicDial
|
||||||
|
}
|
||||||
+30
-30
@@ -87,10 +87,9 @@ 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
|
||||||
|
|
||||||
protoConn map[protocol.DeviceID]protocol.Connection
|
conn map[protocol.DeviceID]Connection
|
||||||
rawConn map[protocol.DeviceID]io.Closer
|
|
||||||
deviceVer map[protocol.DeviceID]string
|
deviceVer map[protocol.DeviceID]string
|
||||||
pmut sync.RWMutex // protects protoConn and rawConn
|
pmut sync.RWMutex // protects conn and deviceVer
|
||||||
|
|
||||||
reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
|
reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
|
||||||
rvmut sync.RWMutex // protects reqValidationCache
|
rvmut sync.RWMutex // protects reqValidationCache
|
||||||
@@ -130,8 +129,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
|
|||||||
folderIgnores: make(map[string]*ignore.Matcher),
|
folderIgnores: make(map[string]*ignore.Matcher),
|
||||||
folderRunners: make(map[string]service),
|
folderRunners: make(map[string]service),
|
||||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||||
protoConn: make(map[protocol.DeviceID]protocol.Connection),
|
conn: make(map[protocol.DeviceID]Connection),
|
||||||
rawConn: make(map[protocol.DeviceID]io.Closer),
|
|
||||||
deviceVer: make(map[protocol.DeviceID]string),
|
deviceVer: make(map[protocol.DeviceID]string),
|
||||||
reqValidationCache: make(map[string]time.Time),
|
reqValidationCache: make(map[string]time.Time),
|
||||||
|
|
||||||
@@ -221,6 +219,7 @@ type ConnectionInfo struct {
|
|||||||
protocol.Statistics
|
protocol.Statistics
|
||||||
Address string
|
Address string
|
||||||
ClientVersion string
|
ClientVersion string
|
||||||
|
Type ConnectionType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
||||||
@@ -229,6 +228,7 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
|||||||
"inBytesTotal": info.InBytesTotal,
|
"inBytesTotal": info.InBytesTotal,
|
||||||
"outBytesTotal": info.OutBytesTotal,
|
"outBytesTotal": info.OutBytesTotal,
|
||||||
"address": info.Address,
|
"address": info.Address,
|
||||||
|
"type": info.Type.String(),
|
||||||
"clientVersion": info.ClientVersion,
|
"clientVersion": info.ClientVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -243,14 +243,15 @@ func (m *Model) ConnectionStats() map[string]interface{} {
|
|||||||
m.fmut.RLock()
|
m.fmut.RLock()
|
||||||
|
|
||||||
var res = make(map[string]interface{})
|
var res = make(map[string]interface{})
|
||||||
conns := make(map[string]ConnectionInfo, len(m.protoConn))
|
conns := make(map[string]ConnectionInfo, len(m.conn))
|
||||||
for device, conn := range m.protoConn {
|
for device, conn := range m.conn {
|
||||||
ci := ConnectionInfo{
|
ci := ConnectionInfo{
|
||||||
Statistics: conn.Statistics(),
|
Statistics: conn.Statistics(),
|
||||||
ClientVersion: m.deviceVer[device],
|
ClientVersion: m.deviceVer[device],
|
||||||
}
|
}
|
||||||
if nc, ok := m.rawConn[device].(remoteAddrer); ok {
|
if addr := m.conn[device].RemoteAddr(); addr != nil {
|
||||||
ci.Address = nc.RemoteAddr().String()
|
ci.Address = addr.String()
|
||||||
|
ci.Type = conn.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
conns[device.String()] = ci
|
conns[device.String()] = ci
|
||||||
@@ -586,8 +587,12 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
|||||||
"clientVersion": cm.ClientVersion,
|
"clientVersion": cm.ClientVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn, ok := m.rawConn[deviceID].(*tls.Conn); ok {
|
if conn, ok := m.conn[deviceID]; ok {
|
||||||
event["addr"] = conn.RemoteAddr().String()
|
event["type"] = conn.Type.String()
|
||||||
|
addr := conn.RemoteAddr()
|
||||||
|
if addr != nil {
|
||||||
|
event["addr"] = addr.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.pmut.Unlock()
|
m.pmut.Unlock()
|
||||||
@@ -693,12 +698,11 @@ func (m *Model) Close(device protocol.DeviceID, err error) {
|
|||||||
}
|
}
|
||||||
m.fmut.RUnlock()
|
m.fmut.RUnlock()
|
||||||
|
|
||||||
conn, ok := m.rawConn[device]
|
conn, ok := m.conn[device]
|
||||||
if ok {
|
if ok {
|
||||||
closeRawConn(conn)
|
closeRawConn(conn)
|
||||||
}
|
}
|
||||||
delete(m.protoConn, device)
|
delete(m.conn, device)
|
||||||
delete(m.rawConn, device)
|
|
||||||
delete(m.deviceVer, device)
|
delete(m.deviceVer, device)
|
||||||
m.pmut.Unlock()
|
m.pmut.Unlock()
|
||||||
}
|
}
|
||||||
@@ -860,7 +864,7 @@ func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
|
|||||||
// ConnectedTo returns true if we are connected to the named device.
|
// ConnectedTo returns true if we are connected to the named device.
|
||||||
func (m *Model) ConnectedTo(deviceID protocol.DeviceID) bool {
|
func (m *Model) ConnectedTo(deviceID protocol.DeviceID) bool {
|
||||||
m.pmut.RLock()
|
m.pmut.RLock()
|
||||||
_, ok := m.protoConn[deviceID]
|
_, ok := m.conn[deviceID]
|
||||||
m.pmut.RUnlock()
|
m.pmut.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
m.deviceWasSeen(deviceID)
|
m.deviceWasSeen(deviceID)
|
||||||
@@ -927,28 +931,24 @@ func (m *Model) SetIgnores(folder string, content []string) error {
|
|||||||
// 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(rawConn io.Closer, protoConn protocol.Connection) {
|
func (m *Model) AddConnection(conn Connection) {
|
||||||
deviceID := protoConn.ID()
|
deviceID := conn.ID()
|
||||||
|
|
||||||
m.pmut.Lock()
|
m.pmut.Lock()
|
||||||
if _, ok := m.protoConn[deviceID]; ok {
|
if _, ok := m.conn[deviceID]; ok {
|
||||||
panic("add existing device")
|
panic("add existing device")
|
||||||
}
|
}
|
||||||
m.protoConn[deviceID] = protoConn
|
m.conn[deviceID] = conn
|
||||||
if _, ok := m.rawConn[deviceID]; ok {
|
|
||||||
panic("add existing device")
|
|
||||||
}
|
|
||||||
m.rawConn[deviceID] = rawConn
|
|
||||||
|
|
||||||
protoConn.Start()
|
conn.Start()
|
||||||
|
|
||||||
cm := m.clusterConfig(deviceID)
|
cm := m.clusterConfig(deviceID)
|
||||||
protoConn.ClusterConfig(cm)
|
conn.ClusterConfig(cm)
|
||||||
|
|
||||||
m.fmut.RLock()
|
m.fmut.RLock()
|
||||||
for _, folder := range m.deviceFolders[deviceID] {
|
for _, folder := range m.deviceFolders[deviceID] {
|
||||||
fs := m.folderFiles[folder]
|
fs := m.folderFiles[folder]
|
||||||
go sendIndexes(protoConn, folder, fs, m.folderIgnores[folder])
|
go sendIndexes(conn, folder, fs, m.folderIgnores[folder])
|
||||||
}
|
}
|
||||||
m.fmut.RUnlock()
|
m.fmut.RUnlock()
|
||||||
m.pmut.Unlock()
|
m.pmut.Unlock()
|
||||||
@@ -1114,7 +1114,7 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
|
|||||||
|
|
||||||
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, flags uint32, options []protocol.Option) ([]byte, error) {
|
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, flags uint32, options []protocol.Option) ([]byte, error) {
|
||||||
m.pmut.RLock()
|
m.pmut.RLock()
|
||||||
nc, ok := m.protoConn[deviceID]
|
nc, ok := m.conn[deviceID]
|
||||||
m.pmut.RUnlock()
|
m.pmut.RUnlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -1640,7 +1640,7 @@ func (m *Model) Availability(folder, file string) []protocol.DeviceID {
|
|||||||
|
|
||||||
availableDevices := []protocol.DeviceID{}
|
availableDevices := []protocol.DeviceID{}
|
||||||
for _, device := range fs.Availability(file) {
|
for _, device := range fs.Availability(file) {
|
||||||
_, ok := m.protoConn[device]
|
_, ok := m.conn[device]
|
||||||
if ok {
|
if ok {
|
||||||
availableDevices = append(availableDevices, device)
|
availableDevices = append(availableDevices, device)
|
||||||
}
|
}
|
||||||
@@ -1764,7 +1764,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
// folder.
|
// folder.
|
||||||
m.pmut.Lock()
|
m.pmut.Lock()
|
||||||
for _, dev := range cfg.DeviceIDs() {
|
for _, dev := range cfg.DeviceIDs() {
|
||||||
if conn, ok := m.rawConn[dev]; ok {
|
if conn, ok := m.conn[dev]; ok {
|
||||||
closeRawConn(conn)
|
closeRawConn(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1812,7 +1812,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
// disconnect it so that we start sharing the folder with it.
|
// disconnect it so that we start sharing the folder with it.
|
||||||
// We close the underlying connection and let the normal error
|
// We close the underlying connection and let the normal error
|
||||||
// handling kick in to clean up and reconnect.
|
// handling kick in to clean up and reconnect.
|
||||||
if conn, ok := m.rawConn[dev]; ok {
|
if conn, ok := m.conn[dev]; ok {
|
||||||
closeRawConn(conn)
|
closeRawConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -281,7 +282,11 @@ func BenchmarkRequest(b *testing.B) {
|
|||||||
id: device1,
|
id: device1,
|
||||||
requestData: []byte("some data to return"),
|
requestData: []byte("some data to return"),
|
||||||
}
|
}
|
||||||
m.AddConnection(fc, fc)
|
m.AddConnection(Connection{
|
||||||
|
&net.TCPConn{},
|
||||||
|
fc,
|
||||||
|
ConnectionTypeBasicAccept,
|
||||||
|
})
|
||||||
m.Index(device1, "default", files, 0, nil)
|
m.Index(device1, "default", files, 0, nil)
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
@@ -314,6 +319,18 @@ func TestDeviceRename(t *testing.T) {
|
|||||||
|
|
||||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||||
|
|
||||||
|
fc := FakeConnection{
|
||||||
|
id: device1,
|
||||||
|
requestData: []byte("some data to return"),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.AddConnection(Connection{
|
||||||
|
&net.TCPConn{},
|
||||||
|
fc,
|
||||||
|
ConnectionTypeBasicAccept,
|
||||||
|
})
|
||||||
|
|
||||||
m.ServeBackground()
|
m.ServeBackground()
|
||||||
if cfg.Devices()[device1].Name != "" {
|
if cfg.Devices()[device1].Name != "" {
|
||||||
t.Errorf("Device already has a name")
|
t.Errorf("Device already has a name")
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/calmh/du"
|
"github.com/calmh/du"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
@@ -221,3 +223,21 @@ func DiskFreePercentage(path string) (freePct float64, err error) {
|
|||||||
u, err := du.Get(path)
|
u, err := du.Get(path)
|
||||||
return (float64(u.FreeBytes) / float64(u.TotalBytes)) * 100, err
|
return (float64(u.FreeBytes) / float64(u.TotalBytes)) * 100, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTCPOptions sets syncthings default TCP options on a TCP connection
|
||||||
|
func SetTCPOptions(conn *net.TCPConn) error {
|
||||||
|
var err error
|
||||||
|
if err = conn.SetLinger(0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = conn.SetNoDelay(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = conn.SetKeepAlive(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// 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 osutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TCPPing returns the duration required to establish a TCP connection
|
||||||
|
// to the given host. ICMP packets require root priviledges, hence why we use
|
||||||
|
// tcp.
|
||||||
|
func TCPPing(address string) (time.Duration, error) {
|
||||||
|
dialer := net.Dialer{
|
||||||
|
Deadline: time.Now().Add(time.Second),
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
conn, err := dialer.Dial("tcp", address)
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
return time.Since(start), err
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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/calmh/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debug = strings.Contains(os.Getenv("STTRACE"), "relay") || os.Getenv("STTRACE") == "all"
|
||||||
|
l = logger.DefaultLogger
|
||||||
|
)
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
// 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"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/relaysrv/client"
|
||||||
|
"github.com/syncthing/relaysrv/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
|
|
||||||
|
"github.com/thejerf/suture"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) *Svc {
|
||||||
|
svc := &Svc{
|
||||||
|
Supervisor: suture.New("Svc", suture.Spec{
|
||||||
|
Log: func(log string) {
|
||||||
|
if debug {
|
||||||
|
l.Infoln(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.ProtocolClient),
|
||||||
|
mut: sync.NewRWMutex(),
|
||||||
|
|
||||||
|
invitations: make(chan protocol.SessionInvitation),
|
||||||
|
}
|
||||||
|
|
||||||
|
rcfg := cfg.Raw()
|
||||||
|
svc.CommitConfiguration(rcfg, rcfg)
|
||||||
|
cfg.Subscribe(svc)
|
||||||
|
|
||||||
|
receiver := &invitationReceiver{
|
||||||
|
tlsCfg: tlsCfg,
|
||||||
|
conns: conns,
|
||||||
|
invitations: svc.invitations,
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.receiverToken = svc.Add(receiver)
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Svc struct {
|
||||||
|
*suture.Supervisor
|
||||||
|
cfg *config.Wrapper
|
||||||
|
tlsCfg *tls.Config
|
||||||
|
|
||||||
|
receiverToken suture.ServiceToken
|
||||||
|
tokens map[string]suture.ServiceToken
|
||||||
|
clients map[string]*client.ProtocolClient
|
||||||
|
mut sync.RWMutex
|
||||||
|
invitations chan protocol.SessionInvitation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Svc) 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 *Svc) 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 {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Failed to parse relay address", addr, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing[uri.String()] = uri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand dynamic addresses into a set of relays
|
||||||
|
for key, uri := range existing {
|
||||||
|
if uri.Scheme != "dynamic+http" && uri.Scheme != "dynamic+https" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(existing, key)
|
||||||
|
|
||||||
|
uri.Scheme = uri.Scheme[8:]
|
||||||
|
|
||||||
|
data, err := http.Get(uri.String())
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Failed to lookup dynamic relays", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var ann dynamicAnnouncement
|
||||||
|
err = json.NewDecoder(data.Body).Decode(&ann)
|
||||||
|
data.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Failed to lookup dynamic relays", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, relayAnn := range ann.Relays {
|
||||||
|
ruri, err := url.Parse(relayAnn.URL)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Failed to parse dynamic relay address", relayAnn.URL, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Found", ruri, "via", uri)
|
||||||
|
}
|
||||||
|
existing[ruri.String()] = ruri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, uri := range existing {
|
||||||
|
_, ok := s.tokens[key]
|
||||||
|
if !ok {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Connecting to relay", uri)
|
||||||
|
}
|
||||||
|
c := client.NewProtocolClient(uri, s.tlsCfg.Certificates, s.invitations)
|
||||||
|
s.tokens[key] = s.Add(c)
|
||||||
|
s.mut.Lock()
|
||||||
|
s.clients[key] = c
|
||||||
|
s.mut.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, token := range s.tokens {
|
||||||
|
_, ok := existing[key]
|
||||||
|
if !ok {
|
||||||
|
err := s.Remove(token)
|
||||||
|
delete(s.tokens, key)
|
||||||
|
s.mut.Lock()
|
||||||
|
delete(s.clients, key)
|
||||||
|
s.mut.Unlock()
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Disconnecting from relay", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Svc) ClientStatus() map[string]bool {
|
||||||
|
s.mut.RLock()
|
||||||
|
status := make(map[string]bool, len(s.clients))
|
||||||
|
for uri, client := range s.clients {
|
||||||
|
status[uri] = client.StatusOK()
|
||||||
|
}
|
||||||
|
s.mut.RUnlock()
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
type invitationReceiver struct {
|
||||||
|
invitations chan protocol.SessionInvitation
|
||||||
|
tlsCfg *tls.Config
|
||||||
|
conns chan<- model.IntermediateConnection
|
||||||
|
stop chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *invitationReceiver) Serve() {
|
||||||
|
if r.stop != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.stop = make(chan struct{})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case inv := <-r.invitations:
|
||||||
|
if debug {
|
||||||
|
l.Debugln("Received relay invitation", inv)
|
||||||
|
}
|
||||||
|
conn, err := client.JoinSession(inv)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
l.Debugf("Failed to join relay session %s: %v", inv, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <- model.IntermediateConnection{
|
||||||
|
tc, model.ConnectionTypeRelayAccept,
|
||||||
|
}
|
||||||
|
case <-r.stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *invitationReceiver) Stop() {
|
||||||
|
if r.stop == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.stop <- struct{}{}
|
||||||
|
r.stop = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dynamicAnnouncement struct {
|
||||||
|
Relays []relayAnnouncement `json:"relays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type relayAnnouncement struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
Reference in new issue
Block a user