all: Support multiple device connections (fixes #141) (#8918)

This adds the ability to have multiple concurrent connections to a single device. This is primarily useful when the network has multiple physical links for aggregated bandwidth. A single connection will never see a higher rate than a single link can give, but multiple connections are load-balanced over multiple links.

It is also incidentally useful for older multi-core CPUs, where bandwidth could be limited by the TLS performance of a single CPU core -- using multiple connections achieves concurrency in the required crypto calculations...

Co-authored-by: Simon Frei <freisim93@gmail.com>
Co-authored-by: tomasz1986 <twilczynski@naver.com>
Co-authored-by: bt90 <btom1990@googlemail.com>
This commit is contained in:
Jakob Borg 2023-09-06 12:52:01 +02:00 committed by GitHub
parent 38bbdebffa
commit c6334e61aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1640 additions and 933 deletions

View File

@ -15,6 +15,7 @@ import (
"sort" "sort"
"time" "time"
"github.com/syncthing/syncthing/lib/sliceutil"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage" "github.com/syndtr/goleveldb/leveldb/storage"
"github.com/syndtr/goleveldb/leveldb/util" "github.com/syndtr/goleveldb/leveldb/util"
@ -352,14 +353,7 @@ func expire(addrs []DatabaseAddress, now int64) []DatabaseAddress {
i := 0 i := 0
for i < len(addrs) { for i < len(addrs) {
if addrs[i].Expires < now { if addrs[i].Expires < now {
// This item is expired. Replace it with the last in the list addrs = sliceutil.RemoveAndZero(addrs, i)
// (noop if we are at the last item).
addrs[i] = addrs[len(addrs)-1]
// Wipe the last item of the list to release references to
// strings and stuff.
addrs[len(addrs)-1] = DatabaseAddress{}
// Shorten the slice.
addrs = addrs[:len(addrs)-1]
continue continue
} }
i++ i++

View File

@ -185,7 +185,7 @@ func TestFilter(t *testing.T) {
}, },
{ {
a: []DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}}, a: []DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}},
b: []DatabaseAddress{{Address: "d", Expires: 15}, {Address: "b", Expires: 15}}, // gets reordered b: []DatabaseAddress{{Address: "b", Expires: 15}, {Address: "d", Expires: 15}},
}, },
} }

View File

@ -871,6 +871,13 @@
</span> </span>
</td> </td>
</tr> </tr>
<tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fas fa-fw fa-random"></span>&nbsp;<span translate>Number of Connections</span></th>
<td class="text-right">
<span ng-if="connections[deviceCfg.deviceID].secondary.length">1 + {{connections[deviceCfg.deviceID].secondary.length | alwaysNumber}}</span>
<span ng-if="!connections[deviceCfg.deviceID].secondary.length">1</span>
</td>
</tr>
<tr ng-if="deviceCfg.allowedNetworks.length > 0"> <tr ng-if="deviceCfg.allowedNetworks.length > 0">
<th><span class="fas fa-fw fa-filter"></span>&nbsp;<span translate>Allowed Networks</span></th> <th><span class="fas fa-fw fa-filter"></span>&nbsp;<span translate>Allowed Networks</span></th>
<td class="text-right"> <td class="text-right">

View File

@ -137,11 +137,22 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row form-group"> <div class="row">
<div class="col-md-12"> <div class="col-md-6" ng-class="{'has-error': deviceEditor.numConnections.$invalid && deviceEditor.numConnections.$dirty}">
<label translate>Connection Management</label>
<div class="row">
<span class="col-md-8" translate>Number of Connections</span>
<div class="col-md-4">
<input name="numConnections" id="numConnections" class="form-control" type="number" pattern="\d+" ng-model="currentDevice.numConnections" min="0" />
</div>
</div>
<p class="help-block" ng-if="!deviceEditor.numConnections.$valid && deviceEditor.numConnections.$dirty" translate>The number of connections must be a non-negative number.</p>
<p class="help-block" ng-if="deviceEditor.numConnections.$valid || deviceEditor.numConnections.$pristine" translate>When set to more than one on both devices, Syncthing will attempt to establish multiple concurrent connections. If the values differ, the highest will be used. Set to zero to let Syncthing decide.</p>
</div>
<div class="col-md-6 form-group">
<label translate>Device rate limits</label> <label translate>Device rate limits</label>
<div class="row"> <div class="row">
<div class="col-md-6" ng-class="{'has-error': deviceEditor.maxRecvKbps.$invalid && deviceEditor.maxRecvKbps.$dirty}"> <div class="col-md-12" ng-class="{'has-error': deviceEditor.maxRecvKbps.$invalid && deviceEditor.maxRecvKbps.$dirty}">
<div class="row"> <div class="row">
<span class="col-md-8" translate>Incoming Rate Limit (KiB/s)</span> <span class="col-md-8" translate>Incoming Rate Limit (KiB/s)</span>
<div class="col-md-4"> <div class="col-md-4">
@ -150,7 +161,7 @@
</div> </div>
<p class="help-block" ng-if="!deviceEditor.maxRecvKbps.$valid && deviceEditor.maxRecvKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p> <p class="help-block" ng-if="!deviceEditor.maxRecvKbps.$valid && deviceEditor.maxRecvKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p>
</div> </div>
<div class="col-md-6" ng-class="{'has-error': deviceEditor.maxSendKbps.$invalid && deviceEditor.maxSendKbps.$dirty}"> <div class="col-md-12" ng-class="{'has-error': deviceEditor.maxSendKbps.$invalid && deviceEditor.maxSendKbps.$dirty}">
<div class="row"> <div class="row">
<span class="col-md-8" translate>Outgoing Rate Limit (KiB/s)</span> <span class="col-md-8" translate>Outgoing Rate Limit (KiB/s)</span>
<div class="col-md-4"> <div class="col-md-4">
@ -158,6 +169,7 @@
</div> </div>
</div> </div>
<p class="help-block" ng-if="!deviceEditor.maxSendKbps.$valid && deviceEditor.maxSendKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p> <p class="help-block" ng-if="!deviceEditor.maxSendKbps.$valid && deviceEditor.maxSendKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p>
<p class="help-block" ng-if="(deviceEditor.maxSendKbps.$valid || deviceEditor.maxSendKbps.$pristine) && (deviceEditor.maxRecvKbps.$valid || deviceEditor.maxRecvKbps.$pristine)">The rate limit is applied to the accumulated traffic of all connections to this device.</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1230,6 +1230,14 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
promhttp.Handler().ServeHTTP(wr, &http.Request{Method: http.MethodGet}) promhttp.Handler().ServeHTTP(wr, &http.Request{Method: http.MethodGet})
files = append(files, fileEntry{name: "metrics.txt", data: buf.Bytes()}) files = append(files, fileEntry{name: "metrics.txt", data: buf.Bytes()})
// Connection data as JSON
connStats := s.model.ConnectionStats()
if connStatsJSON, err := json.MarshalIndent(connStats, "", " "); err != nil {
l.Warnln("Support bundle: failed to serialize connection-stats.json.txt", err)
} else {
files = append(files, fileEntry{name: "connection-stats.json.txt", data: connStatsJSON})
}
// Heap and CPU Proofs as a pprof extension // Heap and CPU Proofs as a pprof extension
var heapBuffer, cpuBuffer bytes.Buffer var heapBuffer, cpuBuffer bytes.Buffer
filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, build.Version, time.Now().Format("150405")) // hhmmss filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, build.Version, time.Now().Format("150405")) // hhmmss
@ -1607,7 +1615,7 @@ func (s *service) getPeerCompletion(w http.ResponseWriter, _ *http.Request) {
for _, folder := range s.cfg.Folders() { for _, folder := range s.cfg.Folders() {
for _, device := range folder.DeviceIDs() { for _, device := range folder.DeviceIDs() {
deviceStr := device.String() deviceStr := device.String()
if _, ok := s.model.Connection(device); ok { if s.model.ConnectedTo(device) {
comp, err := s.model.Completion(device, folder.ID) comp, err := s.model.Completion(device, folder.ID)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -25,6 +25,7 @@ import (
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/netutil" "github.com/syncthing/syncthing/lib/netutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sliceutil"
"github.com/syncthing/syncthing/lib/structutil" "github.com/syncthing/syncthing/lib/structutil"
) )
@ -564,8 +565,7 @@ func ensureNoUntrustedTrustingSharing(f *FolderConfiguration, devices []FolderDe
} }
if devCfg := existingDevices[dev.DeviceID]; devCfg.Untrusted { if devCfg := existingDevices[dev.DeviceID]; devCfg.Untrusted {
l.Warnf("Folder %s (%s) is shared in trusted mode with untrusted device %s (%s); unsharing.", f.ID, f.Label, dev.DeviceID.Short(), devCfg.Name) l.Warnf("Folder %s (%s) is shared in trusted mode with untrusted device %s (%s); unsharing.", f.ID, f.Label, dev.DeviceID.Short(), devCfg.Name)
copy(devices[i:], devices[i+1:]) devices = sliceutil.RemoveAndZero(devices, i)
devices = devices[:len(devices)-1]
i-- i--
} }
} }
@ -601,9 +601,7 @@ func filterURLSchemePrefix(addrs []string, prefix string) []string {
continue continue
} }
if strings.HasPrefix(uri.Scheme, prefix) { if strings.HasPrefix(uri.Scheme, prefix) {
// Remove this entry addrs = sliceutil.RemoveAndZero(addrs, i)
copy(addrs[i:], addrs[i+1:])
addrs = addrs[:len(addrs)-1]
i-- i--
} }
} }

View File

@ -11,6 +11,8 @@ import (
"sort" "sort"
) )
const defaultNumConnections = 1 // number of connections to use by default; may change in the future.
func (cfg DeviceConfiguration) Copy() DeviceConfiguration { func (cfg DeviceConfiguration) Copy() DeviceConfiguration {
c := cfg c := cfg
c.Addresses = make([]string, len(cfg.Addresses)) c.Addresses = make([]string, len(cfg.Addresses))
@ -49,6 +51,17 @@ func (cfg *DeviceConfiguration) prepare(sharedFolders []string) {
} }
} }
func (cfg *DeviceConfiguration) NumConnections() int {
switch {
case cfg.RawNumConnections == 0:
return defaultNumConnections
case cfg.RawNumConnections < 0:
return 1
default:
return cfg.RawNumConnections
}
}
func (cfg *DeviceConfiguration) IgnoredFolder(folder string) bool { func (cfg *DeviceConfiguration) IgnoredFolder(folder string) bool {
for _, ignoredFolder := range cfg.IgnoredFolders { for _, ignoredFolder := range cfg.IgnoredFolders {
if ignoredFolder.ID == folder { if ignoredFolder.ID == folder {

View File

@ -28,7 +28,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type DeviceConfiguration struct { type DeviceConfiguration struct {
DeviceID github_com_syncthing_syncthing_lib_protocol.DeviceID `protobuf:"bytes,1,opt,name=device_id,json=deviceId,proto3,customtype=github.com/syncthing/syncthing/lib/protocol.DeviceID" json:"deviceID" xml:"id,attr" nodefault:"true"` DeviceID github_com_syncthing_syncthing_lib_protocol.DeviceID `protobuf:"bytes,1,opt,name=device_id,json=deviceId,proto3,customtype=github.com/syncthing/syncthing/lib/protocol.DeviceID" json:"deviceID" xml:"id,attr" nodefault:"true"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name" xml:"name,attr,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name" xml:"name,attr,omitempty"`
Addresses []string `protobuf:"bytes,3,rep,name=addresses,proto3" json:"addresses" xml:"address,omitempty" default:"dynamic"` Addresses []string `protobuf:"bytes,3,rep,name=addresses,proto3" json:"addresses" xml:"address,omitempty"`
Compression protocol.Compression `protobuf:"varint,4,opt,name=compression,proto3,enum=protocol.Compression" json:"compression" xml:"compression,attr"` Compression protocol.Compression `protobuf:"varint,4,opt,name=compression,proto3,enum=protocol.Compression" json:"compression" xml:"compression,attr"`
CertName string `protobuf:"bytes,5,opt,name=cert_name,json=certName,proto3" json:"certName" xml:"certName,attr,omitempty"` CertName string `protobuf:"bytes,5,opt,name=cert_name,json=certName,proto3" json:"certName" xml:"certName,attr,omitempty"`
Introducer bool `protobuf:"varint,6,opt,name=introducer,proto3" json:"introducer" xml:"introducer,attr"` Introducer bool `protobuf:"varint,6,opt,name=introducer,proto3" json:"introducer" xml:"introducer,attr"`
@ -44,6 +44,7 @@ type DeviceConfiguration struct {
MaxRequestKiB int `protobuf:"varint,16,opt,name=max_request_kib,json=maxRequestKib,proto3,casttype=int" json:"maxRequestKiB" xml:"maxRequestKiB"` MaxRequestKiB int `protobuf:"varint,16,opt,name=max_request_kib,json=maxRequestKib,proto3,casttype=int" json:"maxRequestKiB" xml:"maxRequestKiB"`
Untrusted bool `protobuf:"varint,17,opt,name=untrusted,proto3" json:"untrusted" xml:"untrusted"` Untrusted bool `protobuf:"varint,17,opt,name=untrusted,proto3" json:"untrusted" xml:"untrusted"`
RemoteGUIPort int `protobuf:"varint,18,opt,name=remote_gui_port,json=remoteGuiPort,proto3,casttype=int" json:"remoteGUIPort" xml:"remoteGUIPort"` RemoteGUIPort int `protobuf:"varint,18,opt,name=remote_gui_port,json=remoteGuiPort,proto3,casttype=int" json:"remoteGUIPort" xml:"remoteGUIPort"`
RawNumConnections int `protobuf:"varint,19,opt,name=num_connections,json=numConnections,proto3,casttype=int" json:"numConnections" xml:"numConnections"`
} }
func (m *DeviceConfiguration) Reset() { *m = DeviceConfiguration{} } func (m *DeviceConfiguration) Reset() { *m = DeviceConfiguration{} }
@ -88,72 +89,74 @@ func init() {
} }
var fileDescriptor_744b782bd13071dd = []byte{ var fileDescriptor_744b782bd13071dd = []byte{
// 1026 bytes of a gzipped FileDescriptorProto // 1057 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xbf, 0x6f, 0xdb, 0x46, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0x41, 0x6f, 0xe3, 0x44,
0x18, 0x15, 0xeb, 0xc4, 0xb6, 0xce, 0x3f, 0x64, 0xd3, 0x88, 0xc3, 0x18, 0x88, 0x4e, 0x50, 0x35, 0x14, 0x8e, 0xe9, 0x6e, 0xb7, 0x99, 0x6d, 0x9b, 0xc6, 0x65, 0xbb, 0xde, 0x4a, 0x9b, 0x89, 0x42,
0x28, 0x68, 0x22, 0x17, 0x6e, 0x27, 0xa3, 0x2d, 0x50, 0xc6, 0x68, 0x63, 0x18, 0x4d, 0x5c, 0x16, 0x0e, 0x41, 0xec, 0xa6, 0xa8, 0x70, 0xaa, 0x00, 0x89, 0xb4, 0x82, 0xad, 0x2a, 0xba, 0x65, 0x10,
0x5d, 0xbc, 0xb0, 0x24, 0xef, 0xac, 0x1c, 0x2c, 0xf2, 0x58, 0xf2, 0xa8, 0x58, 0x40, 0xff, 0x80, 0x97, 0xdd, 0x83, 0x71, 0x3c, 0xd3, 0xac, 0xd5, 0x78, 0xc6, 0xd8, 0xe3, 0xb4, 0x95, 0x38, 0x72,
0x76, 0x2b, 0x02, 0x74, 0xea, 0x92, 0xf6, 0xdf, 0xe8, 0xd0, 0xd5, 0x9b, 0x35, 0x16, 0x1d, 0x0e, 0x80, 0x1b, 0xaa, 0xc4, 0x89, 0xcb, 0xc2, 0xdf, 0xe0, 0xc0, 0xb5, 0xb7, 0xe6, 0x08, 0x1c, 0x46,
0x88, 0xbd, 0x71, 0x29, 0xc0, 0x31, 0x53, 0x71, 0x77, 0x14, 0x45, 0xca, 0x51, 0x50, 0xa0, 0x1b, 0xda, 0xf4, 0xe6, 0xa3, 0x8f, 0x9c, 0xd0, 0x8c, 0x1d, 0xc7, 0x76, 0x37, 0x2b, 0x24, 0x6e, 0x9e,
0xef, 0xbd, 0x77, 0xef, 0xdd, 0xf7, 0xe9, 0xbb, 0x13, 0xe8, 0x0c, 0x88, 0xbb, 0xeb, 0xd1, 0xe0, 0xef, 0x7b, 0xf3, 0x7d, 0xf3, 0x9e, 0xdf, 0x9b, 0x01, 0xed, 0xa1, 0xd3, 0xdf, 0xb2, 0x19, 0x3d,
0x94, 0xf4, 0x77, 0x11, 0x1e, 0x12, 0x0f, 0xab, 0x45, 0x12, 0x39, 0x8c, 0xd0, 0xa0, 0x17, 0x46, 0x76, 0x06, 0x5b, 0x98, 0x8c, 0x1c, 0x9b, 0x24, 0x8b, 0xd0, 0xb7, 0xb8, 0xc3, 0x68, 0xd7, 0xf3,
0x94, 0x51, 0x7d, 0x51, 0x81, 0x3b, 0xdb, 0x42, 0x2d, 0x21, 0x8f, 0x0e, 0x76, 0x5d, 0x1c, 0x2a, 0x19, 0x67, 0xfa, 0x62, 0x02, 0x6e, 0x6e, 0xc8, 0x68, 0x05, 0xd9, 0x6c, 0xb8, 0xd5, 0x27, 0x5e,
0x7e, 0xe7, 0x5e, 0xc9, 0x85, 0xba, 0x31, 0x8e, 0x86, 0x18, 0xe5, 0x54, 0x1d, 0x9f, 0x33, 0xf5, 0xc2, 0x6f, 0x3e, 0xc8, 0xa9, 0xb0, 0x7e, 0x40, 0xfc, 0x11, 0xc1, 0x29, 0x55, 0x25, 0x67, 0x3c,
0xd9, 0xfe, 0x67, 0x03, 0x6c, 0x1d, 0xc8, 0x8c, 0xc7, 0xe5, 0x0c, 0xfd, 0x4f, 0x0d, 0xd4, 0x55, 0xf9, 0x6c, 0xfd, 0x55, 0x07, 0xeb, 0x7b, 0xca, 0x63, 0x37, 0xef, 0xa1, 0xff, 0xa1, 0x81, 0x6a,
0xb6, 0x4d, 0x90, 0xa1, 0xb5, 0xb4, 0xee, 0xaa, 0xf9, 0x9b, 0x76, 0xc1, 0x61, 0xed, 0x6f, 0x0e, 0xe2, 0x6d, 0x3a, 0xd8, 0xd0, 0x9a, 0x5a, 0x67, 0xb9, 0xf7, 0xab, 0x76, 0x29, 0x60, 0xe5, 0x6f,
0x3f, 0xee, 0x13, 0xf6, 0x3c, 0x71, 0x7b, 0x1e, 0xf5, 0x77, 0xe3, 0x51, 0xe0, 0xb1, 0xe7, 0x24, 0x01, 0x3f, 0x1c, 0x38, 0xfc, 0x45, 0xd8, 0xef, 0xda, 0xcc, 0xdd, 0x0a, 0xce, 0xa9, 0xcd, 0x5f,
0xe8, 0x97, 0xbe, 0xca, 0x27, 0xea, 0x29, 0xf7, 0xc3, 0x83, 0x2b, 0x0e, 0x97, 0x27, 0xdf, 0x29, 0x38, 0x74, 0x90, 0xfb, 0xca, 0x9f, 0xa8, 0x9b, 0xa8, 0xef, 0xef, 0x4d, 0x04, 0x5c, 0x9a, 0x7e,
0x87, 0xcb, 0x28, 0xff, 0xce, 0x38, 0x6c, 0x9e, 0xfb, 0x83, 0xfd, 0x36, 0x41, 0x0f, 0x1d, 0xc6, 0x47, 0x02, 0x2e, 0xe1, 0xf4, 0x3b, 0x16, 0xb0, 0x71, 0xe6, 0x0e, 0x77, 0x5a, 0x0e, 0x7e, 0x64,
0xa2, 0x76, 0x2b, 0xa0, 0x08, 0x9f, 0x3a, 0xc9, 0x80, 0xed, 0xb7, 0x59, 0x94, 0xe0, 0x76, 0x7a, 0x71, 0xee, 0xb7, 0x9a, 0x94, 0x61, 0x72, 0x6c, 0x85, 0x43, 0xbe, 0xd3, 0xe2, 0x7e, 0x48, 0x5a,
0xd9, 0x59, 0xca, 0xc9, 0xec, 0xb2, 0x53, 0x6c, 0xfc, 0x71, 0xdc, 0xd1, 0x5e, 0x8e, 0x3b, 0x85, 0xd1, 0x55, 0xfb, 0x4e, 0x4a, 0xc6, 0x57, 0xed, 0x6c, 0xe3, 0x0f, 0xe3, 0xb6, 0x76, 0x31, 0x6e,
0xe9, 0xab, 0x71, 0x47, 0xb3, 0x26, 0x2c, 0xd2, 0x8f, 0xc1, 0xad, 0xc0, 0xf1, 0xb1, 0xf1, 0x5e, 0x67, 0xa2, 0x2f, 0xc7, 0x6d, 0x0d, 0x4d, 0x59, 0xac, 0x1f, 0x81, 0x5b, 0xd4, 0x72, 0x89, 0xf1,
0x4b, 0xeb, 0xd6, 0xcd, 0x4f, 0x52, 0x0e, 0xe5, 0x3a, 0xe3, 0xf0, 0x9e, 0x8c, 0x13, 0x0b, 0xe9, 0x56, 0x53, 0xeb, 0x54, 0x7b, 0x1f, 0x45, 0x02, 0xaa, 0x75, 0x2c, 0xe0, 0x03, 0x65, 0x27, 0x17,
0xf9, 0x90, 0xfa, 0x84, 0x61, 0x3f, 0x64, 0x23, 0x91, 0xb4, 0xf5, 0x16, 0xdc, 0x92, 0x3b, 0xf5, 0x4a, 0xf3, 0x11, 0x73, 0x1d, 0x4e, 0x5c, 0x8f, 0x9f, 0x4b, 0xa7, 0xf5, 0xd7, 0xe0, 0x48, 0xed,
0x73, 0x50, 0x77, 0x10, 0x8a, 0x70, 0x1c, 0xe3, 0xd8, 0x58, 0x68, 0x2d, 0x74, 0xeb, 0xe6, 0x49, 0xd4, 0x9f, 0x83, 0xaa, 0x85, 0xb1, 0x4f, 0x82, 0x80, 0x04, 0xc6, 0x42, 0x73, 0xa1, 0x53, 0xed,
0xca, 0xe1, 0x14, 0xcc, 0x38, 0x7c, 0x20, 0xbd, 0x73, 0xa4, 0xe4, 0xdc, 0x2a, 0x4a, 0x42, 0xa3, 0x7d, 0x1c, 0x09, 0x38, 0x03, 0x63, 0x01, 0xef, 0x2b, 0xed, 0x14, 0x29, 0x2a, 0xd7, 0x6f, 0xa0,
0xc0, 0xf1, 0x89, 0x27, 0xb2, 0x36, 0x6f, 0xe8, 0xde, 0x5c, 0x76, 0x96, 0x72, 0x81, 0x35, 0xf5, 0x68, 0xb6, 0x55, 0x1f, 0x81, 0xbb, 0x36, 0x73, 0x3d, 0xb9, 0x72, 0x18, 0x35, 0x6e, 0x35, 0xb5,
0xd5, 0x87, 0x60, 0xc5, 0xa3, 0x7e, 0x28, 0x56, 0x84, 0x06, 0xc6, 0xad, 0x96, 0xd6, 0x5d, 0xdf, 0xce, 0xea, 0xf6, 0xbd, 0x6e, 0x56, 0xc6, 0xdd, 0x19, 0xa9, 0x5c, 0xf3, 0xd1, 0xb1, 0x80, 0x1b,
0xbb, 0xd3, 0x2b, 0x7a, 0xfc, 0x78, 0x4a, 0x9a, 0x9f, 0xa6, 0x1c, 0x96, 0xd5, 0x19, 0x87, 0xdb, 0xca, 0x37, 0x87, 0x25, 0xb5, 0x8c, 0xae, 0xda, 0x6b, 0x65, 0x10, 0xe5, 0xb7, 0xea, 0x04, 0x54,
0xf2, 0x50, 0x25, 0x4c, 0x35, 0x3a, 0xbd, 0xec, 0x6c, 0xcc, 0x82, 0x56, 0x79, 0xab, 0x8e, 0x41, 0x6d, 0xe2, 0x73, 0x53, 0xd5, 0xea, 0xb6, 0xaa, 0xd5, 0x13, 0xf9, 0x7b, 0x24, 0x78, 0x98, 0xd4,
0xdd, 0xc3, 0x11, 0xb3, 0x65, 0x23, 0x6f, 0xcb, 0x46, 0x3e, 0x11, 0xbf, 0x9d, 0x00, 0x9f, 0xaa, 0xeb, 0x61, 0xa2, 0x9d, 0x02, 0xaf, 0xa9, 0xd9, 0xfd, 0x39, 0x1c, 0xca, 0x54, 0xf4, 0x67, 0x00,
0x66, 0xde, 0x57, 0xde, 0x39, 0xf0, 0x96, 0x86, 0xde, 0x9d, 0xc3, 0x59, 0x85, 0x8b, 0x7e, 0x02, 0x38, 0x94, 0xfb, 0x0c, 0x87, 0x36, 0xf1, 0x8d, 0xc5, 0xa6, 0xd6, 0x59, 0xea, 0xed, 0x44, 0x02,
0x00, 0x09, 0x58, 0x44, 0x51, 0xe2, 0xe1, 0xc8, 0x58, 0x6c, 0x69, 0xdd, 0x65, 0x73, 0x3f, 0xe5, 0xe6, 0xd0, 0x58, 0xc0, 0x7b, 0x49, 0x23, 0x64, 0x50, 0x96, 0x44, 0xad, 0x84, 0xa1, 0xdc, 0x3e,
0xb0, 0x84, 0x66, 0x1c, 0xde, 0x51, 0x53, 0x52, 0x40, 0x45, 0x11, 0x8d, 0x19, 0xcc, 0x2a, 0xed, 0xfd, 0x37, 0x0d, 0x6c, 0x06, 0x27, 0x8e, 0x67, 0x4e, 0x31, 0xd9, 0xc1, 0xa6, 0x4f, 0x5c, 0x36,
0xd3, 0x7f, 0xd7, 0xc0, 0x4e, 0x7c, 0x46, 0x42, 0x7b, 0x82, 0x89, 0xf1, 0xb6, 0x23, 0xec, 0xd3, 0xb2, 0x86, 0x81, 0x71, 0x47, 0x99, 0xe1, 0x48, 0x40, 0x43, 0x46, 0xed, 0xe7, 0x82, 0x50, 0x1a,
0xa1, 0x33, 0x88, 0x8d, 0x25, 0x19, 0x86, 0x52, 0x0e, 0x0d, 0xa1, 0x3a, 0x2c, 0x89, 0xac, 0x5c, 0x13, 0x0b, 0xf8, 0x8e, 0xb2, 0x9e, 0x17, 0x90, 0x1d, 0xe4, 0xe1, 0x1b, 0x23, 0xd0, 0x5c, 0x07,
0x93, 0x71, 0xf8, 0xbe, 0x8c, 0x9e, 0x27, 0x28, 0x0e, 0x72, 0xff, 0x9d, 0x0a, 0x6b, 0x6e, 0x82, 0xfd, 0x77, 0x0d, 0xac, 0x64, 0x67, 0xc6, 0x66, 0xff, 0xdc, 0x58, 0x52, 0x43, 0xf5, 0xf3, 0xff,
0xfe, 0x87, 0x06, 0xd6, 0x8a, 0x33, 0x23, 0xdb, 0x1d, 0x19, 0xcb, 0xf2, 0xc6, 0xfd, 0xf2, 0xbf, 0x1a, 0xaa, 0x48, 0xc0, 0xe5, 0x99, 0x6a, 0xef, 0x3c, 0x16, 0xb0, 0x53, 0xac, 0x21, 0xee, 0x9d,
0x6e, 0x5c, 0xca, 0xe1, 0xea, 0xd4, 0xd5, 0x1c, 0x65, 0x1c, 0x76, 0xab, 0x3d, 0x44, 0xe6, 0x68, 0xcf, 0x1f, 0xab, 0xfa, 0x8d, 0x30, 0x39, 0x54, 0x6a, 0x90, 0x0a, 0xb2, 0xfa, 0x36, 0x58, 0xf4,
0xfe, 0x9d, 0xdb, 0xbc, 0x21, 0x13, 0x37, 0x4e, 0xde, 0xb2, 0x8a, 0xad, 0xbe, 0x07, 0x16, 0x43, 0xac, 0x30, 0x20, 0xd8, 0xa8, 0xaa, 0x6a, 0x6e, 0x46, 0x02, 0xa6, 0x48, 0x2c, 0xe0, 0xb2, 0xb2,
0x27, 0x89, 0x31, 0x32, 0xea, 0xb2, 0x9b, 0x3b, 0x29, 0x87, 0x39, 0x92, 0x71, 0xb8, 0x2a, 0x23, 0x4c, 0x96, 0x2d, 0x94, 0xe2, 0xfa, 0x77, 0x60, 0xcd, 0x1a, 0x0e, 0xd9, 0x29, 0xc1, 0x26, 0x25,
0xd5, 0xb2, 0x6d, 0xe5, 0xb8, 0xfe, 0x03, 0xd8, 0x70, 0x06, 0x03, 0xfa, 0x02, 0x23, 0x3b, 0xc0, 0xfc, 0x94, 0xf9, 0x27, 0x81, 0x01, 0xd4, 0xd4, 0x7c, 0x19, 0x09, 0x58, 0x4b, 0xb9, 0xc3, 0x94,
0xec, 0x05, 0x8d, 0xce, 0x62, 0x03, 0xc8, 0x2b, 0xf5, 0x75, 0xca, 0x61, 0x23, 0xe7, 0x9e, 0xe6, 0xca, 0xae, 0x81, 0x22, 0x5e, 0x6c, 0x34, 0x63, 0x1e, 0x89, 0xca, 0x72, 0xfa, 0x37, 0x60, 0xdd,
0x54, 0xf1, 0x46, 0x54, 0xf1, 0xea, 0xa0, 0x19, 0xf3, 0x48, 0x6b, 0xd6, 0x4e, 0xff, 0x0e, 0x6c, 0x0a, 0x39, 0x33, 0x2d, 0xdb, 0x26, 0x1e, 0x37, 0x8f, 0xd9, 0x10, 0x13, 0x3f, 0x30, 0xee, 0xaa,
0x39, 0x09, 0xa3, 0xb6, 0xe3, 0x79, 0x38, 0x64, 0xf6, 0x29, 0x1d, 0x20, 0x1c, 0xc5, 0xc6, 0x8a, 0xe3, 0xbf, 0x1f, 0x09, 0x58, 0x97, 0xf4, 0xa7, 0x8a, 0xfd, 0x2c, 0x21, 0x67, 0xe3, 0x5b, 0x66,
0x3c, 0xfe, 0x87, 0x29, 0x87, 0x9b, 0x82, 0xfe, 0x5c, 0xb2, 0x5f, 0x28, 0x32, 0xe3, 0xf0, 0xae, 0x5a, 0xe8, 0x66, 0xb4, 0xfe, 0x14, 0xac, 0xb8, 0xd6, 0x99, 0x19, 0x10, 0x8a, 0xcd, 0x93, 0xbe,
0x3a, 0xc2, 0x2c, 0xd3, 0xb6, 0x6e, 0xaa, 0xf5, 0x67, 0x60, 0xcd, 0x77, 0xce, 0xed, 0x18, 0x07, 0x17, 0x18, 0xcb, 0x4d, 0xad, 0x73, 0xbb, 0xf7, 0x9e, 0x1c, 0x4e, 0xd7, 0x3a, 0xfb, 0x8a, 0x50,
0xc8, 0x3e, 0x73, 0xc3, 0xd8, 0x58, 0x6d, 0x69, 0xdd, 0xdb, 0xe6, 0x07, 0xe2, 0x72, 0xfa, 0xce, 0x7c, 0xd0, 0xf7, 0xa4, 0x6a, 0x5d, 0xa9, 0xe6, 0xb0, 0xd6, 0x3f, 0x02, 0x2e, 0x38, 0x94, 0xa3,
0xf9, 0x37, 0x38, 0x40, 0x47, 0x6e, 0x28, 0x5c, 0x37, 0xa5, 0x6b, 0x09, 0x6b, 0xbf, 0xe1, 0x70, 0x7c, 0xe0, 0x54, 0xd0, 0x27, 0xf6, 0x28, 0x11, 0x5c, 0x29, 0x08, 0x22, 0x62, 0x8f, 0xca, 0x82,
0x81, 0x04, 0xcc, 0x2a, 0x0b, 0x27, 0x86, 0x11, 0xf6, 0x86, 0xca, 0x70, 0xad, 0x62, 0x68, 0x61, 0x53, 0xac, 0x20, 0x38, 0x05, 0x75, 0x0a, 0x6a, 0xce, 0x80, 0x32, 0x9f, 0xe0, 0x2c, 0xff, 0xd5,
0x6f, 0x38, 0x6b, 0x38, 0xc1, 0x2a, 0x86, 0x13, 0x50, 0x0f, 0x40, 0x83, 0xf4, 0x03, 0x1a, 0x61, 0xe6, 0x42, 0xe7, 0xee, 0xf6, 0x46, 0x37, 0x79, 0x18, 0xba, 0x4f, 0xd3, 0x87, 0x21, 0xc9, 0xa9,
0x54, 0xd4, 0xbf, 0xde, 0x5a, 0xe8, 0xae, 0xec, 0x6d, 0xf7, 0xd4, 0xbf, 0x46, 0xef, 0x59, 0xfe, 0xf7, 0x58, 0xf6, 0x62, 0x24, 0xe0, 0x6a, 0xba, 0x6d, 0x56, 0x98, 0xf5, 0xa4, 0xab, 0xf2, 0x70,
0xaf, 0xa1, 0x6a, 0x32, 0x1f, 0x89, 0x59, 0x4c, 0x39, 0x5c, 0xcf, 0xb7, 0x4d, 0x1b, 0xb3, 0xa5, 0x0b, 0x95, 0xc2, 0xf4, 0x1f, 0x35, 0x50, 0xf3, 0x08, 0xc5, 0x0e, 0x1d, 0x64, 0x86, 0xb5, 0x37,
0xa6, 0xaa, 0x0c, 0xb7, 0xad, 0x19, 0x99, 0xfe, 0x93, 0x06, 0x1a, 0x21, 0x0e, 0x10, 0x09, 0xfa, 0x1a, 0x3e, 0x91, 0x86, 0x13, 0x01, 0x8d, 0x3d, 0xe2, 0xf9, 0xc4, 0xb6, 0x38, 0xc1, 0x47, 0x89,
0x45, 0x60, 0xe3, 0x9d, 0x81, 0x4f, 0x44, 0xe0, 0x15, 0x87, 0xc6, 0x01, 0x0e, 0x23, 0xec, 0x39, 0x40, 0xaa, 0x19, 0x09, 0xa8, 0x3d, 0xce, 0xee, 0x20, 0x2f, 0xcf, 0xe5, 0x5a, 0xc3, 0xd0, 0xd0,
0x0c, 0xa3, 0x63, 0x65, 0x90, 0x7b, 0xa6, 0x1c, 0x6a, 0x8f, 0x8a, 0x37, 0x28, 0x2c, 0x73, 0xa5, 0x6a, 0x81, 0x0b, 0xf4, 0x5f, 0x34, 0x50, 0x4b, 0xaa, 0xf9, 0x6d, 0x48, 0x02, 0x6e, 0x9e, 0x38,
0xd1, 0x30, 0x34, 0x6b, 0xbd, 0xc2, 0xc5, 0xfa, 0xaf, 0x1a, 0x68, 0xa8, 0x6e, 0x7e, 0x9f, 0xe0, 0x7d, 0x63, 0x4d, 0xd5, 0x33, 0x98, 0x08, 0xb8, 0xf2, 0x85, 0x2c, 0x93, 0x62, 0x0e, 0x9c, 0x5e,
0x98, 0xd9, 0x67, 0xc4, 0x35, 0x36, 0x64, 0x3f, 0xe3, 0x2b, 0x0e, 0xd7, 0xbe, 0x12, 0x6d, 0x92, 0x24, 0xe0, 0x8a, 0x9b, 0x07, 0xb2, 0x84, 0x0b, 0xe8, 0xb4, 0xc8, 0xd1, 0x55, 0xbb, 0x14, 0x5e,
0xcc, 0x11, 0x31, 0x53, 0x0e, 0xd7, 0xfc, 0x32, 0x50, 0x14, 0x5c, 0x41, 0x27, 0x4d, 0x4e, 0x2f, 0x06, 0x2e, 0xc6, 0xed, 0xa2, 0x03, 0x2a, 0xf0, 0x7d, 0xfd, 0x13, 0x50, 0x0d, 0x29, 0xf7, 0xc3,
0x3b, 0x33, 0xf2, 0x59, 0xe0, 0xe5, 0xb8, 0x53, 0x4d, 0xb0, 0x2a, 0xbc, 0xab, 0x7f, 0x06, 0xea, 0x80, 0x13, 0x6c, 0xd4, 0x55, 0x4f, 0x36, 0xe5, 0x53, 0x92, 0x81, 0xb1, 0x80, 0x35, 0x75, 0x82,
0x49, 0xc0, 0xa2, 0x24, 0x66, 0x18, 0x19, 0x9b, 0x72, 0x26, 0x5b, 0xe2, 0x7f, 0xa6, 0x00, 0x33, 0x0c, 0x69, 0xa1, 0x19, 0xab, 0xb2, 0x93, 0x17, 0x1c, 0x27, 0xe6, 0x20, 0x74, 0x4c, 0x8f, 0xf9,
0x0e, 0x1b, 0xf2, 0x04, 0x05, 0xd2, 0xb6, 0xa6, 0xac, 0xac, 0x4e, 0x3c, 0x70, 0x0c, 0xdb, 0xfd, 0xdc, 0xd0, 0x67, 0xd9, 0x21, 0x45, 0x7d, 0xfe, 0xf5, 0xfe, 0x11, 0xf3, 0xb9, 0xcc, 0xce, 0xcf,
0x84, 0xd8, 0x21, 0x8d, 0x98, 0xa1, 0x4f, 0xab, 0xb3, 0x24, 0xf5, 0xe5, 0xb7, 0x87, 0xc7, 0x34, 0x03, 0x59, 0x76, 0x05, 0x34, 0x9f, 0x5d, 0x31, 0xbc, 0x0c, 0xc8, 0xec, 0x0a, 0x0e, 0x68, 0xca,
0x62, 0xa2, 0xba, 0xa8, 0x0c, 0x14, 0xd5, 0x55, 0xd0, 0x72, 0x75, 0x55, 0xf9, 0x2c, 0x20, 0xaa, 0x87, 0x8e, 0x5c, 0xea, 0xdf, 0x6b, 0xa0, 0x46, 0x43, 0xd7, 0xb4, 0x19, 0xa5, 0x44, 0x5d, 0x83,
0xab, 0x24, 0x58, 0x13, 0x3e, 0x21, 0x62, 0x69, 0x1e, 0x5d, 0xbc, 0x6e, 0xd6, 0xc6, 0xaf, 0x9b, 0x81, 0xb1, 0xae, 0x4e, 0xf7, 0x7c, 0x22, 0x60, 0x1d, 0x59, 0xa7, 0x87, 0xa1, 0xbb, 0x3b, 0x23,
0xb5, 0x8b, 0xab, 0xa6, 0x36, 0xbe, 0x6a, 0x6a, 0x3f, 0x5f, 0x37, 0x6b, 0xaf, 0xae, 0x9b, 0xda, 0x65, 0xc7, 0xd1, 0x02, 0x12, 0x0b, 0xf8, 0x76, 0xf2, 0x4a, 0x17, 0xe0, 0xe9, 0x19, 0x2f, 0xc6,
0xf8, 0xba, 0x59, 0xfb, 0xeb, 0xba, 0x59, 0x3b, 0x79, 0xf0, 0x1f, 0x1e, 0x3b, 0x35, 0x31, 0xee, 0xed, 0x9b, 0x2a, 0xa8, 0xa4, 0xd1, 0x3b, 0xb8, 0x7c, 0xd5, 0xa8, 0x8c, 0x5f, 0x35, 0x2a, 0x97,
0xa2, 0x7c, 0xf4, 0x3e, 0xfa, 0x37, 0x00, 0x00, 0xff, 0xff, 0xbf, 0x4a, 0x4f, 0x60, 0x33, 0x09, 0x93, 0x86, 0x36, 0x9e, 0x34, 0xb4, 0x9f, 0xae, 0x1b, 0x95, 0x97, 0xd7, 0x0d, 0x6d, 0x7c, 0xdd,
0x00, 0x00, 0xa8, 0xfc, 0x79, 0xdd, 0xa8, 0x3c, 0x7b, 0xf7, 0x3f, 0xdc, 0xb9, 0x49, 0xe3, 0xf6, 0x17, 0xd5,
0xdd, 0xfb, 0xc1, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x75, 0x19, 0xf5, 0x92, 0x9d, 0x09, 0x00,
0x00,
} }
func (m *DeviceConfiguration) Marshal() (dAtA []byte, err error) { func (m *DeviceConfiguration) Marshal() (dAtA []byte, err error) {
@ -176,6 +179,13 @@ func (m *DeviceConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = l
if m.RawNumConnections != 0 {
i = encodeVarintDeviceconfiguration(dAtA, i, uint64(m.RawNumConnections))
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0x98
}
if m.RemoteGUIPort != 0 { if m.RemoteGUIPort != 0 {
i = encodeVarintDeviceconfiguration(dAtA, i, uint64(m.RemoteGUIPort)) i = encodeVarintDeviceconfiguration(dAtA, i, uint64(m.RemoteGUIPort))
i-- i--
@ -423,6 +433,9 @@ func (m *DeviceConfiguration) ProtoSize() (n int) {
if m.RemoteGUIPort != 0 { if m.RemoteGUIPort != 0 {
n += 2 + sovDeviceconfiguration(uint64(m.RemoteGUIPort)) n += 2 + sovDeviceconfiguration(uint64(m.RemoteGUIPort))
} }
if m.RawNumConnections != 0 {
n += 2 + sovDeviceconfiguration(uint64(m.RawNumConnections))
}
return n return n
} }
@ -918,6 +931,25 @@ func (m *DeviceConfiguration) Unmarshal(dAtA []byte) error {
break break
} }
} }
case 19:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field RawNumConnections", wireType)
}
m.RawNumConnections = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDeviceconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.RawNumConnections |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipDeviceconfiguration(dAtA[iNdEx:]) skippy, err := skipDeviceconfiguration(dAtA[iNdEx:])

View File

@ -20,6 +20,7 @@ import (
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sliceutil"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
"github.com/thejerf/suture/v4" "github.com/thejerf/suture/v4"
) )
@ -198,9 +199,7 @@ func (w *wrapper) Unsubscribe(c Committer) {
w.mut.Lock() w.mut.Lock()
for i := range w.subs { for i := range w.subs {
if w.subs[i] == c { if w.subs[i] == c {
copy(w.subs[i:], w.subs[i+1:]) w.subs = sliceutil.RemoveAndZero(w.subs, i)
w.subs[len(w.subs)-1] = nil
w.subs = w.subs[:len(w.subs)-1]
break break
} }
} }

View File

@ -91,6 +91,7 @@ func (d *quicDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL
if isLocal { if isLocal {
priority = d.lanPriority priority = d.lanPriority
} }
return newInternalConn(&quicTlsConn{session, stream, createdConn}, connTypeQUICClient, isLocal, priority), nil return newInternalConn(&quicTlsConn{session, stream, createdConn}, connTypeQUICClient, isLocal, priority), nil
} }
@ -108,9 +109,10 @@ func (quicDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Confi
commonDialer: commonDialer{ commonDialer: commonDialer{
reconnectInterval: time.Duration(quicInterval) * time.Second, reconnectInterval: time.Duration(quicInterval) * time.Second,
tlsCfg: tlsCfg, tlsCfg: tlsCfg,
lanChecker: lanChecker,
lanPriority: opts.ConnectionPriorityQUICLAN, lanPriority: opts.ConnectionPriorityQUICLAN,
wanPriority: opts.ConnectionPriorityQUICWAN, wanPriority: opts.ConnectionPriorityQUICWAN,
lanChecker: lanChecker, allowsMultiConns: true,
}, },
registry: registry, registry: registry,
} }

View File

@ -105,7 +105,9 @@ func (t *quicListener) serve(ctx context.Context) error {
defer quicTransport.Close() defer quicTransport.Close()
svc := stun.New(t.cfg, t, &transportPacketConn{tran: quicTransport}, tracer) svc := stun.New(t.cfg, t, &transportPacketConn{tran: quicTransport}, tracer)
go svc.Serve(ctx) stunCtx, cancel := context.WithCancel(ctx)
defer cancel()
go svc.Serve(stunCtx)
t.registry.Register(t.uri.Scheme, quicTransport) t.registry.Register(t.uri.Scheme, quicTransport)
defer t.registry.Unregister(t.uri.Scheme, quicTransport) defer t.registry.Unregister(t.uri.Scheme, quicTransport)

View File

@ -12,6 +12,7 @@ package registry
import ( import (
"strings" "strings"
"github.com/syncthing/syncthing/lib/sliceutil"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
) )
@ -41,9 +42,7 @@ func (r *Registry) Unregister(scheme string, item interface{}) {
candidates := r.available[scheme] candidates := r.available[scheme]
for i, existingItem := range candidates { for i, existingItem := range candidates {
if existingItem == item { if existingItem == item {
candidates[i] = candidates[len(candidates)-1] r.available[scheme] = sliceutil.RemoveAndZero(candidates, i)
candidates[len(candidates)-1] = nil
r.available[scheme] = candidates[:len(candidates)-1]
break break
} }
} }

View File

@ -11,10 +11,14 @@ package connections
import ( import (
"context" "context"
"crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base32"
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"io"
"math" "math"
"net" "net"
"net/url" "net/url"
@ -23,8 +27,10 @@ import (
stdsync "sync" stdsync "sync"
"time" "time"
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry" "github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/discover" "github.com/syncthing/syncthing/lib/discover"
@ -33,6 +39,7 @@ import (
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/semaphore" "github.com/syncthing/syncthing/lib/semaphore"
"github.com/syncthing/syncthing/lib/sliceutil"
"github.com/syncthing/syncthing/lib/stringutil" "github.com/syncthing/syncthing/lib/stringutil"
"github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/svcutil"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
@ -66,12 +73,14 @@ var (
errDeviceIgnored = errors.New("device is ignored") errDeviceIgnored = errors.New("device is ignored")
errConnLimitReached = errors.New("connection limit reached") errConnLimitReached = errors.New("connection limit reached")
errDevicePaused = errors.New("device is paused") errDevicePaused = errors.New("device is paused")
// A connection is being closed to make space for better ones
errReplacingConnection = errors.New("replacing connection")
) )
const ( const (
perDeviceWarningIntv = 15 * time.Minute perDeviceWarningIntv = 15 * time.Minute
tlsHandshakeTimeout = 10 * time.Second tlsHandshakeTimeout = 10 * time.Second
minConnectionReplaceAge = 10 * time.Second
minConnectionLoopSleep = 5 * time.Second minConnectionLoopSleep = 5 * time.Second
stdConnectionLoopSleep = time.Minute stdConnectionLoopSleep = time.Minute
worstDialerPriority = math.MaxInt32 worstDialerPriority = math.MaxInt32
@ -79,6 +88,7 @@ const (
shortLivedConnectionThreshold = 5 * time.Second shortLivedConnectionThreshold = 5 * time.Second
dialMaxParallel = 64 dialMaxParallel = 64
dialMaxParallelPerDevice = 8 dialMaxParallelPerDevice = 8
maxNumConnections = 128 // the maximum number of connections we maintain to any given device
) )
// From go/src/crypto/tls/cipher_suites.go // From go/src/crypto/tls/cipher_suites.go
@ -150,6 +160,7 @@ type connWithHello struct {
type service struct { type service struct {
*suture.Supervisor *suture.Supervisor
connectionStatusHandler connectionStatusHandler
deviceConnectionTracker
cfg config.Wrapper cfg config.Wrapper
myID protocol.DeviceID myID protocol.DeviceID
@ -281,21 +292,43 @@ func (s *service) handleConns(ctx context.Context) error {
_ = c.SetDeadline(time.Now().Add(20 * time.Second)) _ = c.SetDeadline(time.Now().Add(20 * time.Second))
go func() { go func() {
hello, err := protocol.ExchangeHello(c, s.model.GetHello(remoteID)) // Exchange Hello messages with the peer.
outgoing := s.helloForDevice(remoteID)
incoming, err := protocol.ExchangeHello(c, outgoing)
// The timestamps are used to create the connection ID.
c.connectionID = newConnectionID(outgoing.Timestamp, incoming.Timestamp)
select { select {
case s.hellos <- &connWithHello{c, hello, err, remoteID, remoteCert}: case s.hellos <- &connWithHello{c, incoming, err, remoteID, remoteCert}:
case <-ctx.Done(): case <-ctx.Done():
} }
}() }()
} }
} }
func (s *service) helloForDevice(remoteID protocol.DeviceID) protocol.Hello {
hello := protocol.Hello{
ClientName: "syncthing",
ClientVersion: build.Version,
Timestamp: time.Now().UnixNano(),
}
if cfg, ok := s.cfg.Device(remoteID); ok {
hello.NumConnections = cfg.NumConnections()
// Set our name (from the config of our device ID) only if we
// already know about the other side device ID.
if myCfg, ok := s.cfg.Device(s.myID); ok {
hello.DeviceName = myCfg.Name
}
}
return hello
}
func (s *service) connectionCheckEarly(remoteID protocol.DeviceID, c internalConn) error { func (s *service) connectionCheckEarly(remoteID protocol.DeviceID, c internalConn) error {
if s.cfg.IgnoredDevice(remoteID) { if s.cfg.IgnoredDevice(remoteID) {
return errDeviceIgnored return errDeviceIgnored
} }
if max := s.cfg.Options().ConnectionLimitMax; max > 0 && s.model.NumConnections() >= max { if max := s.cfg.Options().ConnectionLimitMax; max > 0 && s.numConnectedDevices() >= max {
// We're not allowed to accept any more connections. // We're not allowed to accept any more connections.
return errConnLimitReached return errConnLimitReached
} }
@ -315,31 +348,26 @@ func (s *service) connectionCheckEarly(remoteID protocol.DeviceID, c internalCon
return errNetworkNotAllowed return errNetworkNotAllowed
} }
// Lower priority is better, just like nice etc. currentConns := s.numConnectionsForDevice(cfg.DeviceID)
if ct, ok := s.model.Connection(remoteID); ok { desiredConns := s.desiredConnectionsToDevice(cfg.DeviceID)
if ct.Priority() > c.priority || time.Since(ct.Statistics().StartedAt) > minConnectionReplaceAge { worstPrio := s.worstConnectionPriority(remoteID)
l.Debugf("Switching connections %s (existing: %s new: %s)", remoteID, ct, c) ourUpgradeThreshold := c.priority + s.cfg.Options().ConnectionPriorityUpgradeThreshold
} else { if currentConns >= desiredConns && ourUpgradeThreshold >= worstPrio {
// We should not already be connected to the other party. TODO: This l.Debugf("Not accepting connection to %s at %s: already have %d connections, desire %d", remoteID, c, currentConns, desiredConns)
// could use some better handling. If the old connection is dead but return errDeviceAlreadyConnected
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
return errDeviceAlreadyConnected
}
} }
return nil return nil
} }
func (s *service) handleHellos(ctx context.Context) error { func (s *service) handleHellos(ctx context.Context) error {
var c internalConn
var hello protocol.Hello
var err error
var remoteID protocol.DeviceID
var remoteCert *x509.Certificate
for { for {
var c internalConn
var hello protocol.Hello
var err error
var remoteID protocol.DeviceID
var remoteCert *x509.Certificate
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
@ -416,15 +444,17 @@ func (s *service) handleHellos(ctx context.Context) error {
rd, wr := s.limiter.getLimiters(remoteID, c, c.IsLocal()) rd, wr := s.limiter.getLimiters(remoteID, c, c.IsLocal())
protoConn := protocol.NewConnection(remoteID, rd, wr, c, s.model, c, deviceCfg.Compression, s.cfg.FolderPasswords(remoteID), s.keyGen) protoConn := protocol.NewConnection(remoteID, rd, wr, c, s.model, c, deviceCfg.Compression, s.cfg.FolderPasswords(remoteID), s.keyGen)
s.accountAddedConnection(protoConn, hello, s.cfg.Options().ConnectionPriorityUpgradeThreshold)
go func() { go func() {
<-protoConn.Closed() <-protoConn.Closed()
s.accountRemovedConnection(protoConn)
s.dialNowDevicesMut.Lock() s.dialNowDevicesMut.Lock()
s.dialNowDevices[remoteID] = struct{}{} s.dialNowDevices[remoteID] = struct{}{}
s.scheduleDialNow() s.scheduleDialNow()
s.dialNowDevicesMut.Unlock() s.dialNowDevicesMut.Unlock()
}() }()
l.Infof("Established secure connection to %s at %s", remoteID, c) l.Infof("Established secure connection to %s at %s", remoteID.Short(), c)
s.model.AddConnection(protoConn, hello) s.model.AddConnection(protoConn, hello)
continue continue
@ -518,7 +548,7 @@ func (s *service) dialDevices(ctx context.Context, now time.Time, cfg config.Con
allowAdditional := 0 // no limit allowAdditional := 0 // no limit
connectionLimit := cfg.Options.LowestConnectionLimit() connectionLimit := cfg.Options.LowestConnectionLimit()
if connectionLimit > 0 { if connectionLimit > 0 {
current := s.model.NumConnections() current := s.numConnectedDevices()
allowAdditional = connectionLimit - current allowAdditional = connectionLimit - current
if allowAdditional <= 0 { if allowAdditional <= 0 {
l.Debugf("Skipping dial because we've reached the connection limit, current %d >= limit %d", current, connectionLimit) l.Debugf("Skipping dial because we've reached the connection limit, current %d >= limit %d", current, connectionLimit)
@ -545,19 +575,20 @@ func (s *service) dialDevices(ctx context.Context, now time.Time, cfg config.Con
// See if we are already connected and, if so, what our cutoff is // See if we are already connected and, if so, what our cutoff is
// for dialer priority. // for dialer priority.
priorityCutoff := worstDialerPriority priorityCutoff := worstDialerPriority
connection, connected := s.model.Connection(deviceCfg.DeviceID) if currentConns := s.numConnectionsForDevice(deviceCfg.DeviceID); currentConns > 0 {
if connected {
// Set the priority cutoff to the current connection's priority, // Set the priority cutoff to the current connection's priority,
// so that we don't attempt any dialers with worse priority. // so that we don't attempt any dialers with worse priority.
priorityCutoff = connection.Priority() priorityCutoff = s.worstConnectionPriority(deviceCfg.DeviceID)
// Reduce the priority cutoff by the upgrade threshold, so that // Reduce the priority cutoff by the upgrade threshold, so that
// we don't attempt dialers that aren't considered a worthy upgrade. // we don't attempt dialers that aren't considered a worthy upgrade.
priorityCutoff -= cfg.Options.ConnectionPriorityUpgradeThreshold priorityCutoff -= cfg.Options.ConnectionPriorityUpgradeThreshold
if bestDialerPriority >= priorityCutoff { if bestDialerPriority >= priorityCutoff && currentConns >= s.desiredConnectionsToDevice(deviceCfg.DeviceID) {
// Our best dialer is not any better than what we already // Our best dialer is not any better than what we already
// have, so nothing to do here. // have, and we already have the desired number of
// connections to this device,so nothing to do here.
l.Debugf("Skipping dial to %s because we already have %d connections and our best dialer is not better than %d", deviceCfg.DeviceID.Short(), currentConns, priorityCutoff)
continue continue
} }
} }
@ -625,14 +656,14 @@ func (s *service) resolveDialTargets(ctx context.Context, now time.Time, cfg con
deviceID := deviceCfg.DeviceID deviceID := deviceCfg.DeviceID
addrs := s.resolveDeviceAddrs(ctx, deviceCfg) addrs := s.resolveDeviceAddrs(ctx, deviceCfg)
l.Debugln("Resolved device", deviceID, "addresses:", addrs) l.Debugln("Resolved device", deviceID.Short(), "addresses:", addrs)
dialTargets := make([]dialTarget, 0, len(addrs)) dialTargets := make([]dialTarget, 0, len(addrs))
for _, addr := range addrs { for _, addr := range addrs {
// Use both device and address, as you might have two devices connected // Use both device and address, as you might have two devices connected
// to the same relay // to the same relay
if !initial && nextDialAt.get(deviceID, addr).After(now) { if !initial && nextDialAt.get(deviceID, addr).After(now) {
l.Debugf("Not dialing %s via %v as it's not time yet", deviceID, addr) l.Debugf("Not dialing %s via %v as it's not time yet", deviceID.Short(), addr)
continue continue
} }
@ -669,8 +700,17 @@ func (s *service) resolveDialTargets(ctx context.Context, now time.Time, cfg con
dialer := dialerFactory.New(s.cfg.Options(), s.tlsCfg, s.registry, s.lanChecker) dialer := dialerFactory.New(s.cfg.Options(), s.tlsCfg, s.registry, s.lanChecker)
priority := dialer.Priority(uri.Host) priority := dialer.Priority(uri.Host)
if priority >= priorityCutoff { currentConns := s.numConnectionsForDevice(deviceCfg.DeviceID)
l.Debugf("Not dialing using %s as priority is not better than current connection (%d >= %d)", dialerFactory, priority, priorityCutoff) if priority > priorityCutoff {
l.Debugf("Not dialing %s at %s using %s as priority is worse than current connection (%d > %d)", deviceID.Short(), addr, dialerFactory, priority, priorityCutoff)
continue
}
if currentConns > 0 && !dialer.AllowsMultiConns() {
l.Debugf("Not dialing %s at %s using %s as it does not allow multiple connections and we already have a connection", deviceID.Short(), addr, dialerFactory)
continue
}
if currentConns >= s.desiredConnectionsToDevice(deviceCfg.DeviceID) && priority == priorityCutoff {
l.Debugf("Not dialing %s at %s using %s as priority is equal and we already have %d/%d connections", deviceID.Short(), addr, dialerFactory, currentConns, deviceCfg.NumConnections)
continue continue
} }
@ -1272,3 +1312,165 @@ func (r nextDialRegistry) sleepDurationAndCleanup(now time.Time) time.Duration {
} }
return sleep return sleep
} }
func (s *service) desiredConnectionsToDevice(deviceID protocol.DeviceID) int {
cfg, ok := s.cfg.Device(deviceID)
if !ok {
// We want no connections to an unknown device.
return 0
}
otherSide := s.wantConnectionsForDevice(deviceID)
thisSide := cfg.NumConnections()
switch {
case otherSide <= 0:
// The other side doesn't support multiple connections, or we
// haven't yet connected to them so we don't know what they support
// or not. Use a single connection until we know better.
return 1
case otherSide == 1:
// The other side supports multiple connections, but only wants
// one. We should honour that.
return 1
case thisSide == 1:
// We want only one connection, so we should honour that.
return 1
// Finally, we allow negotiation and use the higher of the two values,
// while keeping at or below the max allowed value.
default:
return min(max(thisSide, otherSide), maxNumConnections)
}
}
// The deviceConnectionTracker keeps track of how many devices we are
// connected to and how many connections we have to each device. It also
// tracks how many connections they are willing to use.
type deviceConnectionTracker struct {
connectionsMut stdsync.Mutex
connections map[protocol.DeviceID][]protocol.Connection // current connections
wantConnections map[protocol.DeviceID]int // number of connections they want
}
func (c *deviceConnectionTracker) accountAddedConnection(conn protocol.Connection, h protocol.Hello, upgradeThreshold int) {
c.connectionsMut.Lock()
defer c.connectionsMut.Unlock()
// Lazily initialize the maps
if c.connections == nil {
c.connections = make(map[protocol.DeviceID][]protocol.Connection)
c.wantConnections = make(map[protocol.DeviceID]int)
}
// Add the connection to the list of current connections and remember
// how many total connections they want
d := conn.DeviceID()
c.connections[d] = append(c.connections[d], conn)
c.wantConnections[d] = int(h.NumConnections)
l.Debugf("Added connection for %s (now %d), they want %d connections", d.Short(), len(c.connections[d]), h.NumConnections)
// Close any connections we no longer want to retain.
c.closeWorsePriorityConnectionsLocked(d, conn.Priority()-upgradeThreshold)
}
func (c *deviceConnectionTracker) accountRemovedConnection(conn protocol.Connection) {
c.connectionsMut.Lock()
defer c.connectionsMut.Unlock()
d := conn.DeviceID()
cid := conn.ConnectionID()
// Remove the connection from the list of current connections
for i, conn := range c.connections[d] {
if conn.ConnectionID() == cid {
c.connections[d] = sliceutil.RemoveAndZero(c.connections[d], i)
break
}
}
// Clean up if required
if len(c.connections[d]) == 0 {
delete(c.connections, d)
delete(c.wantConnections, d)
}
l.Debugf("Removed connection for %s (now %d)", d.Short(), c.connections[d])
}
func (c *deviceConnectionTracker) numConnectionsForDevice(d protocol.DeviceID) int {
c.connectionsMut.Lock()
defer c.connectionsMut.Unlock()
return len(c.connections[d])
}
func (c *deviceConnectionTracker) wantConnectionsForDevice(d protocol.DeviceID) int {
c.connectionsMut.Lock()
defer c.connectionsMut.Unlock()
return c.wantConnections[d]
}
func (c *deviceConnectionTracker) numConnectedDevices() int {
c.connectionsMut.Lock()
defer c.connectionsMut.Unlock()
return len(c.connections)
}
func (c *deviceConnectionTracker) worstConnectionPriority(d protocol.DeviceID) int {
c.connectionsMut.Lock()
defer c.connectionsMut.Unlock()
if len(c.connections[d]) == 0 {
return math.MaxInt // worst possible priority
}
worstPriority := c.connections[d][0].Priority()
for _, conn := range c.connections[d][1:] {
if p := conn.Priority(); p > worstPriority {
worstPriority = p
}
}
return worstPriority
}
// closeWorsePriorityConnectionsLocked closes all connections to the given
// device that are worse than the cutoff priority. Must be called with the
// lock held.
func (c *deviceConnectionTracker) closeWorsePriorityConnectionsLocked(d protocol.DeviceID, cutoff int) {
for _, conn := range c.connections[d] {
if p := conn.Priority(); p > cutoff {
l.Debugf("Closing connection %s to %s with priority %d (cutoff %d)", conn, d.Short(), p, cutoff)
go conn.Close(errReplacingConnection)
}
}
}
// newConnectionID generates a connection ID. The connection ID is designed
// to be unique for each connection and chronologically sortable. It is
// based on the sum of two timestamps: when we think the connection was
// started, and when the other side thinks the connection was started. We
// then add some random data for good measure. This way, even if the other
// side does some funny business with the timestamp, we will get no worse
// than random connection IDs.
func newConnectionID(t0, t1 int64) string {
var buf [16]byte // 8 bytes timestamp, 8 bytes random
binary.BigEndian.PutUint64(buf[:], uint64(t0+t1))
_, _ = io.ReadFull(rand.Reader, buf[8:])
enc := base32.HexEncoding.WithPadding(base32.NoPadding)
// We encode the two parts separately and concatenate the results. The
// reason for this is that the timestamp (64 bits) doesn't precisely
// align to the base32 encoding (5 bits per character), so we'd get a
// character in the middle that is a mix of bits from the timestamp and
// from the random. We want the timestamp part deterministic.
return enc.EncodeToString(buf[:8]) + enc.EncodeToString(buf[8:])
}
// temporary implementations of min and max, to be removed once we can use
// Go 1.21 builtins. :)
func min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}

View File

@ -42,6 +42,7 @@ type internalConn struct {
isLocal bool isLocal bool
priority int priority int
establishedAt time.Time establishedAt time.Time
connectionID string // set after Hello exchange
} }
type connType int type connType int
@ -88,12 +89,13 @@ func (t connType) Transport() string {
} }
func newInternalConn(tc tlsConn, connType connType, isLocal bool, priority int) internalConn { func newInternalConn(tc tlsConn, connType connType, isLocal bool, priority int) internalConn {
now := time.Now()
return internalConn{ return internalConn{
tlsConn: tc, tlsConn: tc,
connType: connType, connType: connType,
isLocal: isLocal, isLocal: isLocal,
priority: priority, priority: priority,
establishedAt: time.Now().Truncate(time.Second), establishedAt: now.Truncate(time.Second),
} }
} }
@ -138,12 +140,16 @@ func (c internalConn) EstablishedAt() time.Time {
return c.establishedAt return c.establishedAt
} }
func (c internalConn) ConnectionID() string {
return c.connectionID
}
func (c internalConn) String() string { func (c internalConn) String() string {
t := "WAN" t := "WAN"
if c.isLocal { if c.isLocal {
t = "LAN" t = "LAN"
} }
return fmt.Sprintf("%s-%s/%s/%s/%s-P%d", c.LocalAddr(), c.RemoteAddr(), c.Type(), c.Crypto(), t, c.Priority()) return fmt.Sprintf("%s-%s/%s/%s/%s-P%d-%s", c.LocalAddr(), c.RemoteAddr(), c.Type(), c.Crypto(), t, c.Priority(), c.connectionID)
} }
type dialerFactory interface { type dialerFactory interface {
@ -160,6 +166,7 @@ type commonDialer struct {
lanChecker *lanChecker lanChecker *lanChecker
lanPriority int lanPriority int
wanPriority int wanPriority int
allowsMultiConns bool
} }
func (d *commonDialer) RedialFrequency() time.Duration { func (d *commonDialer) RedialFrequency() time.Duration {
@ -173,10 +180,15 @@ func (d *commonDialer) Priority(host string) int {
return d.wanPriority return d.wanPriority
} }
func (d *commonDialer) AllowsMultiConns() bool {
return d.allowsMultiConns
}
type genericDialer interface { type genericDialer interface {
Dial(context.Context, protocol.DeviceID, *url.URL) (internalConn, error) Dial(context.Context, protocol.DeviceID, *url.URL) (internalConn, error)
RedialFrequency() time.Duration RedialFrequency() time.Duration
Priority(host string) int Priority(host string) int
AllowsMultiConns() bool
} }
type listenerFactory interface { type listenerFactory interface {
@ -212,10 +224,7 @@ type genericListener interface {
type Model interface { type Model interface {
protocol.Model protocol.Model
AddConnection(conn protocol.Connection, hello protocol.Hello) AddConnection(conn protocol.Connection, hello protocol.Hello)
NumConnections() int
Connection(remoteID protocol.DeviceID) (protocol.Connection, bool)
OnHello(protocol.DeviceID, net.Addr, protocol.Hello) error OnHello(protocol.DeviceID, net.Addr, protocol.Hello) error
GetHello(protocol.DeviceID) protocol.HelloIntf
DeviceStatistics() (map[protocol.DeviceID]stats.DeviceStatistics, error) DeviceStatistics() (map[protocol.DeviceID]stats.DeviceStatistics, error)
} }

View File

@ -62,6 +62,7 @@ func (d *tcpDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL)
if isLocal { if isLocal {
priority = d.lanPriority priority = d.lanPriority
} }
return newInternalConn(tc, connTypeTCPClient, isLocal, priority), nil return newInternalConn(tc, connTypeTCPClient, isLocal, priority), nil
} }
@ -73,9 +74,10 @@ func (tcpDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config
trafficClass: opts.TrafficClass, trafficClass: opts.TrafficClass,
reconnectInterval: time.Duration(opts.ReconnectIntervalS) * time.Second, reconnectInterval: time.Duration(opts.ReconnectIntervalS) * time.Second,
tlsCfg: tlsCfg, tlsCfg: tlsCfg,
lanChecker: lanChecker,
lanPriority: opts.ConnectionPriorityTCPLAN, lanPriority: opts.ConnectionPriorityTCPLAN,
wanPriority: opts.ConnectionPriorityTCPWAN, wanPriority: opts.ConnectionPriorityTCPWAN,
lanChecker: lanChecker, allowsMultiConns: true,
}, },
registry: registry, registry: registry,
} }

View File

@ -34,6 +34,7 @@ func newFakeConnection(id protocol.DeviceID, model Model) *fakeConnection {
return f.fileData[name], nil return f.fileData[name], nil
}) })
f.DeviceIDReturns(id) f.DeviceIDReturns(id)
f.ConnectionIDReturns(rand.String(16))
f.CloseCalls(func(err error) { f.CloseCalls(func(err error) {
f.closeOnce.Do(func() { f.closeOnce.Do(func() {
close(f.closed) close(f.closed)

View File

@ -530,7 +530,7 @@ func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder, context.Cancel
cfg.Folders = []config.FolderConfiguration{fcfg} cfg.Folders = []config.FolderConfiguration{fcfg}
replace(t, w, cfg) replace(t, w, cfg)
m := newModel(t, w, myID, "syncthing", "dev", nil) m := newModel(t, w, myID, nil)
m.ServeBackground() m.ServeBackground()
<-m.started <-m.started
must(t, m.ScanFolder("ro")) must(t, m.ScanFolder("ro"))

View File

@ -507,7 +507,7 @@ nextFile:
devices := snap.Availability(fileName) devices := snap.Availability(fileName)
for _, dev := range devices { for _, dev := range devices {
if _, ok := f.model.Connection(dev); ok { if f.model.ConnectedTo(dev) {
// Handle the file normally, by copying and pulling, etc. // Handle the file normally, by copying and pulling, etc.
f.handleFile(fi, snap, copyChan) f.handleFile(fi, snap, copyChan)
continue nextFile continue nextFile

View File

@ -76,18 +76,16 @@ type Model struct {
result1 model.FolderCompletion result1 model.FolderCompletion
result2 error result2 error
} }
ConnectionStub func(protocol.DeviceID) (protocol.Connection, bool) ConnectedToStub func(protocol.DeviceID) bool
connectionMutex sync.RWMutex connectedToMutex sync.RWMutex
connectionArgsForCall []struct { connectedToArgsForCall []struct {
arg1 protocol.DeviceID arg1 protocol.DeviceID
} }
connectionReturns struct { connectedToReturns struct {
result1 protocol.Connection result1 bool
result2 bool
} }
connectionReturnsOnCall map[int]struct { connectedToReturnsOnCall map[int]struct {
result1 protocol.Connection result1 bool
result2 bool
} }
ConnectionStatsStub func() map[string]interface{} ConnectionStatsStub func() map[string]interface{}
connectionStatsMutex sync.RWMutex connectionStatsMutex sync.RWMutex
@ -262,17 +260,6 @@ type Model struct {
result1 map[string][]versioner.FileVersion result1 map[string][]versioner.FileVersion
result2 error result2 error
} }
GetHelloStub func(protocol.DeviceID) protocol.HelloIntf
getHelloMutex sync.RWMutex
getHelloArgsForCall []struct {
arg1 protocol.DeviceID
}
getHelloReturns struct {
result1 protocol.HelloIntf
}
getHelloReturnsOnCall map[int]struct {
result1 protocol.HelloIntf
}
GetMtimeMappingStub func(string, string) (fs.MtimeMapping, error) GetMtimeMappingStub func(string, string) (fs.MtimeMapping, error)
getMtimeMappingMutex sync.RWMutex getMtimeMappingMutex sync.RWMutex
getMtimeMappingArgsForCall []struct { getMtimeMappingArgsForCall []struct {
@ -378,16 +365,6 @@ type Model struct {
result3 []db.FileInfoTruncated result3 []db.FileInfoTruncated
result4 error result4 error
} }
NumConnectionsStub func() int
numConnectionsMutex sync.RWMutex
numConnectionsArgsForCall []struct {
}
numConnectionsReturns struct {
result1 int
}
numConnectionsReturnsOnCall map[int]struct {
result1 int
}
OnHelloStub func(protocol.DeviceID, net.Addr, protocol.Hello) error OnHelloStub func(protocol.DeviceID, net.Addr, protocol.Hello) error
onHelloMutex sync.RWMutex onHelloMutex sync.RWMutex
onHelloArgsForCall []struct { onHelloArgsForCall []struct {
@ -888,68 +865,65 @@ func (fake *Model) CompletionReturnsOnCall(i int, result1 model.FolderCompletion
}{result1, result2} }{result1, result2}
} }
func (fake *Model) Connection(arg1 protocol.DeviceID) (protocol.Connection, bool) { func (fake *Model) ConnectedTo(arg1 protocol.DeviceID) bool {
fake.connectionMutex.Lock() fake.connectedToMutex.Lock()
ret, specificReturn := fake.connectionReturnsOnCall[len(fake.connectionArgsForCall)] ret, specificReturn := fake.connectedToReturnsOnCall[len(fake.connectedToArgsForCall)]
fake.connectionArgsForCall = append(fake.connectionArgsForCall, struct { fake.connectedToArgsForCall = append(fake.connectedToArgsForCall, struct {
arg1 protocol.DeviceID arg1 protocol.DeviceID
}{arg1}) }{arg1})
stub := fake.ConnectionStub stub := fake.ConnectedToStub
fakeReturns := fake.connectionReturns fakeReturns := fake.connectedToReturns
fake.recordInvocation("Connection", []interface{}{arg1}) fake.recordInvocation("ConnectedTo", []interface{}{arg1})
fake.connectionMutex.Unlock() fake.connectedToMutex.Unlock()
if stub != nil { if stub != nil {
return stub(arg1) return stub(arg1)
} }
if specificReturn { if specificReturn {
return ret.result1, ret.result2 return ret.result1
} }
return fakeReturns.result1, fakeReturns.result2 return fakeReturns.result1
} }
func (fake *Model) ConnectionCallCount() int { func (fake *Model) ConnectedToCallCount() int {
fake.connectionMutex.RLock() fake.connectedToMutex.RLock()
defer fake.connectionMutex.RUnlock() defer fake.connectedToMutex.RUnlock()
return len(fake.connectionArgsForCall) return len(fake.connectedToArgsForCall)
} }
func (fake *Model) ConnectionCalls(stub func(protocol.DeviceID) (protocol.Connection, bool)) { func (fake *Model) ConnectedToCalls(stub func(protocol.DeviceID) bool) {
fake.connectionMutex.Lock() fake.connectedToMutex.Lock()
defer fake.connectionMutex.Unlock() defer fake.connectedToMutex.Unlock()
fake.ConnectionStub = stub fake.ConnectedToStub = stub
} }
func (fake *Model) ConnectionArgsForCall(i int) protocol.DeviceID { func (fake *Model) ConnectedToArgsForCall(i int) protocol.DeviceID {
fake.connectionMutex.RLock() fake.connectedToMutex.RLock()
defer fake.connectionMutex.RUnlock() defer fake.connectedToMutex.RUnlock()
argsForCall := fake.connectionArgsForCall[i] argsForCall := fake.connectedToArgsForCall[i]
return argsForCall.arg1 return argsForCall.arg1
} }
func (fake *Model) ConnectionReturns(result1 protocol.Connection, result2 bool) { func (fake *Model) ConnectedToReturns(result1 bool) {
fake.connectionMutex.Lock() fake.connectedToMutex.Lock()
defer fake.connectionMutex.Unlock() defer fake.connectedToMutex.Unlock()
fake.ConnectionStub = nil fake.ConnectedToStub = nil
fake.connectionReturns = struct { fake.connectedToReturns = struct {
result1 protocol.Connection result1 bool
result2 bool }{result1}
}{result1, result2}
} }
func (fake *Model) ConnectionReturnsOnCall(i int, result1 protocol.Connection, result2 bool) { func (fake *Model) ConnectedToReturnsOnCall(i int, result1 bool) {
fake.connectionMutex.Lock() fake.connectedToMutex.Lock()
defer fake.connectionMutex.Unlock() defer fake.connectedToMutex.Unlock()
fake.ConnectionStub = nil fake.ConnectedToStub = nil
if fake.connectionReturnsOnCall == nil { if fake.connectedToReturnsOnCall == nil {
fake.connectionReturnsOnCall = make(map[int]struct { fake.connectedToReturnsOnCall = make(map[int]struct {
result1 protocol.Connection result1 bool
result2 bool
}) })
} }
fake.connectionReturnsOnCall[i] = struct { fake.connectedToReturnsOnCall[i] = struct {
result1 protocol.Connection result1 bool
result2 bool }{result1}
}{result1, result2}
} }
func (fake *Model) ConnectionStats() map[string]interface{} { func (fake *Model) ConnectionStats() map[string]interface{} {
@ -1797,67 +1771,6 @@ func (fake *Model) GetFolderVersionsReturnsOnCall(i int, result1 map[string][]ve
}{result1, result2} }{result1, result2}
} }
func (fake *Model) GetHello(arg1 protocol.DeviceID) protocol.HelloIntf {
fake.getHelloMutex.Lock()
ret, specificReturn := fake.getHelloReturnsOnCall[len(fake.getHelloArgsForCall)]
fake.getHelloArgsForCall = append(fake.getHelloArgsForCall, struct {
arg1 protocol.DeviceID
}{arg1})
stub := fake.GetHelloStub
fakeReturns := fake.getHelloReturns
fake.recordInvocation("GetHello", []interface{}{arg1})
fake.getHelloMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) GetHelloCallCount() int {
fake.getHelloMutex.RLock()
defer fake.getHelloMutex.RUnlock()
return len(fake.getHelloArgsForCall)
}
func (fake *Model) GetHelloCalls(stub func(protocol.DeviceID) protocol.HelloIntf) {
fake.getHelloMutex.Lock()
defer fake.getHelloMutex.Unlock()
fake.GetHelloStub = stub
}
func (fake *Model) GetHelloArgsForCall(i int) protocol.DeviceID {
fake.getHelloMutex.RLock()
defer fake.getHelloMutex.RUnlock()
argsForCall := fake.getHelloArgsForCall[i]
return argsForCall.arg1
}
func (fake *Model) GetHelloReturns(result1 protocol.HelloIntf) {
fake.getHelloMutex.Lock()
defer fake.getHelloMutex.Unlock()
fake.GetHelloStub = nil
fake.getHelloReturns = struct {
result1 protocol.HelloIntf
}{result1}
}
func (fake *Model) GetHelloReturnsOnCall(i int, result1 protocol.HelloIntf) {
fake.getHelloMutex.Lock()
defer fake.getHelloMutex.Unlock()
fake.GetHelloStub = nil
if fake.getHelloReturnsOnCall == nil {
fake.getHelloReturnsOnCall = make(map[int]struct {
result1 protocol.HelloIntf
})
}
fake.getHelloReturnsOnCall[i] = struct {
result1 protocol.HelloIntf
}{result1}
}
func (fake *Model) GetMtimeMapping(arg1 string, arg2 string) (fs.MtimeMapping, error) { func (fake *Model) GetMtimeMapping(arg1 string, arg2 string) (fs.MtimeMapping, error) {
fake.getMtimeMappingMutex.Lock() fake.getMtimeMappingMutex.Lock()
ret, specificReturn := fake.getMtimeMappingReturnsOnCall[len(fake.getMtimeMappingArgsForCall)] ret, specificReturn := fake.getMtimeMappingReturnsOnCall[len(fake.getMtimeMappingArgsForCall)]
@ -2331,59 +2244,6 @@ func (fake *Model) NeedFolderFilesReturnsOnCall(i int, result1 []db.FileInfoTrun
}{result1, result2, result3, result4} }{result1, result2, result3, result4}
} }
func (fake *Model) NumConnections() int {
fake.numConnectionsMutex.Lock()
ret, specificReturn := fake.numConnectionsReturnsOnCall[len(fake.numConnectionsArgsForCall)]
fake.numConnectionsArgsForCall = append(fake.numConnectionsArgsForCall, struct {
}{})
stub := fake.NumConnectionsStub
fakeReturns := fake.numConnectionsReturns
fake.recordInvocation("NumConnections", []interface{}{})
fake.numConnectionsMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) NumConnectionsCallCount() int {
fake.numConnectionsMutex.RLock()
defer fake.numConnectionsMutex.RUnlock()
return len(fake.numConnectionsArgsForCall)
}
func (fake *Model) NumConnectionsCalls(stub func() int) {
fake.numConnectionsMutex.Lock()
defer fake.numConnectionsMutex.Unlock()
fake.NumConnectionsStub = stub
}
func (fake *Model) NumConnectionsReturns(result1 int) {
fake.numConnectionsMutex.Lock()
defer fake.numConnectionsMutex.Unlock()
fake.NumConnectionsStub = nil
fake.numConnectionsReturns = struct {
result1 int
}{result1}
}
func (fake *Model) NumConnectionsReturnsOnCall(i int, result1 int) {
fake.numConnectionsMutex.Lock()
defer fake.numConnectionsMutex.Unlock()
fake.NumConnectionsStub = nil
if fake.numConnectionsReturnsOnCall == nil {
fake.numConnectionsReturnsOnCall = make(map[int]struct {
result1 int
})
}
fake.numConnectionsReturnsOnCall[i] = struct {
result1 int
}{result1}
}
func (fake *Model) OnHello(arg1 protocol.DeviceID, arg2 net.Addr, arg3 protocol.Hello) error { func (fake *Model) OnHello(arg1 protocol.DeviceID, arg2 net.Addr, arg3 protocol.Hello) error {
fake.onHelloMutex.Lock() fake.onHelloMutex.Lock()
ret, specificReturn := fake.onHelloReturnsOnCall[len(fake.onHelloArgsForCall)] ret, specificReturn := fake.onHelloReturnsOnCall[len(fake.onHelloArgsForCall)]
@ -3419,8 +3279,8 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.clusterConfigMutex.RUnlock() defer fake.clusterConfigMutex.RUnlock()
fake.completionMutex.RLock() fake.completionMutex.RLock()
defer fake.completionMutex.RUnlock() defer fake.completionMutex.RUnlock()
fake.connectionMutex.RLock() fake.connectedToMutex.RLock()
defer fake.connectionMutex.RUnlock() defer fake.connectedToMutex.RUnlock()
fake.connectionStatsMutex.RLock() fake.connectionStatsMutex.RLock()
defer fake.connectionStatsMutex.RUnlock() defer fake.connectionStatsMutex.RUnlock()
fake.currentFolderFileMutex.RLock() fake.currentFolderFileMutex.RLock()
@ -3449,8 +3309,6 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.folderStatisticsMutex.RUnlock() defer fake.folderStatisticsMutex.RUnlock()
fake.getFolderVersionsMutex.RLock() fake.getFolderVersionsMutex.RLock()
defer fake.getFolderVersionsMutex.RUnlock() defer fake.getFolderVersionsMutex.RUnlock()
fake.getHelloMutex.RLock()
defer fake.getHelloMutex.RUnlock()
fake.getMtimeMappingMutex.RLock() fake.getMtimeMappingMutex.RLock()
defer fake.getMtimeMappingMutex.RUnlock() defer fake.getMtimeMappingMutex.RUnlock()
fake.globalDirectoryTreeMutex.RLock() fake.globalDirectoryTreeMutex.RLock()
@ -3465,8 +3323,6 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.localChangedFolderFilesMutex.RUnlock() defer fake.localChangedFolderFilesMutex.RUnlock()
fake.needFolderFilesMutex.RLock() fake.needFolderFilesMutex.RLock()
defer fake.needFolderFilesMutex.RUnlock() defer fake.needFolderFilesMutex.RUnlock()
fake.numConnectionsMutex.RLock()
defer fake.numConnectionsMutex.RUnlock()
fake.onHelloMutex.RLock() fake.onHelloMutex.RLock()
defer fake.onHelloMutex.RUnlock() defer fake.onHelloMutex.RUnlock()
fake.overrideMutex.RLock() fake.overrideMutex.RLock()

View File

@ -37,6 +37,7 @@ import (
"github.com/syncthing/syncthing/lib/ignore" "github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/scanner" "github.com/syncthing/syncthing/lib/scanner"
"github.com/syncthing/syncthing/lib/semaphore" "github.com/syncthing/syncthing/lib/semaphore"
"github.com/syncthing/syncthing/lib/stats" "github.com/syncthing/syncthing/lib/stats"
@ -108,6 +109,7 @@ type Model interface {
DeviceStatistics() (map[protocol.DeviceID]stats.DeviceStatistics, error) DeviceStatistics() (map[protocol.DeviceID]stats.DeviceStatistics, error)
FolderStatistics() (map[string]stats.FolderStatistics, error) FolderStatistics() (map[string]stats.FolderStatistics, error)
UsageReportingStats(report *contract.Report, version int, preview bool) UsageReportingStats(report *contract.Report, version int, preview bool)
ConnectedTo(remoteID protocol.DeviceID) bool
PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error) PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error)
PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error) PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error)
@ -124,8 +126,6 @@ type model struct {
// constructor parameters // constructor parameters
cfg config.Wrapper cfg config.Wrapper
id protocol.DeviceID id protocol.DeviceID
clientName string
clientVersion string
db *db.Lowlevel db *db.Lowlevel
protectedFiles []string protectedFiles []string
evLogger events.Logger evLogger events.Logger
@ -143,6 +143,7 @@ type model struct {
fatalChan chan error fatalChan chan error
started chan struct{} started chan struct{}
keyGen *protocol.KeyGenerator keyGen *protocol.KeyGenerator
promotionTimer *time.Timer
// fields protected by fmut // fields protected by fmut
fmut sync.RWMutex fmut sync.RWMutex
@ -158,9 +159,11 @@ type model struct {
// fields protected by pmut // fields protected by pmut
pmut sync.RWMutex pmut sync.RWMutex
conn map[protocol.DeviceID]protocol.Connection connections map[string]protocol.Connection // connection ID -> connection
deviceConnIDs map[protocol.DeviceID][]string // device -> connection IDs (invariant: if the key exists, the value is len >= 1, with the primary connection at the start of the slice)
promotedConnID map[protocol.DeviceID]string // device -> latest promoted connection ID
connRequestLimiters map[protocol.DeviceID]*semaphore.Semaphore connRequestLimiters map[protocol.DeviceID]*semaphore.Semaphore
closed map[protocol.DeviceID]chan struct{} closed map[string]chan struct{} // connection ID -> closed channel
helloMessages map[protocol.DeviceID]protocol.Hello helloMessages map[protocol.DeviceID]protocol.Hello
deviceDownloads map[protocol.DeviceID]*deviceDownloadState deviceDownloads map[protocol.DeviceID]*deviceDownloadState
remoteFolderStates map[protocol.DeviceID]map[string]remoteFolderState // deviceID -> folders remoteFolderStates map[protocol.DeviceID]map[string]remoteFolderState // deviceID -> folders
@ -179,13 +182,11 @@ var folderFactories = make(map[config.FolderType]folderFactory)
var ( var (
errDeviceUnknown = errors.New("unknown device") errDeviceUnknown = errors.New("unknown device")
errDevicePaused = errors.New("device is paused") errDevicePaused = errors.New("device is paused")
errDeviceRemoved = errors.New("device has been removed")
ErrFolderPaused = errors.New("folder is paused") ErrFolderPaused = errors.New("folder is paused")
ErrFolderNotRunning = errors.New("folder is not running") ErrFolderNotRunning = errors.New("folder is not running")
ErrFolderMissing = errors.New("no such folder") ErrFolderMissing = errors.New("no such folder")
errNoVersioner = errors.New("folder has no versioner") errNoVersioner = errors.New("folder has no versioner")
// errors about why a connection is closed // errors about why a connection is closed
errReplacingConnection = errors.New("replacing connection")
errStopped = errors.New("Syncthing is being stopped") errStopped = errors.New("Syncthing is being stopped")
errEncryptionInvConfigLocal = errors.New("can't encrypt outgoing data because local data is encrypted (folder-type receive-encrypted)") errEncryptionInvConfigLocal = errors.New("can't encrypt outgoing data because local data is encrypted (folder-type receive-encrypted)")
errEncryptionInvConfigRemote = errors.New("remote has encrypted data and encrypts that data for us - this is impossible") errEncryptionInvConfigRemote = errors.New("remote has encrypted data and encrypts that data for us - this is impossible")
@ -203,7 +204,7 @@ var (
// NewModel creates and starts a new model. The model starts in read-only mode, // NewModel creates and starts a new model. The model starts in read-only mode,
// where it sends index information to connected peers and responds to requests // where it sends index information to connected peers and responds to requests
// for file data without altering the local folder in any way. // for file data without altering the local folder in any way.
func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string, evLogger events.Logger, keyGen *protocol.KeyGenerator) Model { func NewModel(cfg config.Wrapper, id protocol.DeviceID, ldb *db.Lowlevel, protectedFiles []string, evLogger events.Logger, keyGen *protocol.KeyGenerator) Model {
spec := svcutil.SpecWithDebugLogger(l) spec := svcutil.SpecWithDebugLogger(l)
m := &model{ m := &model{
Supervisor: suture.New("model", spec), Supervisor: suture.New("model", spec),
@ -211,8 +212,6 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
// constructor parameters // constructor parameters
cfg: cfg, cfg: cfg,
id: id, id: id,
clientName: clientName,
clientVersion: clientVersion,
db: ldb, db: ldb,
protectedFiles: protectedFiles, protectedFiles: protectedFiles,
evLogger: evLogger, evLogger: evLogger,
@ -226,6 +225,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
fatalChan: make(chan error), fatalChan: make(chan error),
started: make(chan struct{}), started: make(chan struct{}),
keyGen: keyGen, keyGen: keyGen,
promotionTimer: time.NewTimer(0),
// fields protected by fmut // fields protected by fmut
fmut: sync.NewRWMutex(), fmut: sync.NewRWMutex(),
@ -240,16 +240,19 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
// fields protected by pmut // fields protected by pmut
pmut: sync.NewRWMutex(), pmut: sync.NewRWMutex(),
conn: make(map[protocol.DeviceID]protocol.Connection), connections: make(map[string]protocol.Connection),
deviceConnIDs: make(map[protocol.DeviceID][]string),
promotedConnID: make(map[protocol.DeviceID]string),
connRequestLimiters: make(map[protocol.DeviceID]*semaphore.Semaphore), connRequestLimiters: make(map[protocol.DeviceID]*semaphore.Semaphore),
closed: make(map[protocol.DeviceID]chan struct{}), closed: make(map[string]chan struct{}),
helloMessages: make(map[protocol.DeviceID]protocol.Hello), helloMessages: make(map[protocol.DeviceID]protocol.Hello),
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState), deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
remoteFolderStates: make(map[protocol.DeviceID]map[string]remoteFolderState), remoteFolderStates: make(map[protocol.DeviceID]map[string]remoteFolderState),
indexHandlers: newServiceMap[protocol.DeviceID, *indexHandlerRegistry](evLogger), indexHandlers: newServiceMap[protocol.DeviceID, *indexHandlerRegistry](evLogger),
} }
for devID := range cfg.Devices() { for devID, cfg := range cfg.Devices() {
m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID) m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID)
m.setConnRequestLimitersPLocked(cfg)
} }
m.Add(m.folderRunners) m.Add(m.folderRunners)
m.Add(m.progressEmitter) m.Add(m.progressEmitter)
@ -272,11 +275,18 @@ func (m *model) serve(ctx context.Context) error {
close(m.started) close(m.started)
select { for {
case <-ctx.Done(): select {
return ctx.Err() case <-ctx.Done():
case err := <-m.fatalChan: l.Debugln(m, "context closed, stopping", ctx.Err())
return svcutil.AsFatalErr(err, svcutil.ExitError) return ctx.Err()
case err := <-m.fatalChan:
l.Debugln(m, "fatal error, stopping", err)
return svcutil.AsFatalErr(err, svcutil.ExitError)
case <-m.promotionTimer.C:
l.Debugln("promotion timer fired")
m.promoteConnections()
}
} }
} }
@ -303,9 +313,9 @@ func (m *model) initFolders(cfg config.Configuration) error {
func (m *model) closeAllConnectionsAndWait() { func (m *model) closeAllConnectionsAndWait() {
m.pmut.RLock() m.pmut.RLock()
closed := make([]chan struct{}, 0, len(m.conn)) closed := make([]chan struct{}, 0, len(m.connections))
for id, conn := range m.conn { for connID, conn := range m.connections {
closed = append(closed, m.closed[id]) closed = append(closed, m.closed[connID])
go conn.Close(errStopped) go conn.Close(errStopped)
} }
m.pmut.RUnlock() m.pmut.RUnlock()
@ -635,7 +645,7 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
// Transport stats // Transport stats
m.pmut.RLock() m.pmut.RLock()
for _, conn := range m.conn { for _, conn := range m.connections {
report.TransportStats[conn.Transport()]++ report.TransportStats[conn.Transport()]++
} }
m.pmut.RUnlock() m.pmut.RUnlock()
@ -699,22 +709,28 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
} }
} }
type ConnectionInfo struct { type ConnectionStats struct {
protocol.Statistics protocol.Statistics // Total for primary + secondaries
Connected bool `json:"connected"` Connected bool `json:"connected"`
Paused bool `json:"paused"` Paused bool `json:"paused"`
Address string `json:"address"`
ClientVersion string `json:"clientVersion"` ClientVersion string `json:"clientVersion"`
Type string `json:"type"`
IsLocal bool `json:"isLocal"` Address string `json:"address"` // mirror values from Primary, for compatibility with <1.24.0
Crypto string `json:"crypto"` Type string `json:"type"` // mirror values from Primary, for compatibility with <1.24.0
IsLocal bool `json:"isLocal"` // mirror values from Primary, for compatibility with <1.24.0
Crypto string `json:"crypto"` // mirror values from Primary, for compatibility with <1.24.0
Primary ConnectionInfo `json:"primary,omitempty"`
Secondary []ConnectionInfo `json:"secondary,omitempty"`
} }
// NumConnections returns the current number of active connected devices. type ConnectionInfo struct {
func (m *model) NumConnections() int { protocol.Statistics
m.pmut.RLock() Address string `json:"address"`
defer m.pmut.RUnlock() Type string `json:"type"`
return len(m.conn) IsLocal bool `json:"isLocal"`
Crypto string `json:"crypto"`
} }
// ConnectionStats returns a map with connection statistics for each device. // ConnectionStats returns a map with connection statistics for each device.
@ -724,29 +740,59 @@ func (m *model) ConnectionStats() map[string]interface{} {
res := make(map[string]interface{}) res := make(map[string]interface{})
devs := m.cfg.Devices() devs := m.cfg.Devices()
conns := make(map[string]ConnectionInfo, len(devs)) conns := make(map[string]ConnectionStats, len(devs))
for device, deviceCfg := range devs { for device, deviceCfg := range devs {
if device == m.id {
continue
}
hello := m.helloMessages[device] hello := m.helloMessages[device]
versionString := hello.ClientVersion versionString := hello.ClientVersion
if hello.ClientName != "syncthing" { if hello.ClientName != "syncthing" {
versionString = hello.ClientName + " " + hello.ClientVersion versionString = hello.ClientName + " " + hello.ClientVersion
} }
ci := ConnectionInfo{ connIDs, ok := m.deviceConnIDs[device]
ClientVersion: strings.TrimSpace(versionString), cs := ConnectionStats{
Connected: ok,
Paused: deviceCfg.Paused, Paused: deviceCfg.Paused,
ClientVersion: strings.TrimSpace(versionString),
} }
if conn, ok := m.conn[device]; ok { if ok {
ci.Type = conn.Type() conn := m.connections[connIDs[0]]
ci.IsLocal = conn.IsLocal()
ci.Crypto = conn.Crypto() cs.Primary.Type = conn.Type()
ci.Connected = ok cs.Primary.IsLocal = conn.IsLocal()
ci.Statistics = conn.Statistics() cs.Primary.Crypto = conn.Crypto()
if addr := conn.RemoteAddr(); addr != nil { cs.Primary.Statistics = conn.Statistics()
ci.Address = addr.String() cs.Primary.Address = conn.RemoteAddr().String()
cs.Type = cs.Primary.Type
cs.IsLocal = cs.Primary.IsLocal
cs.Crypto = cs.Primary.Crypto
cs.Address = cs.Primary.Address
cs.Statistics = cs.Primary.Statistics
for _, connID := range connIDs[1:] {
conn = m.connections[connID]
sec := ConnectionInfo{
Statistics: conn.Statistics(),
Address: conn.RemoteAddr().String(),
Type: conn.Type(),
IsLocal: conn.IsLocal(),
Crypto: conn.Crypto(),
}
if sec.At.After(cs.At) {
cs.At = sec.At
}
if sec.StartedAt.Before(cs.StartedAt) {
cs.StartedAt = sec.StartedAt
}
cs.InBytesTotal += sec.InBytesTotal
cs.OutBytesTotal += sec.OutBytesTotal
cs.Secondary = append(cs.Secondary, sec)
} }
} }
conns[device.String()] = ci conns[device.String()] = cs
} }
res["connections"] = conns res["connections"] = conns
@ -1138,17 +1184,16 @@ func (m *model) handleIndex(conn protocol.Connection, folder string, fs []protoc
} }
m.pmut.RLock() m.pmut.RLock()
indexHandler, ok := m.indexHandlers.Get(deviceID) indexHandler, ok := m.getIndexHandlerPRLocked(conn)
m.pmut.RUnlock() m.pmut.RUnlock()
if !ok { if !ok {
// This should be impossible, as an index handler always exists for an // This should be impossible, as an index handler is registered when
// open connection, and this method can't be called on a closed // we send a cluster config, and that is what triggers index
// connection // sending.
m.evLogger.Log(events.Failure, "index sender does not exist for connection on which indexes were received") m.evLogger.Log(events.Failure, "index sender does not exist for connection on which indexes were received")
l.Debugf("%v for folder (ID %q) sent from device %q: missing index handler", op, folder, deviceID) l.Debugf("%v for folder (ID %q) sent from device %q: missing index handler", op, folder, deviceID)
return fmt.Errorf("index handler missing: %s", folder) return fmt.Errorf("%s: %w", folder, ErrFolderNotRunning)
} }
return indexHandler.ReceiveIndex(folder, fs, update, op) return indexHandler.ReceiveIndex(folder, fs, update, op)
} }
@ -1161,24 +1206,26 @@ type ClusterConfigReceivedEventData struct {
} }
func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfig) error { func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfig) error {
deviceID := conn.DeviceID()
if cm.Secondary {
// No handling of secondary connection ClusterConfigs; they merely
// indicate the connection is ready to start.
l.Debugf("Skipping secondary ClusterConfig from %v at %s", deviceID.Short(), conn)
return nil
}
// Check the peer device's announced folders against our own. Emits events // Check the peer device's announced folders against our own. Emits events
// for folders that we don't expect (unknown or not shared). // for folders that we don't expect (unknown or not shared).
// Also, collect a list of folders we do share, and if he's interested in // Also, collect a list of folders we do share, and if he's interested in
// temporary indexes, subscribe the connection. // temporary indexes, subscribe the connection.
deviceID := conn.DeviceID() l.Debugf("Handling ClusterConfig from %v at %s", deviceID.Short(), conn)
l.Debugf("Handling ClusterConfig from %v", deviceID.Short()) indexHandlerRegistry := m.ensureIndexHandler(conn)
m.pmut.RLock()
indexHandlerRegistry, ok := m.indexHandlers.Get(deviceID)
m.pmut.RUnlock()
if !ok {
panic("bug: ClusterConfig called on closed or nonexistent connection")
}
deviceCfg, ok := m.cfg.Device(deviceID) deviceCfg, ok := m.cfg.Device(deviceID)
if !ok { if !ok {
l.Debugln("Device disappeared from config while processing cluster-config") l.Debugf("Device %s disappeared from config while processing cluster-config", deviceID.Short())
return errDeviceUnknown return errDeviceUnknown
} }
@ -1198,11 +1245,11 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
} }
} }
if info.remote.ID == protocol.EmptyDeviceID { if info.remote.ID == protocol.EmptyDeviceID {
l.Infof("Device %v sent cluster-config without the device info for the remote on folder %v", deviceID, folder.Description()) l.Infof("Device %v sent cluster-config without the device info for the remote on folder %v", deviceID.Short(), folder.Description())
return errMissingRemoteInClusterConfig return errMissingRemoteInClusterConfig
} }
if info.local.ID == protocol.EmptyDeviceID { if info.local.ID == protocol.EmptyDeviceID {
l.Infof("Device %v sent cluster-config without the device info for us locally on folder %v", deviceID, folder.Description()) l.Infof("Device %v sent cluster-config without the device info for us locally on folder %v", deviceID.Short(), folder.Description())
return errMissingLocalInClusterConfig return errMissingLocalInClusterConfig
} }
ccDeviceInfos[folder.ID] = info ccDeviceInfos[folder.ID] = info
@ -1260,12 +1307,16 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
}) })
if len(tempIndexFolders) > 0 { if len(tempIndexFolders) > 0 {
var connOK bool
var conn protocol.Connection
m.pmut.RLock() m.pmut.RLock()
conn, ok := m.conn[deviceID] if connIDs, connIDOK := m.deviceConnIDs[deviceID]; connIDOK {
conn, connOK = m.connections[connIDs[0]]
}
m.pmut.RUnlock() m.pmut.RUnlock()
// In case we've got ClusterConfig, and the connection disappeared // In case we've got ClusterConfig, and the connection disappeared
// from infront of our nose. // from infront of our nose.
if ok { if connOK {
m.progressEmitter.temporaryIndexSubscribe(conn, tempIndexFolders) m.progressEmitter.temporaryIndexSubscribe(conn, tempIndexFolders)
} }
} }
@ -1291,6 +1342,57 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
return nil return nil
} }
func (m *model) ensureIndexHandler(conn protocol.Connection) *indexHandlerRegistry {
deviceID := conn.DeviceID()
connID := conn.ConnectionID()
m.pmut.Lock()
defer m.pmut.Unlock()
indexHandlerRegistry, ok := m.indexHandlers.Get(deviceID)
if ok && indexHandlerRegistry.conn.ConnectionID() == connID {
// This is an existing and proper index handler for this connection.
return indexHandlerRegistry
}
if ok {
// A handler exists, but it's for another connection than the one we
// now got a ClusterConfig on. This should be unusual as it means
// the other side has decided to start using a new primary
// connection but we haven't seen it close yet. Ideally it will
// close shortly by itself...
l.Infof("Abandoning old index handler for %s (%s) in favour of %s", deviceID.Short(), indexHandlerRegistry.conn.ConnectionID(), connID)
m.indexHandlers.RemoveAndWait(deviceID, 0)
}
// Create a new index handler for this device.
indexHandlerRegistry = newIndexHandlerRegistry(conn, m.deviceDownloads[deviceID], m.evLogger)
for id, fcfg := range m.folderCfgs {
l.Debugln("Registering folder", id, "for", deviceID.Short())
runner, _ := m.folderRunners.Get(id)
indexHandlerRegistry.RegisterFolderState(fcfg, m.folderFiles[id], runner)
}
m.indexHandlers.Add(deviceID, indexHandlerRegistry)
return indexHandlerRegistry
}
func (m *model) getIndexHandlerPRLocked(conn protocol.Connection) (*indexHandlerRegistry, bool) {
// Reads from index handlers, which requires pmut to be read locked
deviceID := conn.DeviceID()
connID := conn.ConnectionID()
indexHandlerRegistry, ok := m.indexHandlers.Get(deviceID)
if ok && indexHandlerRegistry.conn.ConnectionID() == connID {
// This is an existing and proper index handler for this connection.
return indexHandlerRegistry, true
}
// There is no index handler, or it's not registered for this connection.
return nil, false
}
func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*clusterConfigDeviceInfo, indexHandlers *indexHandlerRegistry) ([]string, map[string]remoteFolderState, error) { func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*clusterConfigDeviceInfo, indexHandlers *indexHandlerRegistry) ([]string, map[string]remoteFolderState, error) {
var folderDevice config.FolderDeviceConfiguration var folderDevice config.FolderDeviceConfiguration
tempIndexFolders := make([]string, 0, len(folders)) tempIndexFolders := make([]string, 0, len(folders))
@ -1539,8 +1641,8 @@ func (m *model) sendClusterConfig(ids []protocol.DeviceID) {
ccConns := make([]protocol.Connection, 0, len(ids)) ccConns := make([]protocol.Connection, 0, len(ids))
m.pmut.RLock() m.pmut.RLock()
for _, id := range ids { for _, id := range ids {
if conn, ok := m.conn[id]; ok { if connIDs, ok := m.deviceConnIDs[id]; ok {
ccConns = append(ccConns, conn) ccConns = append(ccConns, m.connections[connIDs[0]])
} }
} }
m.pmut.RUnlock() m.pmut.RUnlock()
@ -1777,33 +1879,62 @@ func (m *model) introduceDevice(device protocol.Device, introducerCfg config.Dev
// Closed is called when a connection has been closed // Closed is called when a connection has been closed
func (m *model) Closed(conn protocol.Connection, err error) { func (m *model) Closed(conn protocol.Connection, err error) {
device := conn.DeviceID() connID := conn.ConnectionID()
deviceID := conn.DeviceID()
m.pmut.Lock() m.pmut.Lock()
conn, ok := m.conn[device] conn, ok := m.connections[connID]
if !ok { if !ok {
m.pmut.Unlock() m.pmut.Unlock()
return return
} }
delete(m.conn, device) closed := m.closed[connID]
delete(m.connRequestLimiters, device) delete(m.closed, connID)
delete(m.helloMessages, device) delete(m.connections, connID)
delete(m.deviceDownloads, device)
delete(m.remoteFolderStates, device) removedIsPrimary := m.promotedConnID[deviceID] == connID
closed := m.closed[device] remainingConns := without(m.deviceConnIDs[deviceID], connID)
delete(m.closed, device) var wait <-chan error
wait := m.indexHandlers.RemoveAndWaitChan(device, 0) if removedIsPrimary {
m.progressEmitter.temporaryIndexUnsubscribe(conn)
if idxh, ok := m.indexHandlers.Get(deviceID); ok && idxh.conn.ConnectionID() == connID {
wait = m.indexHandlers.RemoveAndWaitChan(deviceID, 0)
}
m.scheduleConnectionPromotion()
}
if len(remainingConns) == 0 {
// All device connections closed
delete(m.deviceConnIDs, deviceID)
delete(m.promotedConnID, deviceID)
delete(m.connRequestLimiters, deviceID)
delete(m.helloMessages, deviceID)
delete(m.remoteFolderStates, deviceID)
delete(m.deviceDownloads, deviceID)
} else {
// Some connections remain
m.deviceConnIDs[deviceID] = remainingConns
}
m.pmut.Unlock() m.pmut.Unlock()
<-wait if wait != nil {
<-wait
}
m.progressEmitter.temporaryIndexUnsubscribe(conn) m.fmut.RLock()
m.deviceDidClose(device, time.Since(conn.EstablishedAt())) m.deviceDidCloseFRLocked(deviceID, time.Since(conn.EstablishedAt()))
m.fmut.RUnlock()
l.Infof("Connection to %s at %s closed: %v", device, conn, err) k := map[bool]string{false: "secondary", true: "primary"}[removedIsPrimary]
m.evLogger.Log(events.DeviceDisconnected, map[string]string{ l.Infof("Lost %s connection to %s at %s: %v (%d remain)", k, deviceID.Short(), conn, err, len(remainingConns))
"id": device.String(),
"error": err.Error(), if len(remainingConns) == 0 {
}) l.Infof("Connection to %s at %s closed: %v", deviceID.Short(), conn, err)
m.evLogger.Log(events.DeviceDisconnected, map[string]string{
"id": deviceID.String(),
"error": err.Error(),
})
}
close(closed) close(closed)
} }
@ -1852,36 +1983,36 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
if !ok { if !ok {
// The folder might be already unpaused in the config, but not yet // The folder might be already unpaused in the config, but not yet
// in the model. // in the model.
l.Debugf("Request from %s for file %s in unstarted folder %q", deviceID, name, folder) l.Debugf("Request from %s for file %s in unstarted folder %q", deviceID.Short(), name, folder)
return nil, protocol.ErrGeneric return nil, protocol.ErrGeneric
} }
if !folderCfg.SharedWith(deviceID) { if !folderCfg.SharedWith(deviceID) {
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder) l.Warnf("Request from %s for file %s in unshared folder %q", deviceID.Short(), name, folder)
return nil, protocol.ErrGeneric return nil, protocol.ErrGeneric
} }
if folderCfg.Paused { if folderCfg.Paused {
l.Debugf("Request from %s for file %s in paused folder %q", deviceID, name, folder) l.Debugf("Request from %s for file %s in paused folder %q", deviceID.Short(), name, folder)
return nil, protocol.ErrGeneric return nil, protocol.ErrGeneric
} }
// Make sure the path is valid and in canonical form // Make sure the path is valid and in canonical form
if name, err = fs.Canonicalize(name); err != nil { if name, err = fs.Canonicalize(name); err != nil {
l.Debugf("Request from %s in folder %q for invalid filename %s", deviceID, folder, name) l.Debugf("Request from %s in folder %q for invalid filename %s", deviceID.Short(), folder, name)
return nil, protocol.ErrGeneric return nil, protocol.ErrGeneric
} }
if deviceID != protocol.LocalDeviceID { if deviceID != protocol.LocalDeviceID {
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID, folder, name, offset, size, fromTemporary) l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID.Short(), folder, name, offset, size, fromTemporary)
} }
if fs.IsInternal(name) { if fs.IsInternal(name) {
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrInvalid return nil, protocol.ErrInvalid
} }
if folderIgnores.Match(name).IsIgnored() { if folderIgnores.Match(name).IsIgnored() {
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrInvalid return nil, protocol.ErrInvalid
} }
@ -1908,7 +2039,7 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
folderFs := folderCfg.Filesystem(nil) folderFs := folderCfg.Filesystem(nil)
if err := osutil.TraversesSymlink(folderFs, filepath.Dir(name)); err != nil { if err := osutil.TraversesSymlink(folderFs, filepath.Dir(name)); err != nil {
l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile return nil, protocol.ErrNoSuchFile
} }
@ -1920,7 +2051,7 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
if info, err := folderFs.Lstat(tempFn); err != nil || !info.IsRegular() { if info, err := folderFs.Lstat(tempFn); err != nil || !info.IsRegular() {
// Reject reads for anything that doesn't exist or is something // Reject reads for anything that doesn't exist or is something
// other than a regular file. // other than a regular file.
l.Debugf("%v REQ(in) failed stating temp file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) failed stating temp file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile return nil, protocol.ErrNoSuchFile
} }
_, err := readOffsetIntoBuf(folderFs, tempFn, offset, res.data) _, err := readOffsetIntoBuf(folderFs, tempFn, offset, res.data)
@ -1934,13 +2065,13 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
if info, err := folderFs.Lstat(name); err != nil || !info.IsRegular() { if info, err := folderFs.Lstat(name); err != nil || !info.IsRegular() {
// Reject reads for anything that doesn't exist or is something // Reject reads for anything that doesn't exist or is something
// other than a regular file. // other than a regular file.
l.Debugf("%v REQ(in) failed stating file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) failed stating file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile return nil, protocol.ErrNoSuchFile
} }
n, err := readOffsetIntoBuf(folderFs, name, offset, res.data) n, err := readOffsetIntoBuf(folderFs, name, offset, res.data)
if fs.IsNotExist(err) { if fs.IsNotExist(err) {
l.Debugf("%v REQ(in) file doesn't exist: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) file doesn't exist: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile return nil, protocol.ErrNoSuchFile
} else if err == io.EOF { } else if err == io.EOF {
// Read beyond end of file. This might indicate a problem, or it // Read beyond end of file. This might indicate a problem, or it
@ -1949,13 +2080,13 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
// next step take care of it, by only hashing the part we actually // next step take care of it, by only hashing the part we actually
// managed to read. // managed to read.
} else if err != nil { } else if err != nil {
l.Debugf("%v REQ(in) failed reading file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) failed reading file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrGeneric return nil, protocol.ErrGeneric
} }
if folderCfg.Type != config.FolderTypeReceiveEncrypted && len(hash) > 0 && !scanner.Validate(res.data[:n], hash, weakHash) { if folderCfg.Type != config.FolderTypeReceiveEncrypted && len(hash) > 0 && !scanner.Validate(res.data[:n], hash, weakHash) {
m.recheckFile(deviceID, folder, name, offset, hash, weakHash) m.recheckFile(deviceID, folder, name, offset, hash, weakHash)
l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size) l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile return nil, protocol.ErrNoSuchFile
} }
@ -2074,15 +2205,12 @@ func (m *model) GetMtimeMapping(folder string, file string) (fs.MtimeMapping, er
return fs.GetMtimeMapping(fcfg.Filesystem(ffs), file) return fs.GetMtimeMapping(fcfg.Filesystem(ffs), file)
} }
// Connection returns the current connection for device, and a boolean whether a connection was found. // Connection returns if we are connected to the given device.
func (m *model) Connection(deviceID protocol.DeviceID) (protocol.Connection, bool) { func (m *model) ConnectedTo(deviceID protocol.DeviceID) bool {
m.pmut.RLock() m.pmut.RLock()
cn, ok := m.conn[deviceID] _, ok := m.deviceConnIDs[deviceID]
m.pmut.RUnlock() m.pmut.RUnlock()
if ok { return ok
m.deviceWasSeen(deviceID)
}
return cn, ok
} }
// LoadIgnores loads or refreshes the ignore patterns from disk, if the // LoadIgnores loads or refreshes the ignore patterns from disk, if the
@ -2200,74 +2328,29 @@ func (m *model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protoco
return nil return nil
} }
// GetHello is called when we are about to connect to some remote device.
func (m *model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
name := ""
if _, ok := m.cfg.Device(id); ok {
// Set our name (from the config of our device ID) only if we already know about the other side device ID.
if myCfg, ok := m.cfg.Device(m.id); ok {
name = myCfg.Name
}
}
return &protocol.Hello{
DeviceName: name,
ClientName: m.clientName,
ClientVersion: m.clientVersion,
}
}
// AddConnection adds a new peer connection to the model. An initial index will // AddConnection adds a new peer connection to the model. An initial index will
// be sent to the connected peer, thereafter index updates whenever the local // be sent to the connected peer, thereafter index updates whenever the local
// folder changes. // folder changes.
func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) { func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
deviceID := conn.DeviceID() deviceID := conn.DeviceID()
device, ok := m.cfg.Device(deviceID) deviceCfg, ok := m.cfg.Device(deviceID)
if !ok { if !ok {
l.Infoln("Trying to add connection to unknown device") l.Infoln("Trying to add connection to unknown device")
return return
} }
// The slightly unusual locking sequence here is because we must acquire connID := conn.ConnectionID()
// fmut before pmut. (The locks can be *released* in any order.)
m.fmut.RLock()
m.pmut.Lock()
if oldConn, ok := m.conn[deviceID]; ok {
l.Infoln("Replacing old connection", oldConn, "with", conn, "for", deviceID)
// There is an existing connection to this device that we are
// replacing. We must close the existing connection and wait for the
// close to complete before adding the new connection. We do the
// actual close without holding pmut as the connection will call
// back into Closed() for the cleanup.
closed := m.closed[deviceID]
m.fmut.RUnlock()
m.pmut.Unlock()
oldConn.Close(errReplacingConnection)
<-closed
// Again, lock fmut before pmut.
m.fmut.RLock()
m.pmut.Lock()
}
m.conn[deviceID] = conn
closed := make(chan struct{}) closed := make(chan struct{})
m.closed[deviceID] = closed
m.deviceDownloads[deviceID] = newDeviceDownloadState()
indexRegistry := newIndexHandlerRegistry(conn, m.deviceDownloads[deviceID], m.evLogger)
for id, fcfg := range m.folderCfgs {
runner, _ := m.folderRunners.Get(id)
indexRegistry.RegisterFolderState(fcfg, m.folderFiles[id], runner)
}
m.indexHandlers.Add(deviceID, indexRegistry)
m.fmut.RUnlock()
// 0: default, <0: no limiting
switch {
case device.MaxRequestKiB > 0:
m.connRequestLimiters[deviceID] = semaphore.New(1024 * device.MaxRequestKiB)
case device.MaxRequestKiB == 0:
m.connRequestLimiters[deviceID] = semaphore.New(1024 * defaultPullerPendingKiB)
}
m.pmut.Lock()
m.connections[connID] = conn
m.closed[connID] = closed
m.helloMessages[deviceID] = hello m.helloMessages[deviceID] = hello
m.deviceConnIDs[deviceID] = append(m.deviceConnIDs[deviceID], connID)
if m.deviceDownloads[deviceID] == nil {
m.deviceDownloads[deviceID] = newDeviceDownloadState()
}
event := map[string]string{ event := map[string]string{
"id": deviceID.String(), "id": deviceID.String(),
@ -2284,17 +2367,15 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
m.evLogger.Log(events.DeviceConnected, event) m.evLogger.Log(events.DeviceConnected, event)
l.Infof(`Device %s client is "%s %s" named "%s" at %s`, deviceID, hello.ClientName, hello.ClientVersion, hello.DeviceName, conn) if len(m.deviceConnIDs[deviceID]) == 1 {
l.Infof(`Device %s client is "%s %s" named "%s" at %s`, deviceID.Short(), hello.ClientName, hello.ClientVersion, hello.DeviceName, conn)
} else {
l.Infof(`Additional connection (+%d) for device %s at %s`, len(m.deviceConnIDs[deviceID])-1, deviceID.Short(), conn)
}
conn.Start()
m.pmut.Unlock() m.pmut.Unlock()
// Acquires fmut, so has to be done outside of pmut. if (deviceCfg.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
cm, passwords := m.generateClusterConfig(deviceID)
conn.SetFolderPasswords(passwords)
conn.ClusterConfig(cm)
if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
m.cfg.Modify(func(cfg *config.Configuration) { m.cfg.Modify(func(cfg *config.Configuration) {
for i := range cfg.Devices { for i := range cfg.Devices {
if cfg.Devices[i].DeviceID == deviceID { if cfg.Devices[i].DeviceID == deviceID {
@ -2308,26 +2389,78 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
} }
m.deviceWasSeen(deviceID) m.deviceWasSeen(deviceID)
m.scheduleConnectionPromotion()
}
func (m *model) scheduleConnectionPromotion() {
// Keeps deferring to prevent multiple executions in quick succession,
// e.g. if multiple connections to a single device are closed.
m.promotionTimer.Reset(time.Second)
}
// promoteConnections checks for devices that have connections, but where
// the primary connection hasn't started index handlers etc. yet, and
// promotes the primary connection to be the index handling one. This should
// be called after adding new connections, and after closing a primary
// device connection.
func (m *model) promoteConnections() {
m.fmut.RLock() // for generateClusterConfigFRLocked
defer m.fmut.RUnlock()
m.pmut.Lock() // for most other things
defer m.pmut.Unlock()
for deviceID, connIDs := range m.deviceConnIDs {
cm, passwords := m.generateClusterConfigFRLocked(deviceID)
if m.promotedConnID[deviceID] != connIDs[0] {
// The previously promoted connection is not the current
// primary; we should promote the primary connection to be the
// index handling one. We do this by sending a ClusterConfig on
// it, which will cause the other side to start sending us index
// messages there. (On our side, we manage index handlers based
// on where we get ClusterConfigs from the peer.)
conn := m.connections[connIDs[0]]
l.Debugf("Promoting connection to %s at %s", deviceID.Short(), conn)
if conn.Statistics().StartedAt.IsZero() {
conn.SetFolderPasswords(passwords)
conn.Start()
}
conn.ClusterConfig(cm)
m.promotedConnID[deviceID] = connIDs[0]
}
// Make sure any other new connections also get started, and that
// they get a secondary-marked ClusterConfig.
for _, connID := range connIDs[1:] {
conn := m.connections[connID]
if conn.Statistics().StartedAt.IsZero() {
conn.SetFolderPasswords(passwords)
conn.Start()
conn.ClusterConfig(protocol.ClusterConfig{Secondary: true})
}
}
}
} }
func (m *model) DownloadProgress(conn protocol.Connection, folder string, updates []protocol.FileDownloadProgressUpdate) error { func (m *model) DownloadProgress(conn protocol.Connection, folder string, updates []protocol.FileDownloadProgressUpdate) error {
deviceID := conn.DeviceID()
m.fmut.RLock() m.fmut.RLock()
cfg, ok := m.folderCfgs[folder] cfg, ok := m.folderCfgs[folder]
m.fmut.RUnlock() m.fmut.RUnlock()
device := conn.DeviceID() if !ok || cfg.DisableTempIndexes || !cfg.SharedWith(deviceID) {
if !ok || cfg.DisableTempIndexes || !cfg.SharedWith(device) {
return nil return nil
} }
m.pmut.RLock() m.pmut.RLock()
downloads := m.deviceDownloads[device] downloads := m.deviceDownloads[deviceID]
m.pmut.RUnlock() m.pmut.RUnlock()
downloads.Update(folder, updates) downloads.Update(folder, updates)
state := downloads.GetBlockCounts(folder) state := downloads.GetBlockCounts(folder)
m.evLogger.Log(events.RemoteDownloadProgress, map[string]interface{}{ m.evLogger.Log(events.RemoteDownloadProgress, map[string]interface{}{
"device": device.String(), "device": deviceID.String(),
"folder": folder, "folder": folder,
"state": state, "state": state,
}) })
@ -2344,27 +2477,47 @@ func (m *model) deviceWasSeen(deviceID protocol.DeviceID) {
} }
} }
func (m *model) deviceDidClose(deviceID protocol.DeviceID, duration time.Duration) { func (m *model) deviceDidCloseFRLocked(deviceID protocol.DeviceID, duration time.Duration) {
m.fmut.RLock() if sr, ok := m.deviceStatRefs[deviceID]; ok {
sr, ok := m.deviceStatRefs[deviceID]
m.fmut.RUnlock()
if ok {
_ = sr.LastConnectionDuration(duration) _ = sr.LastConnectionDuration(duration)
} }
} }
func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) { func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
m.pmut.RLock() conn, connOK := m.requestConnectionForDevice(deviceID)
nc, ok := m.conn[deviceID] if !connOK {
m.pmut.RUnlock() return nil, fmt.Errorf("requestGlobal: no connection to device: %s", deviceID.Short())
if !ok {
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
} }
l.Debugf("%v REQ(out): %s: %q / %q b=%d o=%d s=%d h=%x wh=%x ft=%t", m, deviceID, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary) l.Debugf("%v REQ(out): %s (%s): %q / %q b=%d o=%d s=%d h=%x wh=%x ft=%t", m, deviceID.Short(), conn, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
return conn.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
}
return nc.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary) // requestConnectionForDevice returns a connection to the given device, to
// be used for sending a request. If there is only one device connection,
// this is the one to use. If there are multiple then we avoid the first
// ("primary") connection, which is dedicated to index data, and pick a
// random one of the others.
func (m *model) requestConnectionForDevice(deviceID protocol.DeviceID) (protocol.Connection, bool) {
m.pmut.RLock()
defer m.pmut.RUnlock()
connIDs, ok := m.deviceConnIDs[deviceID]
if !ok {
return nil, false
}
// If there is an entry in deviceConns, it always contains at least one
// connection.
connID := connIDs[0]
if len(connIDs) > 1 {
// Pick a random connection of the non-primary ones
idx := rand.Intn(len(connIDs)-1) + 1
connID = connIDs[idx]
}
conn, connOK := m.connections[connID]
return conn, connOK
} }
func (m *model) ScanFolders() map[string]error { func (m *model) ScanFolders() map[string]error {
@ -2455,11 +2608,13 @@ func (m *model) numHashers(folder string) int {
// generateClusterConfig returns a ClusterConfigMessage that is correct and the // generateClusterConfig returns a ClusterConfigMessage that is correct and the
// set of folder passwords for the given peer device // set of folder passwords for the given peer device
func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) { func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
var message protocol.ClusterConfig
m.fmut.RLock() m.fmut.RLock()
defer m.fmut.RUnlock() defer m.fmut.RUnlock()
return m.generateClusterConfigFRLocked(device)
}
func (m *model) generateClusterConfigFRLocked(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
var message protocol.ClusterConfig
folders := m.cfg.FolderList() folders := m.cfg.FolderList()
passwords := make(map[string]string, len(folders)) passwords := make(map[string]string, len(folders))
for _, folderCfg := range folders { for _, folderCfg := range folders {
@ -2771,7 +2926,7 @@ func (m *model) availabilityInSnapshotPRlocked(cfg config.FolderConfiguration, s
if state := m.remoteFolderStates[device][cfg.ID]; state != remoteFolderValid { if state := m.remoteFolderStates[device][cfg.ID]; state != remoteFolderValid {
continue continue
} }
_, ok := m.conn[device] _, ok := m.deviceConnIDs[device]
if ok { if ok {
availabilities = append(availabilities, Availability{ID: device, FromTemporary: false}) availabilities = append(availabilities, Availability{ID: device, FromTemporary: false})
} }
@ -2904,13 +3059,6 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
} }
} }
// Removing a device. We actually don't need to do anything.
// Because folder config has changed (since the device lists do not match)
// Folders for that had device got "restarted", which involves killing
// connections to all devices that we were sharing the folder with.
// At some point model.Close() will get called for that device which will
// clean residue device state that is not part of any folder.
// Pausing a device, unpausing is handled by the connection service. // Pausing a device, unpausing is handled by the connection service.
fromDevices := from.DeviceMap() fromDevices := from.DeviceMap()
toDevices := to.DeviceMap() toDevices := to.DeviceMap()
@ -2941,7 +3089,14 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
l.Infoln("Resuming", deviceID) l.Infoln("Resuming", deviceID)
m.evLogger.Log(events.DeviceResumed, map[string]string{"device": deviceID.String()}) m.evLogger.Log(events.DeviceResumed, map[string]string{"device": deviceID.String()})
} }
if toCfg.MaxRequestKiB != fromCfg.MaxRequestKiB {
m.pmut.Lock()
m.setConnRequestLimitersPLocked(toCfg)
m.pmut.Unlock()
}
} }
// Clean up after removed devices // Clean up after removed devices
removedDevices := make([]protocol.DeviceID, 0, len(fromDevices)) removedDevices := make([]protocol.DeviceID, 0, len(fromDevices))
m.fmut.Lock() m.fmut.Lock()
@ -2955,14 +3110,18 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
m.pmut.RLock() m.pmut.RLock()
for _, id := range closeDevices { for _, id := range closeDevices {
delete(clusterConfigDevices, id) delete(clusterConfigDevices, id)
if conn, ok := m.conn[id]; ok { if conns, ok := m.deviceConnIDs[id]; ok {
go conn.Close(errDevicePaused) for _, connID := range conns {
go m.connections[connID].Close(errDevicePaused)
}
} }
} }
for _, id := range removedDevices { for _, id := range removedDevices {
delete(clusterConfigDevices, id) delete(clusterConfigDevices, id)
if conn, ok := m.conn[id]; ok { if conns, ok := m.deviceConnIDs[id]; ok {
go conn.Close(errDeviceRemoved) for _, connID := range conns {
go m.connections[connID].Close(errDevicePaused)
}
} }
} }
m.pmut.RUnlock() m.pmut.RUnlock()
@ -2986,6 +3145,17 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
return true return true
} }
func (m *model) setConnRequestLimitersPLocked(cfg config.DeviceConfiguration) {
// Touches connRequestLimiters which is protected by pmut.
// 0: default, <0: no limiting
switch {
case cfg.MaxRequestKiB > 0:
m.connRequestLimiters[cfg.DeviceID] = semaphore.New(1024 * cfg.MaxRequestKiB)
case cfg.MaxRequestKiB == 0:
m.connRequestLimiters[cfg.DeviceID] = semaphore.New(1024 * defaultPullerPendingKiB)
}
}
func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.DeviceConfiguration, existingFolders map[string]config.FolderConfiguration, ignoredDevices deviceIDSet, removedFolders map[string]struct{}) { func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.DeviceConfiguration, existingFolders map[string]config.FolderConfiguration, ignoredDevices deviceIDSet, removedFolders map[string]struct{}) {
var removedPendingFolders []map[string]string var removedPendingFolders []map[string]string
pendingFolders, err := m.db.PendingFolders() pendingFolders, err := m.db.PendingFolders()
@ -3332,3 +3502,12 @@ type redactedError struct {
error error
redacted error redacted error
} }
func without[E comparable, S ~[]E](s S, e E) S {
for i, x := range s {
if x == e {
return append(s[:i], s[i+1:]...)
}
}
return s
}

View File

@ -270,7 +270,7 @@ func TestDeviceRename(t *testing.T) {
cfg, cfgCancel := newConfigWrapper(rawCfg) cfg, cfgCancel := newConfigWrapper(rawCfg)
defer cfgCancel() defer cfgCancel()
m := newModel(t, cfg, myID, "syncthing", "dev", nil) m := newModel(t, cfg, myID, nil)
if cfg.Devices()[device1].Name != "" { if cfg.Devices()[device1].Name != "" {
t.Errorf("Device already has a name") t.Errorf("Device already has a name")
@ -422,7 +422,7 @@ func TestClusterConfig(t *testing.T) {
wrapper, cancel := newConfigWrapper(cfg) wrapper, cancel := newConfigWrapper(cfg)
defer cancel() defer cancel()
m := newModel(t, wrapper, myID, "syncthing", "dev", nil) m := newModel(t, wrapper, myID, nil)
m.ServeBackground() m.ServeBackground()
defer cleanupModel(m) defer cleanupModel(m)
@ -903,7 +903,7 @@ func TestIssue5063(t *testing.T) {
defer cancel() defer cancel()
m.pmut.Lock() m.pmut.Lock()
for _, c := range m.conn { for _, c := range m.connections {
conn := c.(*fakeConnection) conn := c.(*fakeConnection)
conn.CloseCalls(func(_ error) {}) conn.CloseCalls(func(_ error) {})
defer m.Closed(c, errStopped) // to unblock deferred m.Stop() defer m.Closed(c, errStopped) // to unblock deferred m.Stop()
@ -1626,7 +1626,7 @@ func TestROScanRecovery(t *testing.T) {
}, },
}) })
defer cancel() defer cancel()
m := newModel(t, cfg, myID, "syncthing", "dev", nil) m := newModel(t, cfg, myID, nil)
set := newFileSet(t, "default", m.db) set := newFileSet(t, "default", m.db)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{ set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@ -1673,7 +1673,7 @@ func TestRWScanRecovery(t *testing.T) {
}, },
}) })
defer cancel() defer cancel()
m := newModel(t, cfg, myID, "syncthing", "dev", nil) m := newModel(t, cfg, myID, nil)
set := newFileSet(t, "default", m.db) set := newFileSet(t, "default", m.db)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{ set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@ -2073,7 +2073,7 @@ func TestIssue4357(t *testing.T) {
// Create a separate wrapper not to pollute other tests. // Create a separate wrapper not to pollute other tests.
wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion}) wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion})
defer cancel() defer cancel()
m := newModel(t, wrapper, myID, "syncthing", "dev", nil) m := newModel(t, wrapper, myID, nil)
m.ServeBackground() m.ServeBackground()
defer cleanupModel(m) defer cleanupModel(m)
@ -2125,7 +2125,7 @@ func TestIssue4357(t *testing.T) {
} }
func TestIndexesForUnknownDevicesDropped(t *testing.T) { func TestIndexesForUnknownDevicesDropped(t *testing.T) {
m := newModel(t, defaultCfgWrapper, myID, "syncthing", "dev", nil) m := newModel(t, defaultCfgWrapper, myID, nil)
files := newFileSet(t, "default", m.db) files := newFileSet(t, "default", m.db)
files.Drop(device1) files.Drop(device1)
@ -2231,7 +2231,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
t.Error("device still in config") t.Error("device still in config")
} }
if _, ok := m.conn[device2]; ok { if _, ok := m.deviceConnIDs[device2]; ok {
t.Error("conn not missing") t.Error("conn not missing")
} }
@ -2374,7 +2374,7 @@ func TestCustomMarkerName(t *testing.T) {
ffs := fcfg.Filesystem(nil) ffs := fcfg.Filesystem(nil)
m := newModel(t, cfg, myID, "syncthing", "dev", nil) m := newModel(t, cfg, myID, nil)
set := newFileSet(t, "default", m.db) set := newFileSet(t, "default", m.db)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{ set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@ -2736,7 +2736,7 @@ func TestIssue4094(t *testing.T) {
// Create a separate wrapper not to pollute other tests. // Create a separate wrapper not to pollute other tests.
wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion}) wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion})
defer cancel() defer cancel()
m := newModel(t, wrapper, myID, "syncthing", "dev", nil) m := newModel(t, wrapper, myID, nil)
m.ServeBackground() m.ServeBackground()
defer cleanupModel(m) defer cleanupModel(m)
@ -2971,6 +2971,7 @@ func TestConnCloseOnRestart(t *testing.T) {
br := &testutil.BlockingRW{} br := &testutil.BlockingRW{}
nw := &testutil.NoopRW{} nw := &testutil.NoopRW{}
ci := &protocolmocks.ConnectionInfo{} ci := &protocolmocks.ConnectionInfo{}
ci.ConnectionIDReturns(srand.String(16))
m.AddConnection(protocol.NewConnection(device1, br, nw, testutil.NoopCloser{}, m, ci, protocol.CompressionNever, nil, m.keyGen), protocol.Hello{}) m.AddConnection(protocol.NewConnection(device1, br, nw, testutil.NoopCloser{}, m, ci, protocol.CompressionNever, nil, m.keyGen), protocol.Hello{})
m.pmut.RLock() m.pmut.RLock()
if len(m.closed) != 1 { if len(m.closed) != 1 {
@ -3639,6 +3640,7 @@ func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSec
}) })
m.AddConnection(fc1, protocol.Hello{}) m.AddConnection(fc1, protocol.Hello{})
m.AddConnection(fc2, protocol.Hello{}) m.AddConnection(fc2, protocol.Hello{})
m.promoteConnections()
// Initial CCs // Initial CCs
select { select {
@ -3690,7 +3692,7 @@ func TestIssue6961(t *testing.T) {
must(t, err) must(t, err)
waiter.Wait() waiter.Wait()
// Always recalc/repair when opening a fileset. // Always recalc/repair when opening a fileset.
m := newModel(t, wcfg, myID, "syncthing", "dev", nil) m := newModel(t, wcfg, myID, nil)
m.db.Close() m.db.Close()
m.db, err = db.NewLowlevel(backend.OpenMemory(), m.evLogger, db.WithRecheckInterval(time.Millisecond)) m.db, err = db.NewLowlevel(backend.OpenMemory(), m.evLogger, db.WithRecheckInterval(time.Millisecond))
if err != nil { if err != nil {
@ -3952,7 +3954,7 @@ func TestCCFolderNotRunning(t *testing.T) {
w, fcfg, wCancel := newDefaultCfgWrapper() w, fcfg, wCancel := newDefaultCfgWrapper()
defer wCancel() defer wCancel()
tfs := fcfg.Filesystem(nil) tfs := fcfg.Filesystem(nil)
m := newModel(t, w, myID, "syncthing", "dev", nil) m := newModel(t, w, myID, nil)
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
// A connection can happen before all the folders are started. // A connection can happen before all the folders are started.

View File

@ -1214,7 +1214,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
// Initialise db with an entry and then stop everything again // Initialise db with an entry and then stop everything again
must(t, tfs.Mkdir(dir1, 0o777)) must(t, tfs.Mkdir(dir1, 0o777))
m := newModel(t, w, myID, "syncthing", "dev", nil) m := newModel(t, w, myID, nil)
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
m.ServeBackground() m.ServeBackground()
m.ScanFolders() m.ScanFolders()
@ -1223,7 +1223,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
// Add connection (sends incoming cluster config) before starting the new model // Add connection (sends incoming cluster config) before starting the new model
m = &testModel{ m = &testModel{
model: NewModel(m.cfg, m.id, m.clientName, m.clientVersion, m.db, m.protectedFiles, m.evLogger, protocol.NewKeyGenerator()).(*model), model: NewModel(m.cfg, m.id, m.db, m.protectedFiles, m.evLogger, protocol.NewKeyGenerator()).(*model),
evCancel: m.evCancel, evCancel: m.evCancel,
stopped: make(chan struct{}), stopped: make(chan struct{}),
} }

View File

@ -39,7 +39,9 @@ func init() {
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR") device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY") device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
device1Conn.DeviceIDReturns(device1) device1Conn.DeviceIDReturns(device1)
device1Conn.ConnectionIDReturns(rand.String(16))
device2Conn.DeviceIDReturns(device2) device2Conn.DeviceIDReturns(device2)
device2Conn.ConnectionIDReturns(rand.String(16))
cfg := config.New(myID) cfg := config.New(myID)
cfg.Options.MinHomeDiskFree.Value = 0 // avoids unnecessary free space checks cfg.Options.MinHomeDiskFree.Value = 0 // avoids unnecessary free space checks
@ -127,7 +129,7 @@ func setupModelWithConnectionFromWrapper(t testing.TB, w config.Wrapper) (*testM
func setupModel(t testing.TB, w config.Wrapper) *testModel { func setupModel(t testing.TB, w config.Wrapper) *testModel {
t.Helper() t.Helper()
m := newModel(t, w, myID, "syncthing", "dev", nil) m := newModel(t, w, myID, nil)
m.ServeBackground() m.ServeBackground()
<-m.started <-m.started
@ -144,14 +146,14 @@ type testModel struct {
stopped chan struct{} stopped chan struct{}
} }
func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, protectedFiles []string) *testModel { func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, protectedFiles []string) *testModel {
t.Helper() t.Helper()
evLogger := events.NewLogger() evLogger := events.NewLogger()
ldb, err := db.NewLowlevel(backend.OpenMemory(), evLogger) ldb, err := db.NewLowlevel(backend.OpenMemory(), evLogger)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m := NewModel(cfg, id, clientName, clientVersion, ldb, protectedFiles, evLogger, protocol.NewKeyGenerator()).(*model) m := NewModel(cfg, id, ldb, protectedFiles, evLogger, protocol.NewKeyGenerator()).(*model)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
go evLogger.Serve(ctx) go evLogger.Serve(ctx)
return &testModel{ return &testModel{

View File

@ -211,9 +211,11 @@ func (FileDownloadProgressUpdateType) EnumDescriptor() ([]byte, []int) {
} }
type Hello struct { type Hello struct {
DeviceName string `protobuf:"bytes,1,opt,name=device_name,json=deviceName,proto3" json:"deviceName" xml:"deviceName"` DeviceName string `protobuf:"bytes,1,opt,name=device_name,json=deviceName,proto3" json:"deviceName" xml:"deviceName"`
ClientName string `protobuf:"bytes,2,opt,name=client_name,json=clientName,proto3" json:"clientName" xml:"clientName"` ClientName string `protobuf:"bytes,2,opt,name=client_name,json=clientName,proto3" json:"clientName" xml:"clientName"`
ClientVersion string `protobuf:"bytes,3,opt,name=client_version,json=clientVersion,proto3" json:"clientVersion" xml:"clientVersion"` ClientVersion string `protobuf:"bytes,3,opt,name=client_version,json=clientVersion,proto3" json:"clientVersion" xml:"clientVersion"`
NumConnections int `protobuf:"varint,4,opt,name=num_connections,json=numConnections,proto3,casttype=int" json:"numConnections" xml:"numConnections"`
Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp" xml:"timestamp"`
} }
func (m *Hello) Reset() { *m = Hello{} } func (m *Hello) Reset() { *m = Hello{} }
@ -288,7 +290,8 @@ func (m *Header) XXX_DiscardUnknown() {
var xxx_messageInfo_Header proto.InternalMessageInfo var xxx_messageInfo_Header proto.InternalMessageInfo
type ClusterConfig struct { type ClusterConfig struct {
Folders []Folder `protobuf:"bytes,1,rep,name=folders,proto3" json:"folders" xml:"folder"` Folders []Folder `protobuf:"bytes,1,rep,name=folders,proto3" json:"folders" xml:"folder"`
Secondary bool `protobuf:"varint,2,opt,name=secondary,proto3" json:"secondary" xml:"secondary"`
} }
func (m *ClusterConfig) Reset() { *m = ClusterConfig{} } func (m *ClusterConfig) Reset() { *m = ClusterConfig{} }
@ -1142,205 +1145,211 @@ func init() {
func init() { proto.RegisterFile("lib/protocol/bep.proto", fileDescriptor_311ef540e10d9705) } func init() { proto.RegisterFile("lib/protocol/bep.proto", fileDescriptor_311ef540e10d9705) }
var fileDescriptor_311ef540e10d9705 = []byte{ var fileDescriptor_311ef540e10d9705 = []byte{
// 3163 bytes of a gzipped FileDescriptorProto // 3251 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x5a, 0x4d, 0x6c, 0x1b, 0xc7, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x5a, 0x4b, 0x6c, 0x23, 0x47,
0xbd, 0x17, 0xc5, 0x0f, 0x51, 0x23, 0xc9, 0xa6, 0xc6, 0x5f, 0x0c, 0x6d, 0x6b, 0xf9, 0x26, 0xce, 0x7a, 0x16, 0x9f, 0xa2, 0x4a, 0x8f, 0xa1, 0x6a, 0x5e, 0x34, 0x67, 0xac, 0x66, 0x6a, 0x67, 0x13,
0x7b, 0x8a, 0xf2, 0x62, 0x27, 0xca, 0xc7, 0xcb, 0x8b, 0xf3, 0x1c, 0x88, 0x22, 0x25, 0x33, 0x96, 0x59, 0x9b, 0x1d, 0xaf, 0xb5, 0xde, 0x8d, 0x63, 0x3b, 0x36, 0xc4, 0x87, 0x34, 0x5c, 0x6b, 0x48,
0x49, 0x65, 0x28, 0xdb, 0xb1, 0xf1, 0x1e, 0x88, 0x15, 0x77, 0x44, 0x2d, 0x4c, 0xee, 0xf2, 0xed, 0xb9, 0xa8, 0x19, 0xaf, 0x07, 0x08, 0x88, 0x16, 0xbb, 0x44, 0x35, 0x86, 0xec, 0x66, 0xba, 0x9b,
0x52, 0x5f, 0x41, 0x2f, 0x6d, 0x80, 0x20, 0xd0, 0xa1, 0x28, 0x72, 0x2a, 0x8a, 0x0a, 0x0d, 0x7a, 0x7a, 0x2c, 0x72, 0x49, 0x16, 0x58, 0x2c, 0x74, 0x08, 0x82, 0x3d, 0x05, 0xc1, 0x0a, 0x59, 0xe4,
0xe9, 0xad, 0x40, 0x0f, 0xbd, 0xe4, 0xd4, 0xa3, 0x8f, 0x46, 0x80, 0x02, 0x45, 0x0f, 0x0b, 0xc4, 0x92, 0x5b, 0x80, 0x1c, 0x72, 0xd9, 0x53, 0x8e, 0x73, 0x1c, 0x2c, 0x10, 0x20, 0xc8, 0xa1, 0x01,
0xbe, 0xb4, 0xec, 0x8d, 0xc7, 0x9e, 0x8a, 0xf9, 0xcf, 0xec, 0xec, 0xac, 0x3e, 0x52, 0x39, 0x39, 0xcf, 0x5c, 0x12, 0xe6, 0xc6, 0x63, 0x0e, 0x41, 0x50, 0x7f, 0x55, 0x57, 0x57, 0xeb, 0xe1, 0x68,
0xf4, 0x64, 0xfe, 0x7f, 0xff, 0xdf, 0xff, 0xbf, 0xb3, 0xf3, 0xff, 0x9a, 0x59, 0x19, 0x5d, 0xec, 0xec, 0x43, 0x4e, 0xc3, 0xfa, 0xfe, 0xef, 0xff, 0xab, 0xba, 0xea, 0x7f, 0x55, 0x69, 0xd0, 0x9d,
0xd8, 0xeb, 0x37, 0x7a, 0x9e, 0xdb, 0x77, 0x5b, 0x6e, 0xe7, 0xc6, 0x3a, 0xeb, 0x5d, 0x07, 0x01, 0x81, 0xbd, 0xf7, 0xee, 0xc8, 0x73, 0x03, 0xb7, 0xe7, 0x0e, 0xde, 0xdd, 0x63, 0xa3, 0x87, 0x30,
0x67, 0x43, 0xac, 0x30, 0xce, 0x76, 0xfb, 0x02, 0x2c, 0xbc, 0xec, 0xb1, 0x9e, 0xeb, 0x0b, 0xfa, 0xc0, 0x85, 0x08, 0x2b, 0xcf, 0xb1, 0xe3, 0x40, 0x80, 0xe5, 0xef, 0x78, 0x6c, 0xe4, 0xfa, 0x82,
0xfa, 0xd6, 0xc6, 0x8d, 0xb6, 0xdb, 0x76, 0x41, 0x80, 0x5f, 0x82, 0x44, 0x9e, 0x25, 0x50, 0xfa, 0xbe, 0x37, 0xde, 0x7f, 0xb7, 0xef, 0xf6, 0x5d, 0x18, 0xc0, 0x2f, 0x41, 0x22, 0xff, 0x93, 0x46,
0x36, 0xeb, 0x74, 0x5c, 0xbc, 0x88, 0x26, 0x2c, 0xb6, 0x6d, 0xb7, 0x58, 0xd3, 0x31, 0xbb, 0x2c, 0xb9, 0x47, 0x6c, 0x30, 0x70, 0x71, 0x0d, 0xcd, 0x5b, 0xec, 0xd0, 0xee, 0xb1, 0xae, 0x63, 0x0e,
0x9f, 0x28, 0x26, 0x66, 0xc7, 0x4b, 0x64, 0x10, 0x18, 0x48, 0xc0, 0x35, 0xb3, 0xcb, 0x86, 0x81, 0x59, 0x29, 0x55, 0x49, 0xad, 0xce, 0x55, 0xc9, 0x24, 0x34, 0x90, 0x80, 0x5b, 0xe6, 0x90, 0x4d,
0x91, 0xdb, 0xed, 0x76, 0xde, 0x27, 0x11, 0x44, 0xa8, 0xa6, 0xe7, 0x4e, 0x5a, 0x1d, 0x9b, 0x39, 0x43, 0xa3, 0x78, 0x3c, 0x1c, 0x7c, 0x48, 0x62, 0x88, 0x50, 0x4d, 0xce, 0x8d, 0xf4, 0x06, 0x36,
0x7d, 0xe1, 0x64, 0x34, 0x72, 0x22, 0xe0, 0x98, 0x93, 0x08, 0x22, 0x54, 0xd3, 0xe3, 0x3a, 0x3a, 0x73, 0x02, 0x61, 0x24, 0x1d, 0x1b, 0x11, 0x70, 0xc2, 0x48, 0x0c, 0x11, 0xaa, 0xc9, 0x71, 0x1b,
0x23, 0x9d, 0x6c, 0x33, 0xcf, 0xb7, 0x5d, 0x27, 0x9f, 0x04, 0x3f, 0xb3, 0x83, 0xc0, 0x98, 0x12, 0x2d, 0x49, 0x23, 0x87, 0xcc, 0xf3, 0x6d, 0xd7, 0x29, 0x65, 0xc0, 0xce, 0xea, 0x24, 0x34, 0x16,
0x9a, 0xfb, 0x42, 0x31, 0x0c, 0x8c, 0x73, 0x9a, 0x2b, 0x89, 0x12, 0x1a, 0x67, 0x91, 0xdf, 0x25, 0x85, 0xe4, 0xa9, 0x10, 0x4c, 0x43, 0xe3, 0xa6, 0x66, 0x4a, 0xa2, 0x84, 0x26, 0x59, 0xf8, 0x19,
0x50, 0xe6, 0x36, 0x33, 0x2d, 0xe6, 0xe1, 0x05, 0x94, 0xea, 0xef, 0xf5, 0xc4, 0xeb, 0x9d, 0x99, 0xba, 0xe1, 0x8c, 0x87, 0xdd, 0x9e, 0xeb, 0x38, 0xac, 0x17, 0xd8, 0xae, 0xe3, 0x97, 0xb2, 0x95,
0xbf, 0x70, 0x3d, 0xdc, 0xb8, 0xeb, 0x77, 0x99, 0xef, 0x9b, 0x6d, 0xb6, 0xb6, 0xd7, 0x63, 0xa5, 0xd4, 0x6a, 0xae, 0xfa, 0xde, 0x24, 0x34, 0x96, 0x9c, 0xf1, 0xb0, 0x16, 0x4b, 0xa6, 0xa1, 0x71,
0x8b, 0x83, 0xc0, 0x00, 0xda, 0x30, 0x30, 0x10, 0xf8, 0xe7, 0x02, 0xa1, 0x80, 0x61, 0x0b, 0x4d, 0x0b, 0x4c, 0x26, 0x61, 0xf2, 0xdf, 0xa1, 0x91, 0xb1, 0x9d, 0x80, 0x9e, 0xa3, 0xe3, 0x4f, 0xd0,
0xb4, 0xdc, 0x6e, 0xcf, 0x63, 0x3e, 0xac, 0x6d, 0x14, 0x3c, 0x5d, 0x39, 0xe2, 0x69, 0x31, 0xe2, 0x5c, 0x60, 0x0f, 0x99, 0x1f, 0x98, 0xc3, 0x51, 0x29, 0x57, 0x49, 0xad, 0x66, 0xaa, 0x95, 0x49,
0x94, 0xae, 0x0d, 0x02, 0x43, 0x37, 0x1a, 0x06, 0xc6, 0xb4, 0x58, 0x77, 0x84, 0x11, 0xaa, 0x33, 0x68, 0xc4, 0xe0, 0x34, 0x34, 0x6e, 0x80, 0x41, 0x85, 0x10, 0x1a, 0x4b, 0xc9, 0x3f, 0xa5, 0x50,
0xc8, 0xff, 0xa2, 0xa9, 0xc5, 0xce, 0x96, 0xdf, 0x67, 0xde, 0xa2, 0xeb, 0x6c, 0xd8, 0x6d, 0x7c, 0xfe, 0x11, 0x33, 0x2d, 0xe6, 0xe1, 0x0d, 0x94, 0x0d, 0x4e, 0x46, 0x62, 0xeb, 0x97, 0xd6, 0x6f,
0x07, 0x8d, 0x6d, 0xb8, 0x1d, 0x8b, 0x79, 0x7e, 0x3e, 0x51, 0x4c, 0xce, 0x4e, 0xcc, 0xe7, 0xa2, 0x3f, 0x8c, 0x0e, 0xf5, 0xe1, 0x63, 0xe6, 0xfb, 0x66, 0x9f, 0xed, 0x9e, 0x8c, 0x58, 0xf5, 0xce,
0x47, 0x2e, 0x81, 0xa2, 0x64, 0x3c, 0x09, 0x8c, 0x91, 0x41, 0x60, 0x84, 0xc4, 0x61, 0x60, 0x4c, 0x24, 0x34, 0x80, 0x36, 0x0d, 0x0d, 0x24, 0xec, 0x9e, 0x8c, 0x18, 0xa1, 0x80, 0x61, 0x0b, 0xcd,
0xc2, 0x63, 0x84, 0x4c, 0x68, 0xa8, 0x20, 0x5f, 0xa7, 0x50, 0x46, 0x18, 0xe1, 0xeb, 0x68, 0xd4, 0xf7, 0xdc, 0xe1, 0xc8, 0x63, 0x3e, 0xec, 0x5b, 0x1a, 0x2c, 0xdd, 0xbf, 0x60, 0xa9, 0x16, 0x73,
0xb6, 0x64, 0xb8, 0x67, 0x9e, 0x05, 0xc6, 0x68, 0xb5, 0x3c, 0x08, 0x8c, 0x51, 0xdb, 0x1a, 0x06, 0xaa, 0x0f, 0x26, 0xa1, 0xa1, 0x2b, 0x4d, 0x43, 0x63, 0x59, 0xec, 0x69, 0x8c, 0x11, 0xaa, 0x33,
0x46, 0x16, 0xac, 0x6d, 0x8b, 0x7c, 0xf9, 0xf4, 0xda, 0x68, 0xb5, 0x4c, 0x47, 0x6d, 0x0b, 0x5f, 0xc8, 0xaf, 0x53, 0x68, 0xb1, 0x36, 0x18, 0xfb, 0x01, 0xf3, 0x6a, 0xae, 0xb3, 0x6f, 0xf7, 0xf1,
0x47, 0xe9, 0x8e, 0xb9, 0xce, 0x3a, 0x32, 0xb8, 0xf9, 0x41, 0x60, 0x08, 0x60, 0x18, 0x18, 0x13, 0x67, 0x68, 0x76, 0xdf, 0x1d, 0x58, 0xcc, 0xf3, 0x4b, 0xa9, 0x4a, 0x66, 0x75, 0x7e, 0xbd, 0x18,
0xc0, 0x07, 0x89, 0x50, 0x81, 0xe2, 0x9b, 0x68, 0xdc, 0x63, 0xa6, 0xd5, 0x74, 0x9d, 0xce, 0x1e, 0xcf, 0xb9, 0x09, 0x82, 0xaa, 0xf1, 0x22, 0x34, 0x66, 0x26, 0xa1, 0x11, 0x11, 0xa7, 0xa1, 0xb1,
0x04, 0x32, 0x5b, 0x9a, 0x19, 0x04, 0x46, 0x96, 0x83, 0x75, 0xa7, 0xb3, 0x37, 0x0c, 0x8c, 0x33, 0x00, 0xf3, 0x88, 0x31, 0xa1, 0x91, 0x80, 0x6f, 0xa9, 0xcf, 0x7a, 0xae, 0x63, 0x99, 0xde, 0x09,
0x60, 0x16, 0x02, 0x84, 0x2a, 0x1d, 0x6e, 0x22, 0x6c, 0xb7, 0x1d, 0xd7, 0x63, 0xcd, 0x1e, 0xf3, 0x7c, 0x42, 0x41, 0x6c, 0xa9, 0x02, 0xd5, 0x96, 0x2a, 0x84, 0xd0, 0x58, 0x4a, 0x7e, 0x9b, 0x45,
0xba, 0x36, 0x6c, 0x8d, 0x9f, 0x4f, 0x81, 0x97, 0x37, 0x06, 0x81, 0x31, 0x2d, 0xb4, 0xab, 0x91, 0x79, 0x31, 0x29, 0x7e, 0x88, 0xd2, 0xb6, 0x25, 0x7d, 0x79, 0xe5, 0x55, 0x68, 0xa4, 0x9b, 0xf5,
0x72, 0x18, 0x18, 0x97, 0xc4, 0xaa, 0x0f, 0x6b, 0x08, 0x3d, 0xca, 0xc6, 0x77, 0xd0, 0x94, 0x7c, 0x49, 0x68, 0xa4, 0x6d, 0x6b, 0x1a, 0x1a, 0x05, 0x30, 0x61, 0x5b, 0xe4, 0x57, 0x2f, 0x1f, 0xa4,
0x80, 0xc5, 0x3a, 0xac, 0xcf, 0xf2, 0x69, 0xf0, 0xfd, 0xef, 0x83, 0xc0, 0x98, 0x14, 0x8a, 0x32, 0x9b, 0x75, 0x9a, 0xb6, 0x2d, 0xfc, 0x10, 0xe5, 0x06, 0xe6, 0x1e, 0x1b, 0x48, 0xcf, 0x2d, 0x4d,
0xe0, 0xc3, 0xc0, 0xc0, 0x9a, 0x5b, 0x01, 0x12, 0x1a, 0xe3, 0x60, 0x0b, 0x9d, 0xb7, 0x6c, 0xdf, 0x42, 0x43, 0x00, 0xd3, 0xd0, 0x98, 0x07, 0x3e, 0x8c, 0x08, 0x15, 0x28, 0xfe, 0x08, 0xcd, 0x79,
0x5c, 0xef, 0xb0, 0x66, 0x9f, 0x75, 0x7b, 0x4d, 0xdb, 0xb1, 0xd8, 0x2e, 0xf3, 0xf3, 0x19, 0xf0, 0xcc, 0xb4, 0xba, 0xae, 0x33, 0x38, 0x01, 0x2f, 0x2d, 0x54, 0x57, 0x26, 0xa1, 0x51, 0xe0, 0x60,
0x39, 0x3f, 0x08, 0x0c, 0x2c, 0xf5, 0x6b, 0xac, 0xdb, 0xab, 0x0a, 0xed, 0x30, 0x30, 0xf2, 0xa2, 0xdb, 0x19, 0xf0, 0x95, 0x2e, 0x81, 0x5a, 0x04, 0x10, 0xaa, 0x64, 0xb8, 0x8b, 0xb0, 0xdd, 0x77,
0xa6, 0x8e, 0xa8, 0x08, 0x3d, 0x86, 0x8f, 0xe7, 0x51, 0xa6, 0x67, 0x6e, 0xf9, 0xcc, 0xca, 0x8f, 0x5c, 0x8f, 0x75, 0x47, 0xcc, 0x1b, 0xda, 0xb0, 0xb7, 0xc2, 0x33, 0x0b, 0xd5, 0x1f, 0x4c, 0x42,
0x81, 0xdf, 0xc2, 0x20, 0x30, 0x24, 0xa2, 0x02, 0x2e, 0x44, 0x42, 0x25, 0xce, 0x93, 0x47, 0x54, 0x63, 0x59, 0x48, 0x77, 0x62, 0xe1, 0x34, 0x34, 0xee, 0x8a, 0x55, 0x9f, 0x97, 0x10, 0x7a, 0x91,
0xa9, 0x9f, 0xcf, 0x1d, 0x4e, 0x9e, 0x32, 0x28, 0xa2, 0xe4, 0x91, 0x44, 0xe5, 0x4b, 0xc8, 0x84, 0x8d, 0x3f, 0x43, 0x8b, 0x72, 0x02, 0x8b, 0x0d, 0x58, 0xc0, 0xc0, 0x3f, 0x0b, 0xd5, 0xdf, 0x9f,
0x86, 0x0a, 0xf2, 0x87, 0x0c, 0xca, 0x08, 0x23, 0x5c, 0x52, 0xc9, 0x33, 0x59, 0x9a, 0xe7, 0x0e, 0x84, 0xc6, 0x82, 0x10, 0xd4, 0x01, 0x9f, 0x86, 0x06, 0xd6, 0xcc, 0x0a, 0x90, 0xd0, 0x04, 0x07,
0xfe, 0x1c, 0x18, 0x59, 0xa1, 0xab, 0x96, 0x4f, 0x4a, 0xa6, 0x2f, 0x9e, 0x5e, 0x4b, 0x68, 0x09, 0x5b, 0xe8, 0x96, 0x65, 0xfb, 0xe6, 0xde, 0x80, 0x75, 0x03, 0x36, 0x1c, 0x75, 0x6d, 0xc7, 0x62,
0x35, 0x87, 0x52, 0x5a, 0xb3, 0x80, 0xda, 0x73, 0x44, 0x9b, 0x10, 0xb5, 0xe7, 0x40, 0x83, 0x00, 0xc7, 0xcc, 0x2f, 0xe5, 0xc1, 0xe6, 0xfa, 0x24, 0x34, 0xb0, 0x94, 0xef, 0xb2, 0xe1, 0xa8, 0x29,
0x0c, 0x7f, 0x80, 0xc6, 0x4d, 0xcb, 0xe2, 0x35, 0xc2, 0xfc, 0x7c, 0xb2, 0x98, 0xe4, 0x39, 0x3b, 0xa4, 0xd3, 0xd0, 0x28, 0x89, 0x84, 0x71, 0x41, 0x44, 0xe8, 0x25, 0x7c, 0xbc, 0x8e, 0xf2, 0x23,
0x08, 0x8c, 0x08, 0x1c, 0x06, 0xc6, 0x14, 0x58, 0x49, 0x84, 0xd0, 0x48, 0x87, 0xff, 0x2f, 0x5e, 0x73, 0xec, 0x33, 0xab, 0x34, 0x0b, 0x76, 0xcb, 0x93, 0xd0, 0x90, 0x88, 0x72, 0x18, 0x31, 0x24,
0xb9, 0xa9, 0xc3, 0x3d, 0xe0, 0x87, 0x95, 0x2c, 0xcf, 0xf4, 0x16, 0xf3, 0x64, 0xeb, 0x4b, 0x8b, 0x54, 0xe2, 0xdc, 0xf9, 0x44, 0x0a, 0xf2, 0x4b, 0xc5, 0xf3, 0xce, 0x57, 0x07, 0x41, 0xec, 0x7c,
0x82, 0xe2, 0x99, 0xce, 0x41, 0xd9, 0xf8, 0x44, 0xa6, 0x87, 0x00, 0xa1, 0x4a, 0x87, 0x97, 0xd1, 0x92, 0xa8, 0x6c, 0x89, 0x31, 0xa1, 0x91, 0x80, 0xfc, 0x4b, 0x1e, 0xe5, 0x85, 0x12, 0xae, 0x2a,
0x64, 0xd7, 0xdc, 0x6d, 0xfa, 0xec, 0xff, 0xb7, 0x98, 0xd3, 0x62, 0x90, 0x33, 0x49, 0xb1, 0x8a, 0xe7, 0x59, 0xa8, 0xae, 0x73, 0x03, 0xff, 0x1e, 0x1a, 0x05, 0x21, 0x6b, 0xd6, 0xaf, 0x72, 0xa6,
0xae, 0xb9, 0xdb, 0x90, 0xb0, 0x5a, 0x85, 0x86, 0x11, 0xaa, 0x33, 0x70, 0x09, 0x21, 0xdb, 0xe9, 0x5f, 0xbe, 0x7c, 0x90, 0xd2, 0x1c, 0x6a, 0x0d, 0x65, 0xb5, 0x4c, 0x08, 0xc1, 0xeb, 0x88, 0x1c,
0x7b, 0xae, 0xb5, 0xd5, 0x62, 0x9e, 0x4c, 0x11, 0xe8, 0xc0, 0x11, 0xaa, 0x3a, 0x70, 0x04, 0x11, 0x28, 0x82, 0xd7, 0x81, 0xec, 0x07, 0x18, 0xfe, 0x18, 0xcd, 0x99, 0x96, 0xc5, 0x83, 0x8c, 0xf9,
0xaa, 0xe9, 0x71, 0x1b, 0x65, 0x21, 0x77, 0x9b, 0xb6, 0x95, 0xcf, 0x16, 0x13, 0xb3, 0xa9, 0xd2, 0xa5, 0x4c, 0x25, 0xc3, 0x7d, 0x96, 0xfb, 0xbd, 0x02, 0xa7, 0xa1, 0xb1, 0x08, 0x5a, 0x12, 0x21,
0x8a, 0x0c, 0xee, 0x18, 0x64, 0x21, 0xc4, 0x36, 0xfc, 0xc9, 0x73, 0x06, 0xd8, 0x55, 0x4b, 0xed, 0x34, 0x96, 0xe1, 0x3f, 0x4d, 0x86, 0x7e, 0xf6, 0x7c, 0x12, 0xf9, 0x76, 0x31, 0xcf, 0x3d, 0xbd,
0xbe, 0x94, 0x79, 0xdf, 0x08, 0x69, 0xbf, 0x88, 0x7e, 0xd2, 0x90, 0x8f, 0x7f, 0x84, 0x0a, 0xfe, 0xc7, 0x3c, 0x99, 0xd7, 0x73, 0x22, 0xa0, 0xb8, 0xa7, 0x73, 0x50, 0x66, 0x75, 0xe1, 0xe9, 0x11,
0x63, 0x9b, 0x57, 0x8a, 0x78, 0x76, 0xdf, 0x76, 0x9d, 0xa6, 0xc7, 0xba, 0xee, 0xb6, 0xd9, 0xf1, 0x40, 0xa8, 0x92, 0xe1, 0x2d, 0xb4, 0x30, 0x34, 0x8f, 0xbb, 0x3e, 0xfb, 0xb3, 0x31, 0x73, 0x7a,
0xf3, 0xe3, 0xb0, 0xf8, 0x5b, 0x83, 0xc0, 0xc8, 0x73, 0x56, 0x55, 0x23, 0x51, 0xc9, 0x19, 0x06, 0x0c, 0x7c, 0x26, 0x23, 0x56, 0x31, 0x34, 0x8f, 0x3b, 0x12, 0x56, 0xab, 0xd0, 0x30, 0x42, 0x75,
0xc6, 0x0c, 0x3c, 0xf1, 0x24, 0x02, 0xa1, 0x27, 0xda, 0xe2, 0x5d, 0xf4, 0x12, 0x73, 0x5a, 0xde, 0x06, 0xae, 0x22, 0x64, 0x3b, 0x81, 0xe7, 0x5a, 0xe3, 0x1e, 0xf3, 0xa4, 0x8b, 0x40, 0x79, 0x89,
0x5e, 0x0f, 0x1e, 0xdb, 0x33, 0x7d, 0x7f, 0xc7, 0xf5, 0xac, 0x66, 0xdf, 0x7d, 0xcc, 0x9c, 0x3c, 0x51, 0x55, 0x5e, 0x62, 0x88, 0x50, 0x4d, 0x8e, 0xfb, 0xa8, 0x00, 0xbe, 0xdb, 0xb5, 0xad, 0x52,
0x82, 0xa4, 0xfe, 0x60, 0x10, 0x18, 0x97, 0x22, 0xd2, 0xaa, 0xe4, 0xac, 0x71, 0xca, 0x30, 0x30, 0xa1, 0x92, 0x5a, 0xcd, 0x56, 0xb7, 0xe5, 0xe1, 0xce, 0x82, 0x17, 0xc2, 0xd9, 0x46, 0x3f, 0xb9,
0xae, 0xc2, 0xb3, 0x4f, 0xd0, 0x13, 0x7a, 0x92, 0x25, 0xf9, 0x49, 0x02, 0xa5, 0x61, 0x33, 0x78, 0xcf, 0x00, 0xbb, 0x69, 0xa9, 0xdd, 0x97, 0x63, 0x9e, 0x37, 0x22, 0xda, 0xdf, 0xc6, 0x3f, 0x69,
0x35, 0x8b, 0xa6, 0x2c, 0x5b, 0x30, 0x54, 0xb3, 0x40, 0x8e, 0xb4, 0x6f, 0x89, 0xe3, 0x0a, 0x4a, 0xc4, 0xc7, 0x7f, 0x8e, 0xca, 0xfe, 0x73, 0x9b, 0x47, 0x8a, 0x98, 0x9b, 0x17, 0x8c, 0xae, 0xc7,
0x6f, 0xd8, 0x1d, 0xe6, 0xe7, 0x47, 0xa1, 0x96, 0xb1, 0x36, 0x08, 0xec, 0x0e, 0xab, 0x3a, 0x1b, 0x86, 0xee, 0xa1, 0x39, 0xf0, 0x4b, 0x73, 0xb0, 0xf8, 0x4f, 0x26, 0xa1, 0x51, 0xe2, 0xac, 0xa6,
0x6e, 0xe9, 0xb2, 0xac, 0x66, 0x41, 0x54, 0xb5, 0xc4, 0x25, 0x42, 0x05, 0x48, 0xbe, 0x48, 0xa0, 0x46, 0xa2, 0x92, 0x33, 0x0d, 0x8d, 0x15, 0x91, 0xe7, 0xae, 0x20, 0x10, 0x7a, 0xa5, 0x2e, 0x3e,
0x09, 0x58, 0xc4, 0xbd, 0x9e, 0x65, 0xf6, 0xd9, 0xbf, 0x72, 0x29, 0x9f, 0x4f, 0xa1, 0x6c, 0x68, 0x46, 0x6f, 0x31, 0xa7, 0xe7, 0x9d, 0x8c, 0x60, 0xda, 0x91, 0xe9, 0xfb, 0x47, 0xae, 0x67, 0x75,
0xa0, 0x1a, 0x42, 0xe2, 0x14, 0x0d, 0x61, 0x0e, 0xa5, 0x7c, 0xfb, 0x53, 0x06, 0x83, 0x25, 0x29, 0x03, 0xf7, 0x39, 0x73, 0x4a, 0x08, 0x9c, 0xfa, 0xe3, 0x49, 0x68, 0xdc, 0x8d, 0x49, 0x3b, 0x92,
0xb8, 0x5c, 0x56, 0x5c, 0x2e, 0x10, 0x0a, 0x18, 0xfe, 0x10, 0xa1, 0xae, 0x6b, 0xd9, 0x1b, 0x36, 0xb3, 0xcb, 0x29, 0xd3, 0xd0, 0x78, 0x1b, 0xe6, 0xbe, 0x42, 0x4e, 0xe8, 0x55, 0x9a, 0xe4, 0x2f,
0xb3, 0x9a, 0x3e, 0x14, 0x68, 0xb2, 0x54, 0xe4, 0xdd, 0x23, 0x44, 0x1b, 0xc3, 0xc0, 0x38, 0x2b, 0x53, 0x28, 0x07, 0x9b, 0xc1, 0xa3, 0x59, 0x24, 0x75, 0x99, 0x82, 0x21, 0x9a, 0x05, 0x72, 0x21,
0xca, 0x2b, 0x44, 0x08, 0x8d, 0xb4, 0xbc, 0x7f, 0x28, 0x07, 0xeb, 0x7b, 0xf9, 0x49, 0xa8, 0x8c, 0xfd, 0x4b, 0x1c, 0x37, 0x50, 0x6e, 0xdf, 0x1e, 0x30, 0xbf, 0x94, 0x86, 0x58, 0xc6, 0x5a, 0x21,
0x0f, 0xc2, 0xca, 0x68, 0x6c, 0xba, 0x5e, 0x1f, 0xca, 0x41, 0x3d, 0xa6, 0xb4, 0xa7, 0x4a, 0x2d, 0xb1, 0x07, 0xac, 0xe9, 0xec, 0xbb, 0xd5, 0x7b, 0x32, 0x9a, 0x05, 0x51, 0xc5, 0x12, 0x1f, 0x11,
0x82, 0x08, 0xaf, 0x04, 0x49, 0xa6, 0x1a, 0x15, 0xaf, 0xa0, 0xb1, 0xf0, 0xc0, 0xc3, 0x33, 0x3f, 0x2a, 0x40, 0xf2, 0xcb, 0x14, 0x9a, 0x87, 0x45, 0x3c, 0x19, 0x59, 0x66, 0xc0, 0xfe, 0x3f, 0x97,
0xd6, 0xa4, 0xef, 0xb3, 0x56, 0xdf, 0xf5, 0x4a, 0xc5, 0xb0, 0x49, 0x6f, 0xab, 0x03, 0x90, 0x28, 0xf2, 0x8b, 0x45, 0x54, 0x88, 0x14, 0x54, 0x42, 0x48, 0x5d, 0x23, 0x21, 0xac, 0xa1, 0xac, 0x6f,
0xb8, 0xed, 0xf0, 0xe8, 0x13, 0x6a, 0xf0, 0xfb, 0x28, 0xab, 0x9a, 0x09, 0x82, 0x77, 0x85, 0x66, 0xff, 0x8c, 0x41, 0x61, 0xc9, 0x08, 0x2e, 0x1f, 0x2b, 0x2e, 0x1f, 0x10, 0x0a, 0x18, 0xfe, 0x14,
0xe4, 0x47, 0x9d, 0x44, 0x34, 0x23, 0x5f, 0xb5, 0x11, 0xa5, 0xc3, 0x1f, 0xa1, 0xcc, 0x7a, 0xc7, 0xa1, 0xa1, 0x6b, 0xd9, 0xfb, 0x36, 0xb3, 0xba, 0xbe, 0xde, 0x88, 0x44, 0x68, 0x47, 0x55, 0x4d,
0x6d, 0x3d, 0x0e, 0xa7, 0xc5, 0xb9, 0x68, 0x21, 0x25, 0x8e, 0x43, 0x5c, 0xaf, 0xca, 0xb5, 0x48, 0x85, 0x10, 0x1a, 0x4b, 0x79, 0xfe, 0x50, 0x06, 0xf6, 0x4e, 0x4a, 0x0b, 0x10, 0x19, 0x1f, 0x47,
0xaa, 0x1a, 0xff, 0x20, 0x12, 0x2a, 0x61, 0x7e, 0x9a, 0xf3, 0xf7, 0xba, 0x1d, 0xdb, 0x79, 0xdc, 0x91, 0xd1, 0x39, 0x70, 0xbd, 0x00, 0xc2, 0x41, 0x4d, 0x53, 0x3d, 0x51, 0xa1, 0x16, 0x43, 0x84,
0xec, 0x9b, 0x5e, 0x9b, 0xf5, 0xf3, 0xd3, 0xd1, 0x69, 0x4e, 0x6a, 0xd6, 0x40, 0xa1, 0x4e, 0x73, 0x47, 0x82, 0x24, 0x53, 0x8d, 0x8a, 0xb7, 0xd1, 0x6c, 0xd4, 0xcd, 0x71, 0xcf, 0x4f, 0x24, 0xe9,
0x31, 0x94, 0xd0, 0x38, 0x8b, 0x9f, 0x31, 0x85, 0xeb, 0xe6, 0xa6, 0xe9, 0x6f, 0xe6, 0x31, 0xd4, 0xa7, 0xac, 0x17, 0xb8, 0x5e, 0xb5, 0x12, 0x25, 0xe9, 0x43, 0xd5, 0xdd, 0x89, 0x80, 0x3b, 0x8c,
0x29, 0x74, 0x38, 0x01, 0xdf, 0x36, 0xfd, 0x4d, 0xb5, 0xed, 0x11, 0x44, 0xa8, 0xa6, 0xc7, 0xb7, 0xfa, 0xba, 0x48, 0x82, 0x3f, 0x44, 0x05, 0x95, 0x4c, 0x10, 0x7c, 0x2b, 0x24, 0x23, 0x3f, 0xce,
0xd0, 0xb8, 0xac, 0x4d, 0x66, 0xe5, 0xcf, 0x81, 0x0b, 0x48, 0x05, 0x05, 0xaa, 0x54, 0x50, 0x08, 0x24, 0x4b, 0xb2, 0x41, 0x88, 0xd2, 0x88, 0x92, 0xe1, 0x9f, 0xa0, 0xfc, 0xde, 0xc0, 0xed, 0x3d,
0xa1, 0x91, 0x16, 0x97, 0xe4, 0x39, 0x52, 0x9c, 0xfe, 0x2e, 0x1e, 0x4d, 0xfb, 0x53, 0x1c, 0x24, 0x8f, 0xaa, 0xc5, 0xcd, 0x78, 0x21, 0x55, 0x8e, 0xc3, 0xb9, 0xbe, 0x2d, 0xd7, 0x22, 0xa9, 0xaa,
0x97, 0xd0, 0xc4, 0xe1, 0x53, 0xcd, 0x94, 0xe8, 0xf8, 0xbd, 0xd8, 0x79, 0x46, 0x74, 0xfc, 0x9e, 0xfc, 0xc3, 0x90, 0x50, 0x09, 0xf3, 0x56, 0xd5, 0x3f, 0x19, 0x0e, 0x6c, 0xe7, 0x79, 0x37, 0x30,
0x7e, 0x92, 0xd1, 0x19, 0xf8, 0x23, 0x2d, 0x2d, 0x1d, 0x3f, 0x3f, 0x51, 0x4c, 0xcc, 0xa6, 0x4b, 0xbd, 0x3e, 0x0b, 0x4a, 0xcb, 0x71, 0xab, 0x2a, 0x25, 0xbb, 0x20, 0x50, 0xad, 0x6a, 0x02, 0x25,
0xaf, 0xea, 0x79, 0x58, 0xf3, 0x8f, 0xe4, 0x61, 0xcd, 0x27, 0x7f, 0x0f, 0x8c, 0xa4, 0xed, 0xf4, 0x34, 0xc9, 0xe2, 0x0d, 0xb4, 0x30, 0xdd, 0x3d, 0x30, 0xfd, 0x83, 0x12, 0x86, 0x38, 0x85, 0x0c,
0xa9, 0x46, 0xc3, 0x1b, 0x48, 0xec, 0x52, 0x13, 0xaa, 0x6a, 0x0a, 0x5c, 0x2d, 0x3f, 0x0b, 0x8c, 0x27, 0xe0, 0x47, 0xa6, 0x7f, 0xa0, 0xb6, 0x3d, 0x86, 0x08, 0xd5, 0xe4, 0xbc, 0x81, 0x92, 0xb1,
0x49, 0x6a, 0xee, 0x40, 0xe8, 0x1b, 0xf6, 0xa7, 0x8c, 0x6f, 0xd4, 0x7a, 0x28, 0xa8, 0x8d, 0x52, 0xc9, 0xac, 0xd2, 0x4d, 0x30, 0x01, 0xae, 0xa0, 0x40, 0xe5, 0x0a, 0x0a, 0x21, 0x34, 0x96, 0xe2,
0x48, 0xe8, 0xf8, 0xcb, 0xa7, 0xd7, 0x62, 0x66, 0x34, 0x32, 0xc2, 0xf7, 0x51, 0xb6, 0xd7, 0x31, 0xaa, 0x6c, 0x44, 0x45, 0xfb, 0x78, 0xe7, 0xa2, 0xdb, 0x5f, 0xa3, 0x13, 0xdd, 0x44, 0xf3, 0xe7,
0xfb, 0x1b, 0xae, 0xd7, 0xcd, 0x9f, 0x81, 0x64, 0xd7, 0xf6, 0x70, 0x55, 0x6a, 0xca, 0x66, 0xdf, 0xbb, 0x9a, 0x45, 0x91, 0xf1, 0x47, 0x89, 0x7e, 0x46, 0x64, 0xfc, 0x91, 0xde, 0xc9, 0xe8, 0x0c,
0x2c, 0x11, 0x99, 0x66, 0x8a, 0xaf, 0x32, 0x37, 0x04, 0x08, 0x55, 0x3a, 0x5c, 0x46, 0x13, 0x1d, 0xfc, 0x13, 0xcd, 0x2d, 0x1d, 0xbf, 0x34, 0x0f, 0x7d, 0xfb, 0x3b, 0xba, 0x1f, 0xb6, 0xfc, 0x0b,
0xb7, 0x65, 0x76, 0x9a, 0x1b, 0x1d, 0xb3, 0xed, 0xe7, 0xff, 0x32, 0x06, 0x9b, 0x0a, 0xd9, 0x01, 0x7e, 0xd8, 0x8a, 0xfb, 0x75, 0x8d, 0x86, 0xf7, 0x91, 0xd8, 0xa5, 0x2e, 0x44, 0xd5, 0x22, 0x98,
0xf8, 0x12, 0x87, 0xd5, 0x66, 0x44, 0x10, 0xa1, 0x9a, 0x1e, 0xdf, 0x46, 0x93, 0xb2, 0x8c, 0x44, 0xda, 0x7a, 0x15, 0x1a, 0x0b, 0xd4, 0x3c, 0x82, 0xa3, 0xef, 0xd8, 0x3f, 0x63, 0x7c, 0xa3, 0xf6,
0x8e, 0xfd, 0x75, 0x0c, 0x32, 0x04, 0x62, 0x23, 0x15, 0x32, 0xcb, 0xa6, 0xf5, 0xea, 0x13, 0x69, 0xa2, 0x81, 0xda, 0x28, 0x85, 0x44, 0x86, 0x7f, 0xf5, 0xf2, 0x41, 0x42, 0x8d, 0xc6, 0x4a, 0xf8,
0xa6, 0x33, 0xf0, 0xc7, 0xe8, 0xac, 0xed, 0xb8, 0x16, 0x6b, 0xb6, 0x36, 0x4d, 0xa7, 0xcd, 0x78, 0x29, 0x2a, 0x8c, 0x06, 0x66, 0xb0, 0xef, 0x7a, 0xc3, 0xd2, 0x12, 0x38, 0xbb, 0xb6, 0x87, 0x3b,
0x7c, 0x06, 0x63, 0x50, 0x8d, 0x90, 0xff, 0xa0, 0x5b, 0x04, 0x15, 0xc4, 0xe8, 0x9c, 0x9c, 0x9e, 0x52, 0x52, 0x37, 0x03, 0xb3, 0x4a, 0xa4, 0x9b, 0x29, 0xbe, 0xf2, 0xdc, 0x08, 0x20, 0x54, 0xc9,
0x1a, 0x4a, 0x68, 0x9c, 0x85, 0x77, 0x91, 0x36, 0x56, 0x9a, 0x7d, 0xcf, 0xb4, 0x3b, 0xcc, 0x13, 0x70, 0x1d, 0xcd, 0x0f, 0xdc, 0x9e, 0x39, 0xe8, 0xee, 0x0f, 0xcc, 0xbe, 0x5f, 0xfa, 0x8f, 0x59,
0xf1, 0xfa, 0xdb, 0x18, 0x04, 0xec, 0xc3, 0x41, 0x60, 0x5c, 0x88, 0x38, 0x6b, 0x82, 0x22, 0x83, 0xd8, 0x54, 0xf0, 0x0e, 0xc0, 0x37, 0x39, 0xac, 0x36, 0x23, 0x86, 0x08, 0xd5, 0xe4, 0xf8, 0x11,
0x75, 0xf9, 0xd0, 0xc8, 0xd2, 0xb4, 0x2a, 0x23, 0x8e, 0x37, 0xc6, 0xef, 0xf2, 0x53, 0x24, 0x3f, 0x5a, 0x90, 0x61, 0x24, 0x7c, 0xec, 0x3f, 0x67, 0xc1, 0x43, 0xe0, 0x6c, 0xa4, 0x40, 0x7a, 0xd9,
0xe9, 0x5a, 0xf2, 0x48, 0x7b, 0x45, 0x9c, 0x17, 0x01, 0x52, 0xad, 0x48, 0xca, 0x70, 0x60, 0x84, 0xb2, 0x1e, 0x7d, 0xc2, 0xcd, 0x74, 0x06, 0xfe, 0x1c, 0xdd, 0xb0, 0x1d, 0xd7, 0x62, 0xdd, 0xde,
0x5f, 0x98, 0xa2, 0x31, 0xdb, 0xd9, 0x36, 0x3b, 0x76, 0x78, 0x64, 0x7d, 0xef, 0x59, 0x60, 0x20, 0x81, 0xe9, 0xf4, 0x19, 0x3f, 0x9f, 0xc9, 0x2c, 0x44, 0x23, 0xf8, 0x3f, 0xc8, 0x6a, 0x20, 0x82,
0x6a, 0xee, 0x54, 0x05, 0x2a, 0x4e, 0x10, 0xf0, 0x53, 0x3b, 0x41, 0x80, 0xcc, 0x4f, 0x10, 0x1a, 0x33, 0xba, 0x29, 0xab, 0xa7, 0x86, 0x12, 0x9a, 0x64, 0xe1, 0x63, 0xa4, 0x95, 0x95, 0x6e, 0xe0,
0x93, 0x86, 0x3c, 0xde, 0x56, 0x1c, 0x37, 0x76, 0x2b, 0xc8, 0x82, 0x6b, 0xd8, 0x56, 0xc7, 0x8d, 0x99, 0xf6, 0x80, 0x79, 0xe2, 0xbc, 0xfe, 0x6b, 0x16, 0x0e, 0xec, 0xd3, 0x49, 0x68, 0xdc, 0x8e,
0xdf, 0x08, 0xc4, 0xb6, 0xc6, 0x50, 0x42, 0xe3, 0xac, 0xf7, 0x53, 0x3f, 0xff, 0xca, 0x18, 0x21, 0x39, 0xbb, 0x82, 0x22, 0x0f, 0xeb, 0xde, 0xb9, 0x92, 0xa5, 0x49, 0x95, 0x47, 0x5c, 0xae, 0x8c,
0xdf, 0x26, 0xd0, 0xb8, 0x6a, 0x71, 0x7c, 0xba, 0x40, 0xfc, 0x93, 0x10, 0x7e, 0xa8, 0xe6, 0x4d, 0x7f, 0xcc, 0xbb, 0x48, 0xde, 0xe9, 0x5a, 0xb2, 0xa5, 0xbd, 0x2f, 0xfa, 0x45, 0x80, 0x54, 0x2a,
0x11, 0x77, 0x51, 0xcd, 0x9b, 0x10, 0x70, 0xc0, 0xf8, 0xf4, 0x74, 0x37, 0x36, 0x7c, 0xd6, 0x87, 0x92, 0x63, 0x68, 0x18, 0xe1, 0x17, 0xa6, 0x68, 0xd6, 0x76, 0x0e, 0xcd, 0x81, 0x1d, 0xb5, 0xac,
0xb9, 0x95, 0x14, 0xd3, 0x53, 0x20, 0x6a, 0x7a, 0x0a, 0x91, 0x50, 0x89, 0xe3, 0x37, 0xe5, 0xf4, 0x1f, 0xbc, 0x0a, 0x0d, 0x44, 0xcd, 0xa3, 0xa6, 0x40, 0x45, 0x07, 0x01, 0x3f, 0xb5, 0x0e, 0x02,
0x1a, 0x85, 0xb0, 0x5d, 0x3d, 0x7e, 0x7a, 0x85, 0x41, 0x11, 0x43, 0xec, 0x26, 0x1a, 0xdf, 0x61, 0xc6, 0xbc, 0x83, 0xd0, 0x98, 0x34, 0xe2, 0xf1, 0xb4, 0xe2, 0xb8, 0x89, 0x5b, 0x41, 0x01, 0x4c,
0xe6, 0x63, 0x91, 0x97, 0xa2, 0x65, 0x40, 0x5f, 0xe7, 0xa0, 0xcc, 0x49, 0x51, 0x1d, 0x21, 0x40, 0xc3, 0xb6, 0x3a, 0x6e, 0xf2, 0x46, 0x20, 0xb6, 0x35, 0x81, 0x12, 0x9a, 0x64, 0x7d, 0x98, 0xfd,
0xa8, 0xd2, 0xc9, 0x77, 0x7c, 0x84, 0x32, 0x62, 0x9c, 0xe0, 0x55, 0x94, 0x6d, 0xb9, 0x5b, 0x4e, 0x9b, 0xdf, 0x18, 0x33, 0xe4, 0xab, 0x14, 0x9a, 0x53, 0x29, 0x8e, 0x57, 0x17, 0x38, 0xff, 0x0c,
0x3f, 0xba, 0x54, 0x4e, 0xeb, 0xa7, 0x61, 0xd0, 0x94, 0xfe, 0x2d, 0x2c, 0xc0, 0x90, 0xaa, 0x62, 0x1c, 0x3f, 0x44, 0xf3, 0x81, 0x38, 0x77, 0x11, 0xcd, 0x07, 0x70, 0xe0, 0x80, 0xf1, 0xea, 0xe9,
0x24, 0x01, 0x7e, 0x8c, 0x95, 0x2a, 0xf2, 0x59, 0x02, 0x8d, 0x49, 0x43, 0x7c, 0x5b, 0x5d, 0x0e, 0xee, 0xef, 0xfb, 0x2c, 0x80, 0xba, 0x95, 0x11, 0xd5, 0x53, 0x20, 0xaa, 0x7a, 0x8a, 0x21, 0xa1,
0x52, 0xa5, 0xf7, 0x0e, 0x4d, 0xc9, 0xef, 0xbe, 0x68, 0xea, 0x13, 0x52, 0xde, 0x39, 0xb7, 0xcd, 0x12, 0xc7, 0xef, 0xc9, 0xea, 0x95, 0x86, 0x63, 0x7b, 0xfb, 0xf2, 0xea, 0x15, 0x1d, 0x8a, 0x28,
0xce, 0x96, 0xd8, 0xa8, 0x94, 0xb8, 0x73, 0x02, 0xa0, 0x86, 0x0e, 0x48, 0x84, 0x0a, 0x94, 0x7c, 0x62, 0x1f, 0xa1, 0xb9, 0x23, 0x66, 0x3e, 0x17, 0x7e, 0x29, 0x52, 0x06, 0xe4, 0x75, 0x0e, 0x4a,
0x96, 0x42, 0x93, 0x7a, 0x13, 0xe1, 0xed, 0x7a, 0xcb, 0xb1, 0x77, 0x61, 0x31, 0xb1, 0x53, 0xca, 0x9f, 0x14, 0xd1, 0x11, 0x01, 0x84, 0x2a, 0x99, 0xfc, 0xc6, 0x67, 0x28, 0x2f, 0xca, 0x09, 0xde,
0x3d, 0xc7, 0xde, 0x85, 0x36, 0x53, 0x78, 0x12, 0x18, 0x09, 0x1e, 0x00, 0xce, 0x53, 0x01, 0xe0, 0x41, 0x85, 0x9e, 0x3b, 0x76, 0x82, 0xf8, 0x52, 0xba, 0xac, 0x77, 0xc3, 0x20, 0xa9, 0xfe, 0x5e,
0x02, 0xa1, 0x80, 0xe1, 0x8f, 0xd1, 0xd8, 0x8e, 0xed, 0x58, 0xee, 0x8e, 0x0f, 0xcb, 0x98, 0xd0, 0x14, 0x80, 0x11, 0x55, 0x9d, 0x91, 0x04, 0x78, 0x1b, 0x2b, 0x45, 0xe4, 0xe7, 0x29, 0x34, 0x2b,
0x6f, 0x0e, 0x0f, 0x84, 0x02, 0x3c, 0x15, 0xa5, 0xa7, 0x90, 0xad, 0xb6, 0x4b, 0xca, 0x84, 0x86, 0x15, 0xf1, 0x23, 0x75, 0x39, 0xc8, 0x56, 0x3f, 0x38, 0x57, 0x25, 0xbf, 0xfe, 0xa2, 0xa9, 0x57,
0x1a, 0xbc, 0x8c, 0xd2, 0x1d, 0xdb, 0xd9, 0xda, 0x85, 0x04, 0x8b, 0x8d, 0xd9, 0x4f, 0xcc, 0x7e, 0x48, 0x79, 0xe7, 0x3c, 0x34, 0x07, 0x63, 0xb1, 0x51, 0x59, 0x71, 0xe7, 0x04, 0x40, 0x15, 0x1d,
0xdf, 0x03, 0x77, 0x57, 0xa4, 0x3b, 0xc1, 0x8c, 0x2e, 0xd9, 0x5c, 0xe2, 0x97, 0x6c, 0xfe, 0x2f, 0x18, 0x11, 0x2a, 0x50, 0xf2, 0xf3, 0x2c, 0x5a, 0xd0, 0x93, 0x08, 0x4f, 0xd7, 0x63, 0xc7, 0x3e,
0xbe, 0x83, 0x32, 0x96, 0xe9, 0xed, 0xd8, 0xe2, 0x52, 0x73, 0x82, 0xa7, 0x19, 0xe9, 0x49, 0x52, 0x86, 0xc5, 0x24, 0xba, 0x94, 0x27, 0x8e, 0x7d, 0x0c, 0x69, 0xa6, 0xfc, 0x22, 0x34, 0x52, 0xfc,
0xa3, 0x0b, 0x1e, 0x88, 0x84, 0x4a, 0x1c, 0x33, 0x34, 0xb6, 0xe1, 0x31, 0xb6, 0xee, 0x5b, 0x70, 0x00, 0x38, 0x4f, 0x1d, 0x00, 0x1f, 0x10, 0x0a, 0x18, 0xfe, 0x1c, 0xcd, 0x1e, 0xd9, 0x8e, 0xe5,
0x48, 0x3a, 0xc1, 0xdb, 0xbb, 0xdc, 0x1b, 0xbf, 0x06, 0x2c, 0x79, 0x8c, 0x95, 0x1a, 0x70, 0x0d, 0x1e, 0xf9, 0xb0, 0x8c, 0x79, 0xfd, 0xe6, 0xf0, 0x85, 0x10, 0x80, 0xa5, 0x8a, 0xb4, 0x14, 0xb1,
0x90, 0x66, 0xea, 0x8d, 0xa5, 0x0c, 0xd7, 0x00, 0x49, 0xa3, 0x21, 0x09, 0x37, 0x51, 0xc6, 0x61, 0xd5, 0x76, 0xc9, 0x31, 0xa1, 0x91, 0x04, 0x6f, 0xa1, 0xdc, 0xc0, 0x76, 0xc6, 0xc7, 0xe0, 0x60,
0x7d, 0xfe, 0x94, 0xcc, 0xc9, 0x4f, 0x99, 0x97, 0x4f, 0xc9, 0xd4, 0x58, 0x5f, 0x3c, 0x44, 0x1a, 0x89, 0x32, 0xfb, 0x53, 0x33, 0x08, 0x3c, 0x30, 0x77, 0x5f, 0x9a, 0x13, 0xcc, 0xf8, 0x92, 0xcd,
0xa9, 0xd5, 0x0b, 0x91, 0x3f, 0x42, 0x72, 0xa8, 0x64, 0x90, 0xcf, 0x47, 0x51, 0x36, 0x8c, 0x2f, 0x47, 0xfc, 0x92, 0xcd, 0xff, 0xc5, 0x9f, 0xa1, 0xbc, 0x65, 0x7a, 0x47, 0xb6, 0xb8, 0xd4, 0x5c,
0x3f, 0xfc, 0xb9, 0x3b, 0x0e, 0xf3, 0xf4, 0xaf, 0x5b, 0x30, 0xf1, 0x01, 0x95, 0xd7, 0x33, 0x31, 0x61, 0x69, 0x45, 0x5a, 0x92, 0xd4, 0xf8, 0x82, 0x07, 0x43, 0x42, 0x25, 0x8e, 0x19, 0x9a, 0xdd,
0xc8, 0x14, 0x42, 0x68, 0xa4, 0xe5, 0x0e, 0xda, 0x9e, 0xbb, 0xd5, 0xd3, 0xbf, 0x6c, 0x81, 0x03, 0xf7, 0x18, 0xdb, 0xf3, 0x2d, 0x68, 0x92, 0xae, 0xb0, 0xf6, 0x63, 0x6e, 0x8d, 0x5f, 0x03, 0x36,
0x40, 0x63, 0x0e, 0x14, 0x42, 0x68, 0xa4, 0xc5, 0x37, 0x51, 0x72, 0xcb, 0xb6, 0x20, 0xd4, 0xe9, 0x3d, 0xc6, 0xaa, 0x1d, 0xb8, 0x06, 0x48, 0x35, 0xf5, 0xc5, 0x72, 0x0c, 0xd7, 0x00, 0x49, 0xa3,
0xd2, 0xab, 0xcf, 0x02, 0x23, 0x79, 0x0f, 0x2a, 0x80, 0xa3, 0xc3, 0xc0, 0x18, 0x17, 0x09, 0x67, 0x11, 0x09, 0x77, 0x51, 0xde, 0x61, 0x01, 0x9f, 0x25, 0x7f, 0xf5, 0x2c, 0xeb, 0x72, 0x96, 0x7c,
0x5b, 0xda, 0xf8, 0xe4, 0x0c, 0xca, 0xf5, 0xdc, 0xb8, 0x6d, 0x5b, 0x10, 0x5d, 0x69, 0xbc, 0x2c, 0x8b, 0x05, 0x62, 0x12, 0xa9, 0xa4, 0x56, 0x2f, 0x86, 0x7c, 0x0a, 0xc9, 0xa1, 0x92, 0x41, 0x7e,
0x8c, 0xdb, 0x9a, 0x71, 0x3b, 0x6e, 0xbc, 0xcc, 0x8d, 0x39, 0xf6, 0xcb, 0x04, 0x9a, 0xd0, 0x32, 0x91, 0x46, 0x85, 0xe8, 0x7c, 0x79, 0xf3, 0xe7, 0x1e, 0x39, 0xcc, 0xd3, 0x9f, 0xee, 0xa0, 0xe2,
0xf4, 0x87, 0xef, 0xc5, 0x0a, 0x3a, 0x23, 0x1c, 0xd8, 0x7e, 0x13, 0x5e, 0x10, 0xf6, 0x43, 0x7e, 0x03, 0x2a, 0xaf, 0x67, 0xa2, 0x90, 0x29, 0x84, 0xd0, 0x58, 0xca, 0x0d, 0xf4, 0x3d, 0x77, 0x3c,
0x36, 0x01, 0x4d, 0xd5, 0x5f, 0xe6, 0xb8, 0xfa, 0x6c, 0xa2, 0x83, 0x84, 0xc6, 0x38, 0xa4, 0x81, 0xd2, 0x9f, 0xed, 0xc0, 0x00, 0xa0, 0x09, 0x03, 0x0a, 0x21, 0x34, 0x96, 0xe2, 0x8f, 0x50, 0x66,
0xc6, 0x55, 0xc0, 0xf1, 0x12, 0xca, 0xec, 0x72, 0x21, 0x6c, 0x48, 0x67, 0x0f, 0x65, 0x45, 0x74, 0x6c, 0x5b, 0x70, 0xd4, 0xb9, 0xea, 0x3b, 0xaf, 0x42, 0x23, 0xf3, 0x04, 0x22, 0x80, 0xa3, 0xd3,
0xec, 0x14, 0x34, 0x55, 0x10, 0x20, 0x12, 0x2a, 0x61, 0xd2, 0x42, 0x69, 0xe0, 0xbf, 0xd0, 0x6d, 0xd0, 0x98, 0x13, 0x0e, 0x67, 0x5b, 0x5a, 0xf9, 0xe4, 0x0c, 0xca, 0xe5, 0x5c, 0xb9, 0x6f, 0x5b,
0x22, 0xd6, 0x67, 0x26, 0xff, 0x79, 0x9f, 0xf9, 0x71, 0x0a, 0x8d, 0x51, 0x7e, 0x68, 0xf6, 0xfb, 0xf2, 0x4d, 0x0e, 0x94, 0xb7, 0x84, 0x72, 0x5f, 0x53, 0xee, 0x27, 0x95, 0xb7, 0xb8, 0x32, 0xc7,
0xf8, 0x1d, 0xd5, 0xed, 0xd2, 0xa5, 0x57, 0x4e, 0x6a, 0x6f, 0x51, 0x74, 0xc2, 0xaf, 0x1f, 0xd1, 0x7e, 0x9d, 0x42, 0xf3, 0x9a, 0x87, 0x7e, 0xfb, 0xbd, 0xd8, 0x46, 0x4b, 0xc2, 0x80, 0xed, 0x77,
0xa5, 0x6b, 0xf4, 0xd4, 0x97, 0xae, 0xf0, 0x95, 0x92, 0xa7, 0x78, 0xa5, 0x68, 0x2c, 0xa5, 0x5e, 0xe1, 0x03, 0xe5, 0x1b, 0x14, 0x3c, 0x9b, 0x80, 0xa4, 0xe9, 0x6f, 0x71, 0x5c, 0x3d, 0x9b, 0xe8,
0x78, 0x2c, 0xa5, 0x4f, 0x3f, 0x96, 0xc2, 0x49, 0x99, 0x39, 0xc5, 0xa4, 0xac, 0xa3, 0x33, 0x1b, 0x20, 0xa1, 0x09, 0x0e, 0xe9, 0xa0, 0x39, 0x75, 0xe0, 0x78, 0x13, 0xe5, 0x8f, 0xf9, 0x20, 0x4a,
0x9e, 0xdb, 0x85, 0x6f, 0x64, 0xae, 0x67, 0x7a, 0x7b, 0xf2, 0x54, 0x00, 0xa3, 0x9b, 0x6b, 0xd6, 0x48, 0x37, 0xce, 0x79, 0x45, 0xdc, 0x76, 0x0a, 0x9a, 0x0a, 0x08, 0x18, 0x12, 0x2a, 0x61, 0xd2,
0x42, 0x85, 0x1a, 0xdd, 0x31, 0x94, 0xd0, 0x38, 0x2b, 0x3e, 0x13, 0xb3, 0x2f, 0x36, 0x13, 0xf1, 0x43, 0x39, 0xe0, 0xbf, 0xd1, 0x6d, 0x22, 0x91, 0x67, 0x16, 0xfe, 0xef, 0x3c, 0xf3, 0x17, 0x59,
0x2d, 0x94, 0x15, 0x27, 0x5e, 0xc7, 0x85, 0x6b, 0x57, 0xba, 0xf4, 0x32, 0x6f, 0x65, 0x80, 0xd5, 0x34, 0x4b, 0x79, 0xd3, 0xec, 0x07, 0xf8, 0x47, 0x2a, 0xdb, 0xe5, 0xaa, 0xdf, 0xbd, 0x2a, 0xbd,
0x5c, 0xd5, 0xca, 0xa4, 0xac, 0x5e, 0x3b, 0x24, 0x90, 0xdf, 0x26, 0x50, 0x96, 0x32, 0xbf, 0xe7, 0xc5, 0xa7, 0x13, 0xbd, 0x7e, 0xc4, 0x97, 0xae, 0xf4, 0xb5, 0x2f, 0x5d, 0xd1, 0x27, 0x65, 0xae,
0x3a, 0x3e, 0xfb, 0xbe, 0x49, 0x30, 0x87, 0x52, 0x96, 0xd9, 0x37, 0x65, 0xda, 0xc1, 0xee, 0x71, 0xf1, 0x49, 0x71, 0x59, 0xca, 0xbe, 0x71, 0x59, 0xca, 0x5d, 0xbf, 0x2c, 0x45, 0x95, 0x32, 0x7f,
0x59, 0xed, 0x1e, 0x17, 0x08, 0x05, 0x0c, 0x7f, 0x88, 0x52, 0x2d, 0xd7, 0x12, 0xc1, 0x3f, 0xa3, 0x8d, 0x4a, 0xd9, 0x46, 0x4b, 0xfb, 0x9e, 0x3b, 0x84, 0x37, 0x32, 0xd7, 0x33, 0xbd, 0x13, 0xd9,
0x37, 0xcd, 0x8a, 0xe7, 0xb9, 0xde, 0xa2, 0x6b, 0xc9, 0x6b, 0x07, 0x27, 0x29, 0x07, 0x5c, 0x20, 0x15, 0x40, 0xe9, 0xe6, 0x92, 0xdd, 0x48, 0xa0, 0x4a, 0x77, 0x02, 0x25, 0x34, 0xc9, 0x4a, 0xd6,
0x14, 0x30, 0xf2, 0x9b, 0x04, 0xca, 0x95, 0xdd, 0x1d, 0xa7, 0xe3, 0x9a, 0xd6, 0xaa, 0xe7, 0xb6, 0xc4, 0xc2, 0x9b, 0xd5, 0x44, 0xfc, 0x09, 0x2a, 0x88, 0x8e, 0xd7, 0x71, 0xe1, 0xda, 0x95, 0xab,
0x3d, 0xe6, 0xfb, 0xdf, 0xeb, 0xee, 0xdf, 0x44, 0x63, 0x5b, 0xf0, 0xe5, 0x20, 0xbc, 0xfd, 0x5f, 0x7e, 0x87, 0xa7, 0x32, 0xc0, 0x5a, 0xae, 0x4a, 0x65, 0x72, 0xac, 0x3e, 0x3b, 0x22, 0x90, 0x7f,
0x8b, 0x5f, 0x83, 0x0e, 0x3f, 0x44, 0x7c, 0x66, 0x88, 0x3e, 0x34, 0x4a, 0x63, 0xe5, 0x5f, 0xc8, 0x4c, 0xa1, 0x02, 0x65, 0xfe, 0xc8, 0x75, 0x7c, 0xf6, 0x4d, 0x9d, 0x60, 0x0d, 0x65, 0x2d, 0x33,
0x84, 0x86, 0x0a, 0xf2, 0xeb, 0x24, 0x2a, 0x9c, 0xec, 0x08, 0x77, 0xd1, 0x84, 0x60, 0x36, 0xb5, 0x30, 0xa5, 0xdb, 0xc1, 0xee, 0xf1, 0xb1, 0xda, 0x3d, 0x3e, 0x20, 0x14, 0x30, 0xfc, 0x29, 0xca,
0x4f, 0xfa, 0xb3, 0xa7, 0x59, 0x03, 0x5c, 0xce, 0xe0, 0x52, 0xb0, 0xa5, 0x64, 0x75, 0x29, 0x88, 0xf6, 0x5c, 0x4b, 0x1c, 0xfe, 0x92, 0x9e, 0x34, 0x1b, 0x9e, 0xe7, 0x7a, 0x35, 0xd7, 0x92, 0xd7,
0x20, 0x42, 0x35, 0xfd, 0x0b, 0x7d, 0xa7, 0xd4, 0xae, 0xf2, 0xc9, 0x1f, 0x7e, 0x95, 0x6f, 0xa0, 0x0e, 0x4e, 0x52, 0x06, 0xf8, 0x80, 0x50, 0xc0, 0xc8, 0x3f, 0xa4, 0x50, 0xb1, 0xee, 0x1e, 0x39,
0x29, 0x91, 0xa2, 0xe1, 0x07, 0xe5, 0x54, 0x31, 0x39, 0x9b, 0x2e, 0x5d, 0xe7, 0xdd, 0x76, 0x5d, 0x03, 0xd7, 0xb4, 0x76, 0x3c, 0xb7, 0xef, 0x31, 0xdf, 0xff, 0x46, 0x77, 0xff, 0x2e, 0x9a, 0x1d,
0x1c, 0x56, 0xc3, 0x4f, 0xc9, 0xd3, 0x51, 0xb2, 0x0a, 0x30, 0xcc, 0xb6, 0xdc, 0x08, 0x8d, 0x71, 0xc3, 0xcb, 0x41, 0x74, 0xfb, 0x7f, 0x90, 0xbc, 0x06, 0x9d, 0x9f, 0x44, 0x3c, 0x33, 0xc4, 0x0f,
0xf1, 0x52, 0xec, 0xa6, 0x27, 0x4a, 0xfd, 0x3f, 0x4e, 0x79, 0xb3, 0xd3, 0x6e, 0x72, 0x24, 0x83, 0x8d, 0x52, 0x59, 0xd9, 0x17, 0x63, 0x42, 0x23, 0x01, 0xf9, 0xfb, 0x0c, 0x2a, 0x5f, 0x6d, 0x08,
0x52, 0xab, 0xb6, 0xd3, 0x26, 0x37, 0x51, 0x7a, 0xb1, 0xe3, 0xfa, 0xd0, 0x71, 0x3c, 0x66, 0xfa, 0x0f, 0xd1, 0xbc, 0x60, 0x76, 0xb5, 0xbf, 0x09, 0xac, 0x5e, 0x67, 0x0d, 0x70, 0x39, 0x83, 0x4b,
0xae, 0xa3, 0xa7, 0x92, 0x40, 0x54, 0xa8, 0x85, 0x48, 0xa8, 0xc4, 0xe7, 0xbe, 0x4e, 0xa2, 0x09, 0xc1, 0x58, 0x8d, 0xd5, 0xa5, 0x20, 0x86, 0x08, 0xd5, 0xe4, 0x6f, 0xf4, 0x4e, 0xa9, 0x5d, 0xe5,
0xed, 0x2f, 0x30, 0xf8, 0x7f, 0xd0, 0xe5, 0xbb, 0x95, 0x46, 0x63, 0x61, 0xb9, 0xd2, 0x5c, 0x7b, 0x33, 0xdf, 0xfe, 0x2a, 0xdf, 0x41, 0x8b, 0xc2, 0x45, 0xa3, 0x07, 0xe5, 0x6c, 0x25, 0xb3, 0x9a,
0xb8, 0x5a, 0x69, 0x2e, 0xae, 0xdc, 0x6b, 0xac, 0x55, 0x68, 0x73, 0xb1, 0x5e, 0x5b, 0xaa, 0x2e, 0xab, 0x3e, 0xe4, 0xd9, 0x76, 0x4f, 0x34, 0xab, 0xd1, 0x53, 0xf2, 0x72, 0xec, 0xac, 0x02, 0x8c,
0xe7, 0x46, 0x0a, 0x57, 0xf6, 0x0f, 0x8a, 0x79, 0xcd, 0x22, 0xfe, 0xb7, 0x92, 0xff, 0x44, 0x38, 0xbc, 0xad, 0x38, 0x43, 0x13, 0x5c, 0xbc, 0x99, 0xb8, 0xe9, 0x89, 0x50, 0xff, 0x83, 0x6b, 0xde,
0x66, 0x5e, 0xad, 0x95, 0x2b, 0x9f, 0xe4, 0x12, 0x85, 0xf3, 0xfb, 0x07, 0xc5, 0x9c, 0x66, 0x25, 0xec, 0xb4, 0x9b, 0x1c, 0xc9, 0xa3, 0xec, 0x8e, 0xed, 0xf4, 0xc9, 0x47, 0x28, 0x57, 0x1b, 0xb8,
0x3e, 0xc1, 0xfd, 0x37, 0x7a, 0xe9, 0x28, 0xbb, 0x79, 0x6f, 0xb5, 0xbc, 0xb0, 0x56, 0xc9, 0x8d, 0x3e, 0x64, 0x1c, 0x8f, 0x99, 0xbe, 0xeb, 0xe8, 0xae, 0x24, 0x10, 0x75, 0xd4, 0x62, 0x48, 0xa8,
0x16, 0x0a, 0xfb, 0x07, 0xc5, 0x8b, 0x87, 0x8d, 0x64, 0x0a, 0xbe, 0x81, 0xce, 0xc7, 0x4c, 0x69, 0xc4, 0xd7, 0x7e, 0x9b, 0x41, 0xf3, 0xda, 0x9f, 0x70, 0xf0, 0x9f, 0xa0, 0x7b, 0x8f, 0x1b, 0x9d,
0xe5, 0xe3, 0x7b, 0x95, 0xc6, 0x5a, 0x2e, 0x59, 0xb8, 0xb8, 0x7f, 0x50, 0xc4, 0x9a, 0x55, 0x38, 0xce, 0xc6, 0x56, 0xa3, 0xbb, 0xfb, 0xe5, 0x4e, 0xa3, 0x5b, 0xdb, 0x7e, 0xd2, 0xd9, 0x6d, 0xd0,
0x26, 0xe6, 0xd1, 0x85, 0x43, 0x16, 0x8d, 0xd5, 0x7a, 0xad, 0x51, 0xc9, 0xa5, 0x0a, 0x97, 0xf6, 0x6e, 0xad, 0xdd, 0xda, 0x6c, 0x6e, 0x15, 0x67, 0xca, 0xf7, 0x4f, 0xcf, 0x2a, 0x25, 0x4d, 0x23,
0x0f, 0x8a, 0xe7, 0x62, 0x26, 0xb2, 0xab, 0x2c, 0xa2, 0x99, 0x98, 0x4d, 0xb9, 0xfe, 0xa0, 0xb6, 0xf9, 0xb7, 0x96, 0x3f, 0x44, 0x38, 0xa1, 0xde, 0x6c, 0xd5, 0x1b, 0x3f, 0x2d, 0xa6, 0xca, 0xb7,
0x52, 0x5f, 0x28, 0x37, 0x57, 0x69, 0x7d, 0x99, 0x56, 0x1a, 0x8d, 0x5c, 0xba, 0x60, 0xec, 0x1f, 0x4e, 0xcf, 0x2a, 0x45, 0x4d, 0x4b, 0x3c, 0xc1, 0xfd, 0x31, 0x7a, 0xeb, 0x22, 0xbb, 0xfb, 0x64,
0x14, 0x2f, 0x6b, 0xc6, 0x47, 0x2a, 0x7c, 0x0e, 0x4d, 0xc7, 0x9c, 0xac, 0x56, 0x6b, 0xcb, 0xb9, 0xa7, 0xbe, 0xb1, 0xdb, 0x28, 0xa6, 0xcb, 0xe5, 0xd3, 0xb3, 0xca, 0x9d, 0xf3, 0x4a, 0xd2, 0x05,
0x4c, 0xe1, 0xdc, 0xfe, 0x41, 0xf1, 0xac, 0x66, 0xc7, 0x63, 0x79, 0x64, 0xff, 0x16, 0x57, 0xea, 0x7f, 0x80, 0x6e, 0x25, 0x54, 0x69, 0xe3, 0xf3, 0x27, 0x8d, 0xce, 0x6e, 0x31, 0x53, 0xbe, 0x73,
0x8d, 0x4a, 0x6e, 0xec, 0xc8, 0xfe, 0x41, 0xc0, 0xe7, 0x7e, 0x95, 0x40, 0xf8, 0xe8, 0x1f, 0xbd, 0x7a, 0x56, 0xc1, 0x9a, 0x56, 0x54, 0x26, 0xd6, 0xd1, 0xed, 0x73, 0x1a, 0x9d, 0x9d, 0x76, 0xab,
0xf0, 0x7b, 0x28, 0x1f, 0x3a, 0x59, 0xac, 0xdf, 0x5d, 0xe5, 0xeb, 0xac, 0xd6, 0x6b, 0xcd, 0x5a, 0xd3, 0x28, 0x66, 0xcb, 0x77, 0x4f, 0xcf, 0x2a, 0x37, 0x13, 0x2a, 0x32, 0xab, 0xd4, 0xd0, 0x4a,
0xbd, 0x56, 0xc9, 0x8d, 0xc4, 0x76, 0x55, 0xb3, 0xaa, 0xb9, 0x0e, 0xc3, 0x75, 0x74, 0xe9, 0x38, 0x42, 0xa7, 0xde, 0xfe, 0xa2, 0xb5, 0xdd, 0xde, 0xa8, 0x77, 0x77, 0x68, 0x7b, 0x8b, 0x36, 0x3a,
0xcb, 0x95, 0x47, 0x6f, 0xe7, 0x12, 0x85, 0xf9, 0xfd, 0x83, 0xe2, 0x85, 0xa3, 0x86, 0x2b, 0x8f, 0x9d, 0x62, 0xae, 0x6c, 0x9c, 0x9e, 0x55, 0xee, 0x69, 0xca, 0x17, 0x22, 0x7c, 0x0d, 0x2d, 0x27,
0xde, 0xfe, 0xe6, 0xa7, 0xaf, 0x1c, 0xaf, 0x98, 0xe3, 0x07, 0x20, 0x7d, 0x69, 0x6f, 0xa2, 0xf3, 0x8c, 0xec, 0x34, 0x5b, 0x5b, 0xc5, 0x7c, 0xf9, 0xe6, 0xe9, 0x59, 0xe5, 0x86, 0xa6, 0xc7, 0xcf,
0xba, 0xe3, 0xbb, 0x95, 0xb5, 0x85, 0xf2, 0xc2, 0xda, 0x42, 0x6e, 0x44, 0xc4, 0x40, 0xa3, 0xde, 0xf2, 0xc2, 0xfe, 0xd5, 0xb6, 0xdb, 0x9d, 0x46, 0x71, 0xf6, 0xc2, 0xfe, 0xc1, 0x81, 0xaf, 0xfd,
0x65, 0x7d, 0x13, 0xda, 0xee, 0x6b, 0x68, 0x3a, 0xf6, 0x16, 0x95, 0xfb, 0x15, 0x1a, 0x66, 0x94, 0x5d, 0x0a, 0xe1, 0x8b, 0x7f, 0x35, 0xc3, 0x1f, 0xa0, 0x52, 0x64, 0xa4, 0xd6, 0x7e, 0xbc, 0xc3,
0xbe, 0x7e, 0xb6, 0xcd, 0x3c, 0xfc, 0x3a, 0xc2, 0x3a, 0x79, 0x61, 0xe5, 0xc1, 0xc2, 0xc3, 0x46, 0xd7, 0xd9, 0x6c, 0xb7, 0xba, 0xad, 0x76, 0xab, 0x51, 0x9c, 0x49, 0xec, 0xaa, 0xa6, 0xd5, 0x72,
0x6e, 0xb4, 0x70, 0x61, 0xff, 0xa0, 0x38, 0xad, 0xb1, 0x17, 0x3a, 0x3b, 0xe6, 0x9e, 0x3f, 0xf7, 0x1d, 0x86, 0xdb, 0xe8, 0xee, 0x65, 0x9a, 0xdb, 0xcf, 0xde, 0x2f, 0xa6, 0xca, 0xeb, 0xa7, 0x67,
0xfb, 0x51, 0x34, 0xa9, 0x7f, 0x37, 0xc2, 0xaf, 0xa3, 0x73, 0x4b, 0xd5, 0x15, 0x9e, 0x89, 0x4b, 0x95, 0xdb, 0x17, 0x15, 0xb7, 0x9f, 0xbd, 0xff, 0xbb, 0xbf, 0xfa, 0xee, 0xe5, 0x82, 0x35, 0xde,
0x75, 0x11, 0x01, 0x2e, 0xe6, 0x46, 0xc4, 0xe3, 0x74, 0x2a, 0xff, 0x8d, 0xff, 0x0b, 0xe5, 0x0f, 0x00, 0xe9, 0x4b, 0x7b, 0x0f, 0xdd, 0xd2, 0x0d, 0x3f, 0x6e, 0xec, 0x6e, 0xd4, 0x37, 0x76, 0x37,
0xd1, 0xcb, 0x55, 0x5a, 0x59, 0x5c, 0xab, 0xd3, 0x87, 0xb9, 0x44, 0xe1, 0x25, 0xbe, 0x61, 0xba, 0x8a, 0x33, 0xe2, 0x0c, 0x34, 0xea, 0x63, 0x16, 0x98, 0x90, 0x76, 0xbf, 0x87, 0x96, 0x13, 0x5f,
0x4d, 0xd9, 0xf6, 0xa0, 0x05, 0xed, 0xe1, 0x5b, 0xe8, 0xf2, 0x21, 0xc3, 0xc6, 0xc3, 0xbb, 0x2b, 0xd1, 0x78, 0xda, 0xa0, 0x91, 0x47, 0xe9, 0xeb, 0x67, 0x87, 0xcc, 0xc3, 0xdf, 0x47, 0x58, 0x27,
0xd5, 0xda, 0x1d, 0xf1, 0xbc, 0xd1, 0xc2, 0xd5, 0xfd, 0x83, 0xe2, 0x25, 0xdd, 0xb6, 0x21, 0x3e, 0x6f, 0x6c, 0x7f, 0xb1, 0xf1, 0x65, 0xa7, 0x98, 0x2e, 0xdf, 0x3e, 0x3d, 0xab, 0x2c, 0x6b, 0xec,
0xc5, 0x71, 0x28, 0x9b, 0xc0, 0xb7, 0x51, 0xf1, 0x04, 0xfb, 0x68, 0x01, 0xc9, 0x02, 0xd9, 0x3f, 0x8d, 0xc1, 0x91, 0x79, 0xe2, 0xaf, 0xfd, 0x73, 0x1a, 0x2d, 0xe8, 0xef, 0x46, 0xf8, 0xfb, 0xe8,
0x28, 0x5e, 0x39, 0xc6, 0x89, 0x5a, 0x47, 0x36, 0x81, 0xdf, 0x42, 0x17, 0x8f, 0xf7, 0x14, 0xd6, 0xe6, 0x66, 0x73, 0x9b, 0x7b, 0xe2, 0x66, 0x5b, 0x9c, 0x00, 0x1f, 0x16, 0x67, 0xc4, 0x74, 0x3a,
0xc5, 0x31, 0xf6, 0x73, 0x7f, 0x4c, 0xa0, 0x71, 0x35, 0xf5, 0xf8, 0xa6, 0x55, 0x28, 0xad, 0xf3, 0x95, 0xff, 0xc6, 0x7f, 0x84, 0x4a, 0xe7, 0xe8, 0xf5, 0x26, 0x6d, 0xd4, 0x76, 0xdb, 0xf4, 0xcb,
0x26, 0x51, 0xae, 0x34, 0x6b, 0xf5, 0x26, 0x48, 0xe1, 0xa6, 0x29, 0x5e, 0xcd, 0x85, 0x9f, 0x3c, 0x62, 0xaa, 0xfc, 0x16, 0xdf, 0x30, 0x5d, 0xa7, 0x6e, 0x7b, 0x90, 0x82, 0x4e, 0xf0, 0x27, 0xe8,
0xc7, 0x35, 0xfa, 0x72, 0xa5, 0x56, 0xa1, 0xd5, 0xc5, 0x30, 0xa2, 0x8a, 0xbd, 0xcc, 0x1c, 0xe6, 0xde, 0x39, 0xc5, 0xce, 0x97, 0x8f, 0xb7, 0x9b, 0xad, 0xcf, 0xc4, 0x7c, 0xe9, 0xf2, 0xdb, 0xa7,
0xd9, 0x2d, 0xfc, 0x36, 0xba, 0x14, 0x77, 0xde, 0xb8, 0xb7, 0x78, 0x3b, 0xdc, 0x25, 0x58, 0xa0, 0x67, 0x95, 0xbb, 0xba, 0x6e, 0x47, 0x3c, 0xc5, 0x71, 0xa8, 0x90, 0xc2, 0x8f, 0x50, 0xe5, 0x0a,
0xf6, 0x80, 0xc6, 0x56, 0x6b, 0x13, 0x02, 0xf3, 0x4e, 0xcc, 0xaa, 0x5a, 0xbb, 0xbf, 0xb0, 0x52, 0xfd, 0x78, 0x01, 0x99, 0x32, 0x39, 0x3d, 0xab, 0xdc, 0xbf, 0xc4, 0x88, 0x5a, 0x47, 0x21, 0x85,
0x2d, 0x0b, 0xab, 0x64, 0x21, 0xbf, 0x7f, 0x50, 0x3c, 0xaf, 0xac, 0xe4, 0x07, 0x0e, 0x6e, 0x36, 0x7f, 0x88, 0xee, 0x5c, 0x6e, 0x29, 0x8a, 0x8b, 0x4b, 0xf4, 0xd7, 0xfe, 0x35, 0x85, 0xe6, 0x54,
0xf7, 0x4d, 0x02, 0xcd, 0x7c, 0xf7, 0xf0, 0xc2, 0x0f, 0xd0, 0xab, 0xb0, 0x5f, 0x47, 0x5a, 0x81, 0xd5, 0xe3, 0x9b, 0xd6, 0xa0, 0xb4, 0xcd, 0x93, 0x44, 0xbd, 0xd1, 0x6d, 0xb5, 0xbb, 0x30, 0x8a,
0xec, 0x5b, 0x62, 0x0f, 0x17, 0x56, 0x57, 0x2b, 0xb5, 0x72, 0x6e, 0xa4, 0x30, 0xbb, 0x7f, 0x50, 0x36, 0x4d, 0xf1, 0x5a, 0x2e, 0xfc, 0xe4, 0x3e, 0xae, 0xd1, 0xb7, 0x1a, 0xad, 0x06, 0x6d, 0xd6,
0xbc, 0xf6, 0xdd, 0x2e, 0x17, 0x7a, 0x3d, 0xe6, 0x58, 0xa7, 0x74, 0xbc, 0x54, 0xa7, 0xcb, 0x95, 0xa2, 0x13, 0x55, 0xec, 0x2d, 0xe6, 0x30, 0xcf, 0xee, 0xe1, 0xf7, 0xd1, 0xdd, 0xa4, 0xf1, 0xce,
0xb5, 0x5c, 0xe2, 0x34, 0x8e, 0x97, 0x5c, 0xaf, 0xcd, 0xfa, 0xa5, 0xbb, 0x4f, 0xbe, 0x9d, 0x19, 0x93, 0xda, 0xa3, 0x68, 0x97, 0x60, 0x81, 0xda, 0x04, 0x9d, 0x71, 0xef, 0x00, 0x0e, 0xe6, 0x47,
0x79, 0xfa, 0xed, 0xcc, 0xc8, 0x93, 0x67, 0x33, 0x89, 0xa7, 0xcf, 0x66, 0x12, 0x3f, 0x7b, 0x3e, 0x09, 0xad, 0x66, 0xeb, 0xe9, 0xc6, 0x76, 0xb3, 0x2e, 0xb4, 0x32, 0xe5, 0xd2, 0xe9, 0x59, 0xe5,
0x33, 0xf2, 0xd5, 0xf3, 0x99, 0xc4, 0xd3, 0xe7, 0x33, 0x23, 0x7f, 0x7a, 0x3e, 0x33, 0xf2, 0xe8, 0x96, 0xd2, 0x92, 0x0f, 0x1c, 0x5c, 0x6d, 0xed, 0x77, 0x29, 0xb4, 0xf2, 0xf5, 0xc5, 0x0b, 0x7f,
0xb5, 0xb6, 0xdd, 0xdf, 0xdc, 0x5a, 0xbf, 0xde, 0x72, 0xbb, 0x37, 0xfc, 0x3d, 0xa7, 0xd5, 0xdf, 0x81, 0xde, 0x81, 0xfd, 0xba, 0x90, 0x0a, 0x64, 0xde, 0x12, 0x7b, 0xb8, 0xb1, 0xb3, 0xd3, 0x68,
0xb4, 0x9d, 0xb6, 0xf6, 0x4b, 0xff, 0xcf, 0x0f, 0xeb, 0x19, 0xf8, 0xf5, 0xd6, 0x3f, 0x02, 0x00, 0xd5, 0x8b, 0x33, 0xe5, 0xd5, 0xd3, 0xb3, 0xca, 0x83, 0xaf, 0x37, 0xb9, 0x31, 0x1a, 0x31, 0xc7,
0x00, 0xff, 0xff, 0x68, 0x4a, 0x6e, 0xeb, 0x13, 0x21, 0x00, 0x00, 0xba, 0xa6, 0xe1, 0xcd, 0x36, 0xdd, 0x6a, 0xec, 0x16, 0x53, 0xd7, 0x31, 0xbc, 0xe9, 0x7a, 0x7d,
0x16, 0x54, 0x1f, 0xbf, 0xf8, 0x6a, 0x65, 0xe6, 0xe5, 0x57, 0x2b, 0x33, 0x2f, 0x5e, 0xad, 0xa4,
0x5e, 0xbe, 0x5a, 0x49, 0xfd, 0xf5, 0xeb, 0x95, 0x99, 0xdf, 0xbc, 0x5e, 0x49, 0xbd, 0x7c, 0xbd,
0x32, 0xf3, 0x6f, 0xaf, 0x57, 0x66, 0x9e, 0x7d, 0xaf, 0x6f, 0x07, 0x07, 0xe3, 0xbd, 0x87, 0x3d,
0x77, 0xf8, 0xae, 0x7f, 0xe2, 0xf4, 0x82, 0x03, 0xdb, 0xe9, 0x6b, 0xbf, 0xf4, 0xff, 0xd9, 0xb1,
0x97, 0x87, 0x5f, 0x3f, 0xfc, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x35, 0x55, 0x08, 0x69, 0xf0,
0x21, 0x00, 0x00,
} }
func (m *Hello) Marshal() (dAtA []byte, err error) { func (m *Hello) Marshal() (dAtA []byte, err error) {
@ -1363,6 +1372,16 @@ func (m *Hello) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = l
if m.Timestamp != 0 {
i = encodeVarintBep(dAtA, i, uint64(m.Timestamp))
i--
dAtA[i] = 0x28
}
if m.NumConnections != 0 {
i = encodeVarintBep(dAtA, i, uint64(m.NumConnections))
i--
dAtA[i] = 0x20
}
if len(m.ClientVersion) > 0 { if len(m.ClientVersion) > 0 {
i -= len(m.ClientVersion) i -= len(m.ClientVersion)
copy(dAtA[i:], m.ClientVersion) copy(dAtA[i:], m.ClientVersion)
@ -1440,6 +1459,16 @@ func (m *ClusterConfig) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = l
if m.Secondary {
i--
if m.Secondary {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x10
}
if len(m.Folders) > 0 { if len(m.Folders) > 0 {
for iNdEx := len(m.Folders) - 1; iNdEx >= 0; iNdEx-- { for iNdEx := len(m.Folders) - 1; iNdEx >= 0; iNdEx-- {
{ {
@ -2612,6 +2641,12 @@ func (m *Hello) ProtoSize() (n int) {
if l > 0 { if l > 0 {
n += 1 + l + sovBep(uint64(l)) n += 1 + l + sovBep(uint64(l))
} }
if m.NumConnections != 0 {
n += 1 + sovBep(uint64(m.NumConnections))
}
if m.Timestamp != 0 {
n += 1 + sovBep(uint64(m.Timestamp))
}
return n return n
} }
@ -2642,6 +2677,9 @@ func (m *ClusterConfig) ProtoSize() (n int) {
n += 1 + l + sovBep(uint64(l)) n += 1 + l + sovBep(uint64(l))
} }
} }
if m.Secondary {
n += 2
}
return n return n
} }
@ -3258,6 +3296,44 @@ func (m *Hello) Unmarshal(dAtA []byte) error {
} }
m.ClientVersion = string(dAtA[iNdEx:postIndex]) m.ClientVersion = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex iNdEx = postIndex
case 4:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field NumConnections", wireType)
}
m.NumConnections = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.NumConnections |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 5:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType)
}
m.Timestamp = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Timestamp |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipBep(dAtA[iNdEx:]) skippy, err := skipBep(dAtA[iNdEx:])
@ -3430,6 +3506,26 @@ func (m *ClusterConfig) Unmarshal(dAtA []byte) error {
return err return err
} }
iNdEx = postIndex iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Secondary", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.Secondary = bool(v != 0)
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipBep(dAtA[iNdEx:]) skippy, err := skipBep(dAtA[iNdEx:])

View File

@ -43,12 +43,12 @@ const (
// receives encrypted metadata and requests from the untrusted device, so it // receives encrypted metadata and requests from the untrusted device, so it
// must decrypt those and answer requests by encrypting the data. // must decrypt those and answer requests by encrypting the data.
type encryptedModel struct { type encryptedModel struct {
model contextLessModel model rawModel
folderKeys *folderKeyRegistry folderKeys *folderKeyRegistry
keyGen *KeyGenerator keyGen *KeyGenerator
} }
func newEncryptedModel(model contextLessModel, folderKeys *folderKeyRegistry, keyGen *KeyGenerator) encryptedModel { func newEncryptedModel(model rawModel, folderKeys *folderKeyRegistry, keyGen *KeyGenerator) encryptedModel {
return encryptedModel{ return encryptedModel{
model: model, model: model,
folderKeys: folderKeys, folderKeys: folderKeys,

View File

@ -8,14 +8,6 @@ import (
"io" "io"
) )
// The HelloIntf interface is implemented by the version specific hello
// message. It knows its magic number and how to serialize itself to a byte
// buffer.
type HelloIntf interface {
Magic() uint32
Marshal() ([]byte, error)
}
var ( var (
// ErrTooOldVersion is returned by ExchangeHello when the other side // ErrTooOldVersion is returned by ExchangeHello when the other side
// speaks an older, incompatible version of the protocol. // speaks an older, incompatible version of the protocol.
@ -25,7 +17,10 @@ var (
ErrUnknownMagic = errors.New("the remote device speaks an unknown (newer?) version of the protocol") ErrUnknownMagic = errors.New("the remote device speaks an unknown (newer?) version of the protocol")
) )
func ExchangeHello(c io.ReadWriter, h HelloIntf) (Hello, error) { func ExchangeHello(c io.ReadWriter, h Hello) (Hello, error) {
if h.Timestamp == 0 {
panic("bug: missing timestamp in outgoing hello")
}
if err := writeHello(c, h); err != nil { if err := writeHello(c, h); err != nil {
return Hello{}, err return Hello{}, err
} }
@ -80,7 +75,7 @@ func readHello(c io.Reader) (Hello, error) {
return Hello{}, ErrUnknownMagic return Hello{}, ErrUnknownMagic
} }
func writeHello(c io.Writer, h HelloIntf) error { func writeHello(c io.Writer, h Hello) error {
msg, err := h.Marshal() msg, err := h.Marshal()
if err != nil { if err != nil {
return err return err

View File

@ -35,10 +35,11 @@ func TestVersion14Hello(t *testing.T) {
conn := &readWriter{outBuf, inBuf} conn := &readWriter{outBuf, inBuf}
send := &Hello{ send := Hello{
DeviceName: "this device", DeviceName: "this device",
ClientName: "other client", ClientName: "other client",
ClientVersion: "v0.14.6", ClientVersion: "v0.14.6",
Timestamp: 1234567890,
} }
res, err := ExchangeHello(conn, send) res, err := ExchangeHello(conn, send)
@ -80,10 +81,11 @@ func TestOldHelloMsgs(t *testing.T) {
conn := &readWriter{outBuf, inBuf} conn := &readWriter{outBuf, inBuf}
send := &Hello{ send := Hello{
DeviceName: "this device", DeviceName: "this device",
ClientName: "other client", ClientName: "other client",
ClientVersion: "v1.0.0", ClientVersion: "v1.0.0",
Timestamp: 1234567890,
} }
_, err := ExchangeHello(conn, send) _, err := ExchangeHello(conn, send)

View File

@ -8,6 +8,16 @@ import (
) )
type mockedConnectionInfo struct { type mockedConnectionInfo struct {
ConnectionIDStub func() string
connectionIDMutex sync.RWMutex
connectionIDArgsForCall []struct {
}
connectionIDReturns struct {
result1 string
}
connectionIDReturnsOnCall map[int]struct {
result1 string
}
CryptoStub func() string CryptoStub func() string
cryptoMutex sync.RWMutex cryptoMutex sync.RWMutex
cryptoArgsForCall []struct { cryptoArgsForCall []struct {
@ -92,6 +102,59 @@ type mockedConnectionInfo struct {
invocationsMutex sync.RWMutex invocationsMutex sync.RWMutex
} }
func (fake *mockedConnectionInfo) ConnectionID() string {
fake.connectionIDMutex.Lock()
ret, specificReturn := fake.connectionIDReturnsOnCall[len(fake.connectionIDArgsForCall)]
fake.connectionIDArgsForCall = append(fake.connectionIDArgsForCall, struct {
}{})
stub := fake.ConnectionIDStub
fakeReturns := fake.connectionIDReturns
fake.recordInvocation("ConnectionID", []interface{}{})
fake.connectionIDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *mockedConnectionInfo) ConnectionIDCallCount() int {
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
return len(fake.connectionIDArgsForCall)
}
func (fake *mockedConnectionInfo) ConnectionIDCalls(stub func() string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = stub
}
func (fake *mockedConnectionInfo) ConnectionIDReturns(result1 string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
fake.connectionIDReturns = struct {
result1 string
}{result1}
}
func (fake *mockedConnectionInfo) ConnectionIDReturnsOnCall(i int, result1 string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
if fake.connectionIDReturnsOnCall == nil {
fake.connectionIDReturnsOnCall = make(map[int]struct {
result1 string
})
}
fake.connectionIDReturnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *mockedConnectionInfo) Crypto() string { func (fake *mockedConnectionInfo) Crypto() string {
fake.cryptoMutex.Lock() fake.cryptoMutex.Lock()
ret, specificReturn := fake.cryptoReturnsOnCall[len(fake.cryptoArgsForCall)] ret, specificReturn := fake.cryptoReturnsOnCall[len(fake.cryptoArgsForCall)]
@ -519,6 +582,8 @@ func (fake *mockedConnectionInfo) TypeReturnsOnCall(i int, result1 string) {
func (fake *mockedConnectionInfo) Invocations() map[string][][]interface{} { func (fake *mockedConnectionInfo) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock() fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock() defer fake.invocationsMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.cryptoMutex.RLock() fake.cryptoMutex.RLock()
defer fake.cryptoMutex.RUnlock() defer fake.cryptoMutex.RUnlock()
fake.establishedAtMutex.RLock() fake.establishedAtMutex.RLock()

View File

@ -31,6 +31,16 @@ type Connection struct {
clusterConfigArgsForCall []struct { clusterConfigArgsForCall []struct {
arg1 protocol.ClusterConfig arg1 protocol.ClusterConfig
} }
ConnectionIDStub func() string
connectionIDMutex sync.RWMutex
connectionIDArgsForCall []struct {
}
connectionIDReturns struct {
result1 string
}
connectionIDReturnsOnCall map[int]struct {
result1 string
}
CryptoStub func() string CryptoStub func() string
cryptoMutex sync.RWMutex cryptoMutex sync.RWMutex
cryptoArgsForCall []struct { cryptoArgsForCall []struct {
@ -315,6 +325,59 @@ func (fake *Connection) ClusterConfigArgsForCall(i int) protocol.ClusterConfig {
return argsForCall.arg1 return argsForCall.arg1
} }
func (fake *Connection) ConnectionID() string {
fake.connectionIDMutex.Lock()
ret, specificReturn := fake.connectionIDReturnsOnCall[len(fake.connectionIDArgsForCall)]
fake.connectionIDArgsForCall = append(fake.connectionIDArgsForCall, struct {
}{})
stub := fake.ConnectionIDStub
fakeReturns := fake.connectionIDReturns
fake.recordInvocation("ConnectionID", []interface{}{})
fake.connectionIDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Connection) ConnectionIDCallCount() int {
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
return len(fake.connectionIDArgsForCall)
}
func (fake *Connection) ConnectionIDCalls(stub func() string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = stub
}
func (fake *Connection) ConnectionIDReturns(result1 string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
fake.connectionIDReturns = struct {
result1 string
}{result1}
}
func (fake *Connection) ConnectionIDReturnsOnCall(i int, result1 string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
if fake.connectionIDReturnsOnCall == nil {
fake.connectionIDReturnsOnCall = make(map[int]struct {
result1 string
})
}
fake.connectionIDReturnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *Connection) Crypto() string { func (fake *Connection) Crypto() string {
fake.cryptoMutex.Lock() fake.cryptoMutex.Lock()
ret, specificReturn := fake.cryptoReturnsOnCall[len(fake.cryptoArgsForCall)] ret, specificReturn := fake.cryptoReturnsOnCall[len(fake.cryptoArgsForCall)]
@ -1162,6 +1225,8 @@ func (fake *Connection) Invocations() map[string][][]interface{} {
defer fake.closedMutex.RUnlock() defer fake.closedMutex.RUnlock()
fake.clusterConfigMutex.RLock() fake.clusterConfigMutex.RLock()
defer fake.clusterConfigMutex.RUnlock() defer fake.clusterConfigMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.cryptoMutex.RLock() fake.cryptoMutex.RLock()
defer fake.cryptoMutex.RUnlock() defer fake.cryptoMutex.RUnlock()
fake.deviceIDMutex.RLock() fake.deviceIDMutex.RLock()

View File

@ -10,6 +10,16 @@ import (
) )
type ConnectionInfo struct { type ConnectionInfo struct {
ConnectionIDStub func() string
connectionIDMutex sync.RWMutex
connectionIDArgsForCall []struct {
}
connectionIDReturns struct {
result1 string
}
connectionIDReturnsOnCall map[int]struct {
result1 string
}
CryptoStub func() string CryptoStub func() string
cryptoMutex sync.RWMutex cryptoMutex sync.RWMutex
cryptoArgsForCall []struct { cryptoArgsForCall []struct {
@ -94,6 +104,59 @@ type ConnectionInfo struct {
invocationsMutex sync.RWMutex invocationsMutex sync.RWMutex
} }
func (fake *ConnectionInfo) ConnectionID() string {
fake.connectionIDMutex.Lock()
ret, specificReturn := fake.connectionIDReturnsOnCall[len(fake.connectionIDArgsForCall)]
fake.connectionIDArgsForCall = append(fake.connectionIDArgsForCall, struct {
}{})
stub := fake.ConnectionIDStub
fakeReturns := fake.connectionIDReturns
fake.recordInvocation("ConnectionID", []interface{}{})
fake.connectionIDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *ConnectionInfo) ConnectionIDCallCount() int {
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
return len(fake.connectionIDArgsForCall)
}
func (fake *ConnectionInfo) ConnectionIDCalls(stub func() string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = stub
}
func (fake *ConnectionInfo) ConnectionIDReturns(result1 string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
fake.connectionIDReturns = struct {
result1 string
}{result1}
}
func (fake *ConnectionInfo) ConnectionIDReturnsOnCall(i int, result1 string) {
fake.connectionIDMutex.Lock()
defer fake.connectionIDMutex.Unlock()
fake.ConnectionIDStub = nil
if fake.connectionIDReturnsOnCall == nil {
fake.connectionIDReturnsOnCall = make(map[int]struct {
result1 string
})
}
fake.connectionIDReturnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *ConnectionInfo) Crypto() string { func (fake *ConnectionInfo) Crypto() string {
fake.cryptoMutex.Lock() fake.cryptoMutex.Lock()
ret, specificReturn := fake.cryptoReturnsOnCall[len(fake.cryptoArgsForCall)] ret, specificReturn := fake.cryptoReturnsOnCall[len(fake.cryptoArgsForCall)]
@ -521,6 +584,8 @@ func (fake *ConnectionInfo) TypeReturnsOnCall(i int, result1 string) {
func (fake *ConnectionInfo) Invocations() map[string][][]interface{} { func (fake *ConnectionInfo) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock() fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock() defer fake.invocationsMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.cryptoMutex.RLock() fake.cryptoMutex.RLock()
defer fake.cryptoMutex.RUnlock() defer fake.cryptoMutex.RUnlock()
fake.establishedAtMutex.RLock() fake.establishedAtMutex.RLock()

View File

@ -9,27 +9,27 @@ package protocol
import "golang.org/x/text/unicode/norm" import "golang.org/x/text/unicode/norm"
func makeNative(m contextLessModel) contextLessModel { return nativeModel{m} } func makeNative(m rawModel) rawModel { return nativeModel{m} }
type nativeModel struct { type nativeModel struct {
contextLessModel rawModel
} }
func (m nativeModel) Index(folder string, files []FileInfo) error { func (m nativeModel) Index(folder string, files []FileInfo) error {
for i := range files { for i := range files {
files[i].Name = norm.NFD.String(files[i].Name) files[i].Name = norm.NFD.String(files[i].Name)
} }
return m.contextLessModel.Index(folder, files) return m.rawModel.Index(folder, files)
} }
func (m nativeModel) IndexUpdate(folder string, files []FileInfo) error { func (m nativeModel) IndexUpdate(folder string, files []FileInfo) error {
for i := range files { for i := range files {
files[i].Name = norm.NFD.String(files[i].Name) files[i].Name = norm.NFD.String(files[i].Name)
} }
return m.contextLessModel.IndexUpdate(folder, files) return m.rawModel.IndexUpdate(folder, files)
} }
func (m nativeModel) Request(folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) { func (m nativeModel) Request(folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) {
name = norm.NFD.String(name) name = norm.NFD.String(name)
return m.contextLessModel.Request(folder, name, blockNo, size, offset, hash, weakHash, fromTemporary) return m.rawModel.Request(folder, name, blockNo, size, offset, hash, weakHash, fromTemporary)
} }

View File

@ -7,4 +7,4 @@ package protocol
// Normal Unixes uses NFC and slashes, which is the wire format. // Normal Unixes uses NFC and slashes, which is the wire format.
func makeNative(m contextLessModel) contextLessModel { return m } func makeNative(m rawModel) rawModel { return m }

View File

@ -13,20 +13,20 @@ import (
"strings" "strings"
) )
func makeNative(m contextLessModel) contextLessModel { return nativeModel{m} } func makeNative(m rawModel) rawModel { return nativeModel{m} }
type nativeModel struct { type nativeModel struct {
contextLessModel rawModel
} }
func (m nativeModel) Index(folder string, files []FileInfo) error { func (m nativeModel) Index(folder string, files []FileInfo) error {
files = fixupFiles(files) files = fixupFiles(files)
return m.contextLessModel.Index(folder, files) return m.rawModel.Index(folder, files)
} }
func (m nativeModel) IndexUpdate(folder string, files []FileInfo) error { func (m nativeModel) IndexUpdate(folder string, files []FileInfo) error {
files = fixupFiles(files) files = fixupFiles(files)
return m.contextLessModel.IndexUpdate(folder, files) return m.rawModel.IndexUpdate(folder, files)
} }
func (m nativeModel) Request(folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) { func (m nativeModel) Request(folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) {
@ -36,7 +36,7 @@ func (m nativeModel) Request(folder, name string, blockNo, size int32, offset in
} }
name = filepath.FromSlash(name) name = filepath.FromSlash(name)
return m.contextLessModel.Request(folder, name, blockNo, size, offset, hash, weakHash, fromTemporary) return m.rawModel.Request(folder, name, blockNo, size, offset, hash, weakHash, fromTemporary)
} }
func fixupFiles(files []FileInfo) []FileInfo { func fixupFiles(files []FileInfo) []FileInfo {

View File

@ -136,9 +136,9 @@ type Model interface {
DownloadProgress(conn Connection, folder string, updates []FileDownloadProgressUpdate) error DownloadProgress(conn Connection, folder string, updates []FileDownloadProgressUpdate) error
} }
// contextLessModel is the Model interface, but without the initial // rawModel is the Model interface, but without the initial Connection
// Connection parameter. Internal use only. // parameter. Internal use only.
type contextLessModel interface { type rawModel interface {
Index(folder string, files []FileInfo) error Index(folder string, files []FileInfo) error
IndexUpdate(folder string, files []FileInfo) error IndexUpdate(folder string, files []FileInfo) error
Request(folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) Request(folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error)
@ -177,6 +177,7 @@ type ConnectionInfo interface {
String() string String() string
Crypto() string Crypto() string
EstablishedAt() time.Time EstablishedAt() time.Time
ConnectionID() string
} }
type rawConnection struct { type rawConnection struct {
@ -184,8 +185,9 @@ type rawConnection struct {
deviceID DeviceID deviceID DeviceID
idString string idString string
model contextLessModel model rawModel
startTime time.Time startTime time.Time
started chan struct{}
cr *countingReader cr *countingReader
cw *countingWriter cw *countingWriter
@ -263,7 +265,7 @@ func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, closer
return wc return wc
} }
func newRawConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, closer io.Closer, receiver contextLessModel, connInfo ConnectionInfo, compress Compression) *rawConnection { func newRawConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, closer io.Closer, receiver rawModel, connInfo ConnectionInfo, compress Compression) *rawConnection {
idString := deviceID.String() idString := deviceID.String()
cr := &countingReader{Reader: reader, idString: idString} cr := &countingReader{Reader: reader, idString: idString}
cw := &countingWriter{Writer: writer, idString: idString} cw := &countingWriter{Writer: writer, idString: idString}
@ -274,6 +276,7 @@ func newRawConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, clo
deviceID: deviceID, deviceID: deviceID,
idString: deviceID.String(), idString: deviceID.String(),
model: receiver, model: receiver,
started: make(chan struct{}),
cr: cr, cr: cr,
cw: cw, cw: cw,
closer: closer, closer: closer,
@ -315,6 +318,7 @@ func (c *rawConnection) Start() {
c.loopWG.Done() c.loopWG.Done()
}() }()
c.startTime = time.Now().Truncate(time.Second) c.startTime = time.Now().Truncate(time.Second)
close(c.started)
} }
func (c *rawConnection) DeviceID() DeviceID { func (c *rawConnection) DeviceID() DeviceID {
@ -960,9 +964,9 @@ func (c *rawConnection) Close(err error) {
// internalClose is called if there is an unexpected error during normal operation. // internalClose is called if there is an unexpected error during normal operation.
func (c *rawConnection) internalClose(err error) { func (c *rawConnection) internalClose(err error) {
c.closeOnce.Do(func() { c.closeOnce.Do(func() {
l.Debugln("close due to", err) l.Debugf("close connection to %s at %s due to %v", c.deviceID.Short(), c.ConnectionInfo, err)
if cerr := c.closer.Close(); cerr != nil { if cerr := c.closer.Close(); cerr != nil {
l.Debugln(c.deviceID, "failed to close underlying conn:", cerr) l.Debugf("failed to close underlying conn %s at %s %v:", c.deviceID.Short(), c.ConnectionInfo, cerr)
} }
close(c.closed) close(c.closed)
@ -975,7 +979,11 @@ func (c *rawConnection) internalClose(err error) {
} }
c.awaitingMut.Unlock() c.awaitingMut.Unlock()
<-c.dispatcherLoopStopped if !c.startTime.IsZero() {
// Wait for the dispatcher loop to exit, if it was started to
// begin with.
<-c.dispatcherLoopStopped
}
c.model.Closed(err) c.model.Closed(err)
}) })
@ -1108,7 +1116,7 @@ func messageContext(msg message) (string, error) {
// connectionWrappingModel takes the Model interface from the model package, // connectionWrappingModel takes the Model interface from the model package,
// which expects the Connection as the first parameter in all methods, and // which expects the Connection as the first parameter in all methods, and
// wraps it to conform to the protocol.contextLessModel interface. // wraps it to conform to the rawModel interface.
type connectionWrappingModel struct { type connectionWrappingModel struct {
conn Connection conn Connection
model Model model Model

View File

@ -0,0 +1,16 @@
// Copyright (C) 2023 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 https://mozilla.org/MPL/2.0/.
package sliceutil
// RemoveAndZero removes the element at index i from slice s and returns the
// resulting slice. The slice ordering is preserved; the last slice element
// is zeroed before shrinking.
func RemoveAndZero[E any, S ~[]E](s S, i int) S {
copy(s[i:], s[i+1:])
s[len(s)-1] = *new(E)
return s[:len(s)-1]
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2023 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 https://mozilla.org/MPL/2.0/.
package sliceutil_test
import (
"testing"
"github.com/syncthing/syncthing/lib/sliceutil"
"golang.org/x/exp/slices"
)
func TestRemoveAndZero(t *testing.T) {
a := []int{1, 2, 3, 4, 5}
b := sliceutil.RemoveAndZero(a, 2)
exp := []int{1, 2, 4, 5}
if !slices.Equal(b, exp) {
t.Errorf("got %v, expected %v", b, exp)
}
for _, e := range a {
if e == 3 {
t.Errorf("element should have been zeroed")
}
}
}

View File

@ -249,7 +249,7 @@ func (a *App) startup() error {
} }
keyGen := protocol.NewKeyGenerator() keyGen := protocol.NewKeyGenerator()
m := model.NewModel(a.cfg, a.myID, "syncthing", build.Version, a.ll, protectedFiles, a.evLogger, keyGen) m := model.NewModel(a.cfg, a.myID, a.ll, protectedFiles, a.evLogger, keyGen)
if a.opts.DeadlockTimeoutS > 0 { if a.opts.DeadlockTimeoutS > 0 {
m.StartDeadlockDetector(time.Duration(a.opts.DeadlockTimeoutS) * time.Second) m.StartDeadlockDetector(time.Duration(a.opts.DeadlockTimeoutS) * time.Second)

View File

@ -10,7 +10,7 @@ import "ext.proto";
message DeviceConfiguration { message DeviceConfiguration {
bytes device_id = 1 [(ext.goname) = "DeviceID", (ext.xml) = "id,attr", (ext.json) = "deviceID", (ext.device_id) = true, (ext.nodefault) = true]; bytes device_id = 1 [(ext.goname) = "DeviceID", (ext.xml) = "id,attr", (ext.json) = "deviceID", (ext.device_id) = true, (ext.nodefault) = true];
string name = 2 [(ext.xml) = "name,attr,omitempty"]; string name = 2 [(ext.xml) = "name,attr,omitempty"];
repeated string addresses = 3 [(ext.xml) = "address,omitempty", (ext.default) = "dynamic"]; repeated string addresses = 3 [(ext.xml) = "address,omitempty"];
protocol.Compression compression = 4 [(ext.xml) = "compression,attr"]; protocol.Compression compression = 4 [(ext.xml) = "compression,attr"];
string cert_name = 5 [(ext.xml) = "certName,attr,omitempty"]; string cert_name = 5 [(ext.xml) = "certName,attr,omitempty"];
bool introducer = 6 [(ext.xml) = "introducer,attr"]; bool introducer = 6 [(ext.xml) = "introducer,attr"];
@ -26,4 +26,5 @@ message DeviceConfiguration {
int32 max_request_kib = 16 [(ext.goname) = "MaxRequestKiB", (ext.xml) = "maxRequestKiB", (ext.json) = "maxRequestKiB"]; int32 max_request_kib = 16 [(ext.goname) = "MaxRequestKiB", (ext.xml) = "maxRequestKiB", (ext.json) = "maxRequestKiB"];
bool untrusted = 17; bool untrusted = 17;
int32 remote_gui_port = 18 [(ext.goname) = "RemoteGUIPort", (ext.xml) = "remoteGUIPort", (ext.json) = "remoteGUIPort"]; int32 remote_gui_port = 18 [(ext.goname) = "RemoteGUIPort", (ext.xml) = "remoteGUIPort", (ext.json) = "remoteGUIPort"];
int32 num_connections = 19 [(ext.goname) = "RawNumConnections"]; // attempt to establish this many connections to the device
} }

View File

@ -8,9 +8,11 @@ import "repos/protobuf/gogoproto/gogo.proto";
// --- Pre-auth --- // --- Pre-auth ---
message Hello { message Hello {
string device_name = 1; string device_name = 1;
string client_name = 2; string client_name = 2;
string client_version = 3; string client_version = 3;
int32 num_connections = 4;
int64 timestamp = 5;
} }
// --- Header --- // --- Header ---
@ -41,7 +43,8 @@ enum MessageCompression {
// Cluster Config // Cluster Config
message ClusterConfig { message ClusterConfig {
repeated Folder folders = 1; repeated Folder folders = 1;
bool secondary = 2;
} }
message Folder { message Folder {

View File

@ -1,13 +1,17 @@
<configuration version="32"> <configuration version="37">
<folder id="default" label="" path="s1/" type="sendreceive" rescanIntervalS="10" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true"> <folder id="default" label="" path="s1?files=10000" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType> <filesystemType>fake</filesystemType>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy=""></device> <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy="">
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy=""></device> <encryptionPassword></encryptionPassword>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" introducedBy=""></device> </device>
<device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT" introducedBy=""></device> <device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy="">
<encryptionPassword></encryptionPassword>
</device>
<minDiskFree unit="%">1</minDiskFree> <minDiskFree unit="%">1</minDiskFree>
<versioning> <versioning>
<cleanupIntervalS>3600</cleanupIntervalS> <cleanupIntervalS>3600</cleanupIntervalS>
<fsPath></fsPath>
<fsType>basic</fsType>
</versioning> </versioning>
<copiers>1</copiers> <copiers>1</copiers>
<pullerMaxPendingKiB>0</pullerMaxPendingKiB> <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
@ -24,51 +28,21 @@
<markerName>.stfolder</markerName> <markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent> <copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS> <modTimeWindowS>0</modTimeWindowS>
<maxConcurrentWrites>0</maxConcurrentWrites> <maxConcurrentWrites>2</maxConcurrentWrites>
<disableFsync>false</disableFsync> <disableFsync>false</disableFsync>
<blockPullOrder>standard</blockPullOrder> <blockPullOrder>standard</blockPullOrder>
<copyRangeMethod>standard</copyRangeMethod> <copyRangeMethod>standard</copyRangeMethod>
<caseSensitiveFS>false</caseSensitiveFS> <caseSensitiveFS>false</caseSensitiveFS>
<junctionsAsDirs>true</junctionsAsDirs> <junctionsAsDirs>true</junctionsAsDirs>
<syncOwnership>false</syncOwnership>
<sendOwnership>false</sendOwnership>
<syncXattrs>false</syncXattrs>
<sendXattrs>false</sendXattrs>
<xattrFilter>
<maxSingleEntrySize>0</maxSingleEntrySize>
<maxTotalSize>0</maxTotalSize>
</xattrFilter>
</folder> </folder>
<folder id="¯\_(ツ)_/¯ Räksmörgås 动作 Адрес" label="" path="s12-1/" type="sendreceive" rescanIntervalS="10" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy=""></device>
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy=""></device>
<minDiskFree unit="%">1</minDiskFree>
<versioning>
<cleanupIntervalS>3600</cleanupIntervalS>
</versioning>
<copiers>1</copiers>
<pullerMaxPendingKiB>0</pullerMaxPendingKiB>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
<scanProgressIntervalS>0</scanProgressIntervalS>
<pullerPauseS>0</pullerPauseS>
<maxConflicts>-1</maxConflicts>
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS>
<maxConcurrentWrites>0</maxConcurrentWrites>
<disableFsync>false</disableFsync>
<blockPullOrder>standard</blockPullOrder>
<copyRangeMethod>standard</copyRangeMethod>
<caseSensitiveFS>false</caseSensitiveFS>
<junctionsAsDirs>true</junctionsAsDirs>
</folder>
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>tcp://127.0.0.1:22004</address>
<paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB>
</device>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy=""> <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>tcp://127.0.0.1:22001</address> <address>tcp://127.0.0.1:22001</address>
<paused>false</paused> <paused>false</paused>
@ -76,30 +50,21 @@
<maxSendKbps>0</maxSendKbps> <maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps> <maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB> <maxRequestKiB>0</maxRequestKiB>
<untrusted>false</untrusted>
<remoteGUIPort>0</remoteGUIPort>
<numConnections>3</numConnections>
</device> </device>
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" name="s2" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy=""> <device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" name="s2" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>tcp://127.0.0.1:22002</address> <address>tcp://127.0.0.1:22002</address>
<address>quic://127.0.0.1:22002</address>
<paused>false</paused> <paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders> <autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps> <maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps> <maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB> <maxRequestKiB>0</maxRequestKiB>
</device> <untrusted>false</untrusted>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy=""> <remoteGUIPort>0</remoteGUIPort>
<address>tcp://127.0.0.1:22003</address> <numConnections>3</numConnections>
<paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB>
</device>
<device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT" name="s4" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>tcp://127.0.0.1:22004</address>
<paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB>
</device> </device>
<gui enabled="true" tls="false" debugging="true"> <gui enabled="true" tls="false" debugging="true">
<address>127.0.0.1:8081</address> <address>127.0.0.1:8081</address>
@ -111,6 +76,7 @@
<ldap></ldap> <ldap></ldap>
<options> <options>
<listenAddress>tcp://127.0.0.1:22001</listenAddress> <listenAddress>tcp://127.0.0.1:22001</listenAddress>
<listenAddress>quic://127.0.0.1:22001</listenAddress>
<globalAnnounceServer>default</globalAnnounceServer> <globalAnnounceServer>default</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled> <globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled> <localAnnounceEnabled>true</localAnnounceEnabled>
@ -132,7 +98,6 @@
<urURL>https://data.syncthing.net/newdata</urURL> <urURL>https://data.syncthing.net/newdata</urURL>
<urPostInsecurely>false</urPostInsecurely> <urPostInsecurely>false</urPostInsecurely>
<urInitialDelayS>1800</urInitialDelayS> <urInitialDelayS>1800</urInitialDelayS>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH> <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<upgradeToPreReleases>false</upgradeToPreReleases> <upgradeToPreReleases>false</upgradeToPreReleases>
<keepTemporariesH>24</keepTemporariesH> <keepTemporariesH>24</keepTemporariesH>
@ -144,7 +109,6 @@
<overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect> <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
<tempIndexMinBlocks>10</tempIndexMinBlocks> <tempIndexMinBlocks>10</tempIndexMinBlocks>
<trafficClass>0</trafficClass> <trafficClass>0</trafficClass>
<defaultFolderPath>~</defaultFolderPath>
<setLowPriority>true</setLowPriority> <setLowPriority>true</setLowPriority>
<maxFolderConcurrency>0</maxFolderConcurrency> <maxFolderConcurrency>0</maxFolderConcurrency>
<crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL> <crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL>
@ -155,5 +119,70 @@
<databaseTuning>auto</databaseTuning> <databaseTuning>auto</databaseTuning>
<maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB> <maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB>
<announceLANAddresses>true</announceLANAddresses> <announceLANAddresses>true</announceLANAddresses>
<sendFullIndexOnUpgrade>false</sendFullIndexOnUpgrade>
<connectionLimitEnough>0</connectionLimitEnough>
<connectionLimitMax>0</connectionLimitMax>
<insecureAllowOldTLSVersions>false</insecureAllowOldTLSVersions>
<connectionPriorityTcpLan>10</connectionPriorityTcpLan>
<connectionPriorityQuicLan>20</connectionPriorityQuicLan>
<connectionPriorityTcpWan>30</connectionPriorityTcpWan>
<connectionPriorityQuicWan>40</connectionPriorityQuicWan>
<connectionPriorityRelay>50</connectionPriorityRelay>
<connectionPriorityUpgradeThreshold>0</connectionPriorityUpgradeThreshold>
</options> </options>
<defaults>
<folder id="" label="" path="~" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy="">
<encryptionPassword></encryptionPassword>
</device>
<minDiskFree unit="%">1</minDiskFree>
<versioning>
<cleanupIntervalS>3600</cleanupIntervalS>
<fsPath></fsPath>
<fsType>basic</fsType>
</versioning>
<copiers>0</copiers>
<pullerMaxPendingKiB>0</pullerMaxPendingKiB>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
<scanProgressIntervalS>0</scanProgressIntervalS>
<pullerPauseS>0</pullerPauseS>
<maxConflicts>10</maxConflicts>
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS>
<maxConcurrentWrites>2</maxConcurrentWrites>
<disableFsync>false</disableFsync>
<blockPullOrder>standard</blockPullOrder>
<copyRangeMethod>standard</copyRangeMethod>
<caseSensitiveFS>false</caseSensitiveFS>
<junctionsAsDirs>false</junctionsAsDirs>
<syncOwnership>false</syncOwnership>
<sendOwnership>false</sendOwnership>
<syncXattrs>false</syncXattrs>
<sendXattrs>false</sendXattrs>
<xattrFilter>
<maxSingleEntrySize>1024</maxSingleEntrySize>
<maxTotalSize>4096</maxTotalSize>
</xattrFilter>
</folder>
<device id="" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>dynamic</address>
<paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB>
<untrusted>false</untrusted>
<remoteGUIPort>0</remoteGUIPort>
<numConnections>3</numConnections>
</device>
<ignores></ignores>
</defaults>
</configuration> </configuration>

View File

@ -1,14 +1,19 @@
<configuration version="32"> <configuration version="37">
<folder id="default" label="" path="s2" type="sendreceive" rescanIntervalS="15" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true"> <folder id="default" label="" path="s2" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType> <filesystemType>fake</filesystemType>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy=""></device> <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy="">
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy=""></device> <encryptionPassword></encryptionPassword>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" introducedBy=""></device> </device>
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy="">
<encryptionPassword></encryptionPassword>
</device>
<minDiskFree unit="%">1</minDiskFree> <minDiskFree unit="%">1</minDiskFree>
<versioning> <versioning>
<cleanupIntervalS>3600</cleanupIntervalS> <cleanupIntervalS>3600</cleanupIntervalS>
<fsPath></fsPath>
<fsType>basic</fsType>
</versioning> </versioning>
<copiers>1</copiers> <copiers>8</copiers>
<pullerMaxPendingKiB>0</pullerMaxPendingKiB> <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
<hashers>0</hashers> <hashers>0</hashers>
<order>random</order> <order>random</order>
@ -23,80 +28,32 @@
<markerName>.stfolder</markerName> <markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent> <copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS> <modTimeWindowS>0</modTimeWindowS>
<maxConcurrentWrites>0</maxConcurrentWrites> <maxConcurrentWrites>8</maxConcurrentWrites>
<disableFsync>false</disableFsync>
<blockPullOrder>standard</blockPullOrder>
<copyRangeMethod>standard</copyRangeMethod>
<caseSensitiveFS>false</caseSensitiveFS>
<junctionsAsDirs>true</junctionsAsDirs>
</folder>
<folder id="s23" label="" path="s23-2" type="sendreceive" rescanIntervalS="15" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType>
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy=""></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" introducedBy=""></device>
<minDiskFree unit="%">1</minDiskFree>
<versioning>
<cleanupIntervalS>3600</cleanupIntervalS>
</versioning>
<copiers>1</copiers>
<pullerMaxPendingKiB>0</pullerMaxPendingKiB>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
<scanProgressIntervalS>0</scanProgressIntervalS>
<pullerPauseS>0</pullerPauseS>
<maxConflicts>-1</maxConflicts>
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS>
<maxConcurrentWrites>0</maxConcurrentWrites>
<disableFsync>false</disableFsync>
<blockPullOrder>standard</blockPullOrder>
<copyRangeMethod>standard</copyRangeMethod>
<caseSensitiveFS>false</caseSensitiveFS>
<junctionsAsDirs>true</junctionsAsDirs>
</folder>
<folder id="¯\_(ツ)_/¯ Räksmörgås 动作 Адрес" label="" path="s12-2" type="sendreceive" rescanIntervalS="15" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy=""></device>
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy=""></device>
<minDiskFree unit="%">1</minDiskFree>
<versioning>
<cleanupIntervalS>3600</cleanupIntervalS>
</versioning>
<copiers>1</copiers>
<pullerMaxPendingKiB>0</pullerMaxPendingKiB>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
<scanProgressIntervalS>0</scanProgressIntervalS>
<pullerPauseS>0</pullerPauseS>
<maxConflicts>-1</maxConflicts>
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS>
<maxConcurrentWrites>0</maxConcurrentWrites>
<disableFsync>false</disableFsync> <disableFsync>false</disableFsync>
<blockPullOrder>standard</blockPullOrder> <blockPullOrder>standard</blockPullOrder>
<copyRangeMethod>standard</copyRangeMethod> <copyRangeMethod>standard</copyRangeMethod>
<caseSensitiveFS>false</caseSensitiveFS> <caseSensitiveFS>false</caseSensitiveFS>
<junctionsAsDirs>true</junctionsAsDirs> <junctionsAsDirs>true</junctionsAsDirs>
<syncOwnership>false</syncOwnership>
<sendOwnership>false</sendOwnership>
<syncXattrs>false</syncXattrs>
<sendXattrs>false</sendXattrs>
<xattrFilter>
<maxSingleEntrySize>0</maxSingleEntrySize>
<maxTotalSize>0</maxTotalSize>
</xattrFilter>
</folder> </folder>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy=""> <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>tcp://127.0.0.1:22001</address> <address>tcp://127.0.0.1:22001</address>
<address>quic://127.0.0.1:22001</address>
<paused>false</paused> <paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders> <autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps> <maxSendKbps>800</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps> <maxRecvKbps>800</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB> <maxRequestKiB>0</maxRequestKiB>
<untrusted>false</untrusted>
<remoteGUIPort>0</remoteGUIPort>
<numConnections>3</numConnections>
</device> </device>
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" name="s2" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy=""> <device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" name="s2" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>tcp://127.0.0.1:22002</address> <address>tcp://127.0.0.1:22002</address>
@ -105,14 +62,9 @@
<maxSendKbps>0</maxSendKbps> <maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps> <maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB> <maxRequestKiB>0</maxRequestKiB>
</device> <untrusted>false</untrusted>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy=""> <remoteGUIPort>0</remoteGUIPort>
<address>tcp://127.0.0.1:22003</address> <numConnections>3</numConnections>
<paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB>
</device> </device>
<gui enabled="true" tls="false" debugging="true"> <gui enabled="true" tls="false" debugging="true">
<address>127.0.0.1:8082</address> <address>127.0.0.1:8082</address>
@ -121,15 +73,15 @@
</gui> </gui>
<ldap></ldap> <ldap></ldap>
<options> <options>
<listenAddress>dynamic+https://relays.syncthing.net/endpoint</listenAddress>
<listenAddress>tcp://127.0.0.1:22002</listenAddress> <listenAddress>tcp://127.0.0.1:22002</listenAddress>
<listenAddress>quic://127.0.0.1:22002</listenAddress>
<globalAnnounceServer>default</globalAnnounceServer> <globalAnnounceServer>default</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled> <globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled> <localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21027</localAnnouncePort> <localAnnouncePort>21027</localAnnouncePort>
<localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr> <localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
<maxSendKbps>0</maxSendKbps> <maxSendKbps>1000</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps> <maxRecvKbps>1000</maxRecvKbps>
<reconnectionIntervalS>5</reconnectionIntervalS> <reconnectionIntervalS>5</reconnectionIntervalS>
<relaysEnabled>true</relaysEnabled> <relaysEnabled>true</relaysEnabled>
<relayReconnectIntervalM>10</relayReconnectIntervalM> <relayReconnectIntervalM>10</relayReconnectIntervalM>
@ -144,19 +96,17 @@
<urURL>https://data.syncthing.net/newdata</urURL> <urURL>https://data.syncthing.net/newdata</urURL>
<urPostInsecurely>false</urPostInsecurely> <urPostInsecurely>false</urPostInsecurely>
<urInitialDelayS>1800</urInitialDelayS> <urInitialDelayS>1800</urInitialDelayS>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH> <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<upgradeToPreReleases>false</upgradeToPreReleases> <upgradeToPreReleases>false</upgradeToPreReleases>
<keepTemporariesH>24</keepTemporariesH> <keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>false</cacheIgnoredFiles> <cacheIgnoredFiles>false</cacheIgnoredFiles>
<progressUpdateIntervalS>5</progressUpdateIntervalS> <progressUpdateIntervalS>5</progressUpdateIntervalS>
<limitBandwidthInLan>false</limitBandwidthInLan> <limitBandwidthInLan>true</limitBandwidthInLan>
<minHomeDiskFree unit="%">1</minHomeDiskFree> <minHomeDiskFree unit="%">1</minHomeDiskFree>
<releasesURL>https://upgrades.syncthing.net/meta.json</releasesURL> <releasesURL>https://upgrades.syncthing.net/meta.json</releasesURL>
<overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect> <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
<tempIndexMinBlocks>10</tempIndexMinBlocks> <tempIndexMinBlocks>10</tempIndexMinBlocks>
<trafficClass>0</trafficClass> <trafficClass>0</trafficClass>
<defaultFolderPath>~</defaultFolderPath>
<setLowPriority>true</setLowPriority> <setLowPriority>true</setLowPriority>
<maxFolderConcurrency>0</maxFolderConcurrency> <maxFolderConcurrency>0</maxFolderConcurrency>
<crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL> <crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL>
@ -167,5 +117,70 @@
<databaseTuning>auto</databaseTuning> <databaseTuning>auto</databaseTuning>
<maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB> <maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB>
<announceLANAddresses>true</announceLANAddresses> <announceLANAddresses>true</announceLANAddresses>
<sendFullIndexOnUpgrade>false</sendFullIndexOnUpgrade>
<connectionLimitEnough>0</connectionLimitEnough>
<connectionLimitMax>0</connectionLimitMax>
<insecureAllowOldTLSVersions>false</insecureAllowOldTLSVersions>
<connectionPriorityTcpLan>10</connectionPriorityTcpLan>
<connectionPriorityQuicLan>20</connectionPriorityQuicLan>
<connectionPriorityTcpWan>30</connectionPriorityTcpWan>
<connectionPriorityQuicWan>40</connectionPriorityQuicWan>
<connectionPriorityRelay>50</connectionPriorityRelay>
<connectionPriorityUpgradeThreshold>0</connectionPriorityUpgradeThreshold>
</options> </options>
<defaults>
<folder id="" label="" path="~" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType>
<device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy="">
<encryptionPassword></encryptionPassword>
</device>
<minDiskFree unit="%">1</minDiskFree>
<versioning>
<cleanupIntervalS>3600</cleanupIntervalS>
<fsPath></fsPath>
<fsType>basic</fsType>
</versioning>
<copiers>0</copiers>
<pullerMaxPendingKiB>0</pullerMaxPendingKiB>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
<scanProgressIntervalS>0</scanProgressIntervalS>
<pullerPauseS>0</pullerPauseS>
<maxConflicts>10</maxConflicts>
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS>
<maxConcurrentWrites>2</maxConcurrentWrites>
<disableFsync>false</disableFsync>
<blockPullOrder>standard</blockPullOrder>
<copyRangeMethod>standard</copyRangeMethod>
<caseSensitiveFS>false</caseSensitiveFS>
<junctionsAsDirs>false</junctionsAsDirs>
<syncOwnership>false</syncOwnership>
<sendOwnership>false</sendOwnership>
<syncXattrs>false</syncXattrs>
<sendXattrs>false</sendXattrs>
<xattrFilter>
<maxSingleEntrySize>1024</maxSingleEntrySize>
<maxTotalSize>4096</maxTotalSize>
</xattrFilter>
</folder>
<device id="" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>dynamic</address>
<paused>false</paused>
<autoAcceptFolders>false</autoAcceptFolders>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<maxRequestKiB>0</maxRequestKiB>
<untrusted>false</untrusted>
<remoteGUIPort>0</remoteGUIPort>
<numConnections>3</numConnections>
</device>
<ignores></ignores>
</defaults>
</configuration> </configuration>