Repository: syncthing/syncthing (language: go)

Issue:
Web GUI loses config changes when doing multiple modifications (e.g. on slow hardware or remotely)
I assume this problem has affected the Web GUI since basically forever, or at least I myself have always had this issue when operating it. Basically, if you do multiple modifications quickly where each of them is supposed to save changes to the config, some of them will likely be lost, especially when dealing with slow hardware or slow connections (e.g. when operating the GUI remotely).

To reproduce (on slow hardware or a remote connection):

1. Edit multiple folders quickly one after another (e.g. by changing their folder type to something else).
2. Wait for the GUI to become responsive again.
3. It is very likely that only some of the folders will have their type changed.
4. The rest of the modifications have been lost and need to be re-done.

I think a solution to this could be to simply force the GUI not to accept any input before actually saving the previous modifications to the config. One method to achieve this may be to show a "saving changes" pop-up or similar so that the user would know that something is happening while also being prevented from operating the GUI. Of course, this method isn't flawless as it's possible to have the same GUI open in multiple browser tabs and try to make changes from there, etc. but still I believe that it would be much better than the current state where modifications can be lost very easily.

I may try to do something about this problem myself, but of course, if someone more skilled is willing to do work in this direction, it would be very welcome too.

Issues related to this problem: https://github.com/syncthing/syncthing/issues/8589 (and possibly others, I will add them here if I find any)
Consider multi-connection support to transfer files
To synchronize 2 sites which are geographically distributed Syncthing has to pass through many hops. Single TCP (or any other) connection which is used to transfer data is _very limited in bandwidth by magistral or local providers_.  
It usually happens because traffic exchange points are heavily loaded, traffic shaped, or networks misconfiguration etc.  
It is easy to check via [speedtest.net](https://www.speedtest.net/) by selection of single/multi mode and some server in another country (or even continent).

For example, connecting from Europe to USA in **single connection**:

![image](https://user-images.githubusercontent.com/4306334/209620795-a98c2fc7-de3e-4525-ae02-7cbe2d5d0925.png)

And the same setting, except **multi-connections**:

![image](https://user-images.githubusercontent.com/4306334/209620862-ca2307b4-39a3-4940-8731-a48e47299e06.png)

In my specific case situation even much worse, it results that on 200 MBit/s connection Syncthing exchanges data in peak hours with speed 100 KB/s, and up to 2 MB/s at night. In comparison _aria2c_ with 100 connections even in worst cases can utilize full bandwidth and downloads file with speed 22-24 MB/s.  
Increasing of speed is dramatic... Even 2 MB/s is very slow but 100 KB/s makes Syncthing unusable for scenarios with geo-distributed sites and a lot of hops.

So it is crucial thing for Syncthing to **support multi-connections** like it is made for example in aria2c.



Discussion / hints:

Can you test QUIC? It should work noticeably better on high latency connections.
Duplicate of #8184

In short, not right now.
> Can you test QUIC? It should work noticeably better on high latency connections.

Just tested - no difference. And that is expected, as I said the reason is not high latency.

Fix patch:
diff --git a/cmd/stdiscosrv/database.go b/cmd/stdiscosrv/database.go
index ce151da3c21..92ed45db6a4 100644
--- a/cmd/stdiscosrv/database.go
+++ b/cmd/stdiscosrv/database.go
@@ -15,6 +15,7 @@ import (
 	"sort"
 	"time"
 
+	"github.com/syncthing/syncthing/lib/sliceutil"
 	"github.com/syndtr/goleveldb/leveldb"
 	"github.com/syndtr/goleveldb/leveldb/storage"
 	"github.com/syndtr/goleveldb/leveldb/util"
@@ -352,14 +353,7 @@ func expire(addrs []DatabaseAddress, now int64) []DatabaseAddress {
 	i := 0
 	for i < len(addrs) {
 		if addrs[i].Expires < now {
-			// This item is expired. Replace it with the last in the list
-			// (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]
+			addrs = sliceutil.RemoveAndZero(addrs, i)
 			continue
 		}
 		i++
diff --git a/gui/default/index.html b/gui/default/index.html
index e9691a02bf9..5b151cb0026 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -871,6 +871,13 @@ <h4 class="panel-title">
                           </span>
                         </td>
                       </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">
                         <th><span class="fas fa-fw fa-filter"></span>&nbsp;<span translate>Allowed Networks</span></th>
                         <td class="text-right">
diff --git a/gui/default/syncthing/device/editDeviceModalView.html b/gui/default/syncthing/device/editDeviceModalView.html
index 39d037e0470..160cb0f217e 100644
--- a/gui/default/syncthing/device/editDeviceModalView.html
+++ b/gui/default/syncthing/device/editDeviceModalView.html
@@ -137,11 +137,22 @@
               </div>
             </div>
           </div>
-          <div class="row form-group">
-            <div class="col-md-12">
+          <div class="row">
+            <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>
               <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">
                     <span class="col-md-8" translate>Incoming Rate Limit (KiB/s)</span>
                     <div class="col-md-4">
@@ -150,7 +161,7 @@
                   </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>
                 </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">
                     <span class="col-md-8" translate>Outgoing Rate Limit (KiB/s)</span>
                     <div class="col-md-4">
@@ -158,6 +169,7 @@
                     </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.$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>
diff --git a/lib/api/api.go b/lib/api/api.go
index 363c6a162a2..67ebf0fcee5 100644
--- a/lib/api/api.go
+++ b/lib/api/api.go
@@ -1230,6 +1230,14 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
 	promhttp.Handler().ServeHTTP(wr, &http.Request{Method: http.MethodGet})
 	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
 	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
@@ -1607,7 +1615,7 @@ func (s *service) getPeerCompletion(w http.ResponseWriter, _ *http.Request) {
 	for _, folder := range s.cfg.Folders() {
 		for _, device := range folder.DeviceIDs() {
 			deviceStr := device.String()
-			if _, ok := s.model.Connection(device); ok {
+			if s.model.ConnectedTo(device) {
 				comp, err := s.model.Completion(device, folder.ID)
 				if err != nil {
 					http.Error(w, err.Error(), http.StatusInternalServerError)
diff --git a/lib/config/config.go b/lib/config/config.go
index 4f02085fce3..62f2b278b42 100644
--- a/lib/config/config.go
+++ b/lib/config/config.go
@@ -25,6 +25,7 @@ import (
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/netutil"
 	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/sliceutil"
 	"github.com/syncthing/syncthing/lib/structutil"
 )
 
@@ -564,8 +565,7 @@ func ensureNoUntrustedTrustingSharing(f *FolderConfiguration, devices []FolderDe
 		}
 		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)
-			copy(devices[i:], devices[i+1:])
-			devices = devices[:len(devices)-1]
+			devices = sliceutil.RemoveAndZero(devices, i)
 			i--
 		}
 	}
@@ -601,9 +601,7 @@ func filterURLSchemePrefix(addrs []string, prefix string) []string {
 			continue
 		}
 		if strings.HasPrefix(uri.Scheme, prefix) {
-			// Remove this entry
-			copy(addrs[i:], addrs[i+1:])
-			addrs = addrs[:len(addrs)-1]
+			addrs = sliceutil.RemoveAndZero(addrs, i)
 			i--
 		}
 	}
diff --git a/lib/config/deviceconfiguration.go b/lib/config/deviceconfiguration.go
index 54a30affc44..8635bcc39cf 100644
--- a/lib/config/deviceconfiguration.go
+++ b/lib/config/deviceconfiguration.go
@@ -11,6 +11,8 @@ import (
 	"sort"
 )
 
+const defaultNumConnections = 1 // number of connections to use by default; may change in the future.
+
 func (cfg DeviceConfiguration) Copy() DeviceConfiguration {
 	c := cfg
 	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 {
 	for _, ignoredFolder := range cfg.IgnoredFolders {
 		if ignoredFolder.ID == folder {
diff --git a/lib/config/deviceconfiguration.pb.go b/lib/config/deviceconfiguration.pb.go
index e07bb1eb02e..1f5d2a1a22a 100644
--- a/lib/config/deviceconfiguration.pb.go
+++ b/lib/config/deviceconfiguration.pb.go
@@ -28,7 +28,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 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"`
 	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"`
 	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"`
@@ -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"`
 	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"`
+	RawNumConnections        int                                                  `protobuf:"varint,19,opt,name=num_connections,json=numConnections,proto3,casttype=int" json:"numConnections" xml:"numConnections"`
 }
 
 func (m *DeviceConfiguration) Reset()         { *m = DeviceConfiguration{} }
@@ -88,72 +89,74 @@ func init() {
 }
 
 var fileDescriptor_744b782bd13071dd = []byte{
-	// 1026 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xbf, 0x6f, 0xdb, 0x46,
-	0x18, 0x15, 0xeb, 0xc4, 0xb6, 0xce, 0x3f, 0x64, 0xd3, 0x88, 0xc3, 0x18, 0x88, 0x4e, 0x50, 0x35,
-	0x28, 0x68, 0x22, 0x17, 0x6e, 0x27, 0xa3, 0x2d, 0x50, 0xc6, 0x68, 0x63, 0x18, 0x4d, 0x5c, 0x16,
-	0x5d, 0xbc, 0xb0, 0x24, 0xef, 0xac, 0x1c, 0x2c, 0xf2, 0x58, 0xf2, 0xa8, 0x58, 0x40, 0xff, 0x80,
-	0x76, 0x2b, 0x02, 0x74, 0xea, 0x92, 0xf6, 0xdf, 0xe8, 0xd0, 0xd5, 0x9b, 0x35, 0x16, 0x1d, 0x0e,
-	0x88, 0xbd, 0x71, 0x29, 0xc0, 0x31, 0x53, 0x71, 0x77, 0x14, 0x45, 0xca, 0x51, 0x50, 0xa0, 0x1b,
-	0xef, 0xbd, 0x77, 0xef, 0xdd, 0xf7, 0xe9, 0xbb, 0x13, 0xe8, 0x0c, 0x88, 0xbb, 0xeb, 0xd1, 0xe0,
-	0x94, 0xf4, 0x77, 0x11, 0x1e, 0x12, 0x0f, 0xab, 0x45, 0x12, 0x39, 0x8c, 0xd0, 0xa0, 0x17, 0x46,
-	0x94, 0x51, 0x7d, 0x51, 0x81, 0x3b, 0xdb, 0x42, 0x2d, 0x21, 0x8f, 0x0e, 0x76, 0x5d, 0x1c, 0x2a,
-	0x7e, 0xe7, 0x5e, 0xc9, 0x85, 0xba, 0x31, 0x8e, 0x86, 0x18, 0xe5, 0x54, 0x1d, 0x9f, 0x33, 0xf5,
-	0xd9, 0xfe, 0x67, 0x03, 0x6c, 0x1d, 0xc8, 0x8c, 0xc7, 0xe5, 0x0c, 0xfd, 0x4f, 0x0d, 0xd4, 0x55,
-	0xb6, 0x4d, 0x90, 0xa1, 0xb5, 0xb4, 0xee, 0xaa, 0xf9, 0x9b, 0x76, 0xc1, 0x61, 0xed, 0x6f, 0x0e,
-	0x3f, 0xee, 0x13, 0xf6, 0x3c, 0x71, 0x7b, 0x1e, 0xf5, 0x77, 0xe3, 0x51, 0xe0, 0xb1, 0xe7, 0x24,
-	0xe8, 0x97, 0xbe, 0xca, 0x27, 0xea, 0x29, 0xf7, 0xc3, 0x83, 0x2b, 0x0e, 0x97, 0x27, 0xdf, 0x29,
-	0x87, 0xcb, 0x28, 0xff, 0xce, 0x38, 0x6c, 0x9e, 0xfb, 0x83, 0xfd, 0x36, 0x41, 0x0f, 0x1d, 0xc6,
-	0xa2, 0x76, 0x2b, 0xa0, 0x08, 0x9f, 0x3a, 0xc9, 0x80, 0xed, 0xb7, 0x59, 0x94, 0xe0, 0x76, 0x7a,
-	0xd9, 0x59, 0xca, 0xc9, 0xec, 0xb2, 0x53, 0x6c, 0xfc, 0x71, 0xdc, 0xd1, 0x5e, 0x8e, 0x3b, 0x85,
-	0xe9, 0xab, 0x71, 0x47, 0xb3, 0x26, 0x2c, 0xd2, 0x8f, 0xc1, 0xad, 0xc0, 0xf1, 0xb1, 0xf1, 0x5e,
-	0x4b, 0xeb, 0xd6, 0xcd, 0x4f, 0x52, 0x0e, 0xe5, 0x3a, 0xe3, 0xf0, 0x9e, 0x8c, 0x13, 0x0b, 0xe9,
-	0xf9, 0x90, 0xfa, 0x84, 0x61, 0x3f, 0x64, 0x23, 0x91, 0xb4, 0xf5, 0x16, 0xdc, 0x92, 0x3b, 0xf5,
-	0x73, 0x50, 0x77, 0x10, 0x8a, 0x70, 0x1c, 0xe3, 0xd8, 0x58, 0x68, 0x2d, 0x74, 0xeb, 0xe6, 0x49,
-	0xca, 0xe1, 0x14, 0xcc, 0x38, 0x7c, 0x20, 0xbd, 0x73, 0xa4, 0xe4, 0xdc, 0x2a, 0x4a, 0x42, 0xa3,
-	0xc0, 0xf1, 0x89, 0x27, 0xb2, 0x36, 0x6f, 0xe8, 0xde, 0x5c, 0x76, 0x96, 0x72, 0x81, 0x35, 0xf5,
-	0xd5, 0x87, 0x60, 0xc5, 0xa3, 0x7e, 0x28, 0x56, 0x84, 0x06, 0xc6, 0xad, 0x96, 0xd6, 0x5d, 0xdf,
-	0xbb, 0xd3, 0x2b, 0x7a, 0xfc, 0x78, 0x4a, 0x9a, 0x9f, 0xa6, 0x1c, 0x96, 0xd5, 0x19, 0x87, 0xdb,
-	0xf2, 0x50, 0x25, 0x4c, 0x35, 0x3a, 0xbd, 0xec, 0x6c, 0xcc, 0x82, 0x56, 0x79, 0xab, 0x8e, 0x41,
-	0xdd, 0xc3, 0x11, 0xb3, 0x65, 0x23, 0x6f, 0xcb, 0x46, 0x3e, 0x11, 0xbf, 0x9d, 0x00, 0x9f, 0xaa,
-	0x66, 0xde, 0x57, 0xde, 0x39, 0xf0, 0x96, 0x86, 0xde, 0x9d, 0xc3, 0x59, 0x85, 0x8b, 0x7e, 0x02,
-	0x00, 0x09, 0x58, 0x44, 0x51, 0xe2, 0xe1, 0xc8, 0x58, 0x6c, 0x69, 0xdd, 0x65, 0x73, 0x3f, 0xe5,
-	0xb0, 0x84, 0x66, 0x1c, 0xde, 0x51, 0x53, 0x52, 0x40, 0x45, 0x11, 0x8d, 0x19, 0xcc, 0x2a, 0xed,
-	0xd3, 0x7f, 0xd7, 0xc0, 0x4e, 0x7c, 0x46, 0x42, 0x7b, 0x82, 0x89, 0xf1, 0xb6, 0x23, 0xec, 0xd3,
-	0xa1, 0x33, 0x88, 0x8d, 0x25, 0x19, 0x86, 0x52, 0x0e, 0x0d, 0xa1, 0x3a, 0x2c, 0x89, 0xac, 0x5c,
-	0x93, 0x71, 0xf8, 0xbe, 0x8c, 0x9e, 0x27, 0x28, 0x0e, 0x72, 0xff, 0x9d, 0x0a, 0x6b, 0x6e, 0x82,
-	0xfe, 0x87, 0x06, 0xd6, 0x8a, 0x33, 0x23, 0xdb, 0x1d, 0x19, 0xcb, 0xf2, 0xc6, 0xfd, 0xf2, 0xbf,
-	0x6e, 0x5c, 0xca, 0xe1, 0xea, 0xd4, 0xd5, 0x1c, 0x65, 0x1c, 0x76, 0xab, 0x3d, 0x44, 0xe6, 0x68,
-	0xfe, 0x9d, 0xdb, 0xbc, 0x21, 0x13, 0x37, 0x4e, 0xde, 0xb2, 0x8a, 0xad, 0xbe, 0x07, 0x16, 0x43,
-	0x27, 0x89, 0x31, 0x32, 0xea, 0xb2, 0x9b, 0x3b, 0x29, 0x87, 0x39, 0x92, 0x71, 0xb8, 0x2a, 0x23,
-	0xd5, 0xb2, 0x6d, 0xe5, 0xb8, 0xfe, 0x03, 0xd8, 0x70, 0x06, 0x03, 0xfa, 0x02, 0x23, 0x3b, 0xc0,
-	0xec, 0x05, 0x8d, 0xce, 0x62, 0x03, 0xc8, 0x2b, 0xf5, 0x75, 0xca, 0x61, 0x23, 0xe7, 0x9e, 0xe6,
-	0x54, 0xf1, 0x46, 0x54, 0xf1, 0xea, 0xa0, 0x19, 0xf3, 0x48, 0x6b, 0xd6, 0x4e, 0xff, 0x0e, 0x6c,
-	0x39, 0x09, 0xa3, 0xb6, 0xe3, 0x79, 0x38, 0x64, 0xf6, 0x29, 0x1d, 0x20, 0x1c, 0xc5, 0xc6, 0x8a,
-	0x3c, 0xfe, 0x87, 0x29, 0x87, 0x9b, 0x82, 0xfe, 0x5c, 0xb2, 0x5f, 0x28, 0x32, 0xe3, 0xf0, 0xae,
-	0x3a, 0xc2, 0x2c, 0xd3, 0xb6, 0x6e, 0xaa, 0xf5, 0x67, 0x60, 0xcd, 0x77, 0xce, 0xed, 0x18, 0x07,
-	0xc8, 0x3e, 0x73, 0xc3, 0xd8, 0x58, 0x6d, 0x69, 0xdd, 0xdb, 0xe6, 0x07, 0xe2, 0x72, 0xfa, 0xce,
-	0xf9, 0x37, 0x38, 0x40, 0x47, 0x6e, 0x28, 0x5c, 0x37, 0xa5, 0x6b, 0x09, 0x6b, 0xbf, 0xe1, 0x70,
-	0x81, 0x04, 0xcc, 0x2a, 0x0b, 0x27, 0x86, 0x11, 0xf6, 0x86, 0xca, 0x70, 0xad, 0x62, 0x68, 0x61,
-	0x6f, 0x38, 0x6b, 0x38, 0xc1, 0x2a, 0x86, 0x13, 0x50, 0x0f, 0x40, 0x83, 0xf4, 0x03, 0x1a, 0x61,
-	0x54, 0xd4, 0xbf, 0xde, 0x5a, 0xe8, 0xae, 0xec, 0x6d, 0xf7, 0xd4, 0xbf, 0x46, 0xef, 0x59, 0xfe,
-	0xaf, 0xa1, 0x6a, 0x32, 0x1f, 0x89, 0x59, 0x4c, 0x39, 0x5c, 0xcf, 0xb7, 0x4d, 0x1b, 0xb3, 0xa5,
-	0xa6, 0xaa, 0x0c, 0xb7, 0xad, 0x19, 0x99, 0xfe, 0x93, 0x06, 0x1a, 0x21, 0x0e, 0x10, 0x09, 0xfa,
-	0x45, 0x60, 0xe3, 0x9d, 0x81, 0x4f, 0x44, 0xe0, 0x15, 0x87, 0xc6, 0x01, 0x0e, 0x23, 0xec, 0x39,
-	0x0c, 0xa3, 0x63, 0x65, 0x90, 0x7b, 0xa6, 0x1c, 0x6a, 0x8f, 0x8a, 0x37, 0x28, 0x2c, 0x73, 0xa5,
-	0xd1, 0x30, 0x34, 0x6b, 0xbd, 0xc2, 0xc5, 0xfa, 0xaf, 0x1a, 0x68, 0xa8, 0x6e, 0x7e, 0x9f, 0xe0,
-	0x98, 0xd9, 0x67, 0xc4, 0x35, 0x36, 0x64, 0x3f, 0xe3, 0x2b, 0x0e, 0xd7, 0xbe, 0x12, 0x6d, 0x92,
-	0xcc, 0x11, 0x31, 0x53, 0x0e, 0xd7, 0xfc, 0x32, 0x50, 0x14, 0x5c, 0x41, 0x27, 0x4d, 0x4e, 0x2f,
-	0x3b, 0x33, 0xf2, 0x59, 0xe0, 0xe5, 0xb8, 0x53, 0x4d, 0xb0, 0x2a, 0xbc, 0xab, 0x7f, 0x06, 0xea,
-	0x49, 0xc0, 0xa2, 0x24, 0x66, 0x18, 0x19, 0x9b, 0x72, 0x26, 0x5b, 0xe2, 0x7f, 0xa6, 0x00, 0x33,
-	0x0e, 0x1b, 0xf2, 0x04, 0x05, 0xd2, 0xb6, 0xa6, 0xac, 0xac, 0x4e, 0x3c, 0x70, 0x0c, 0xdb, 0xfd,
-	0x84, 0xd8, 0x21, 0x8d, 0x98, 0xa1, 0x4f, 0xab, 0xb3, 0x24, 0xf5, 0xe5, 0xb7, 0x87, 0xc7, 0x34,
-	0x62, 0xa2, 0xba, 0xa8, 0x0c, 0x14, 0xd5, 0x55, 0xd0, 0x72, 0x75, 0x55, 0xf9, 0x2c, 0x20, 0xaa,
-	0xab, 0x24, 0x58, 0x13, 0x3e, 0x21, 0x62, 0x69, 0x1e, 0x5d, 0xbc, 0x6e, 0xd6, 0xc6, 0xaf, 0x9b,
-	0xb5, 0x8b, 0xab, 0xa6, 0x36, 0xbe, 0x6a, 0x6a, 0x3f, 0x5f, 0x37, 0x6b, 0xaf, 0xae, 0x9b, 0xda,
-	0xf8, 0xba, 0x59, 0xfb, 0xeb, 0xba, 0x59, 0x3b, 0x79, 0xf0, 0x1f, 0x1e, 0x3b, 0x35, 0x31, 0xee,
-	0xa2, 0x7c, 0xf4, 0x3e, 0xfa, 0x37, 0x00, 0x00, 0xff, 0xff, 0xbf, 0x4a, 0x4f, 0x60, 0x33, 0x09,
-	0x00, 0x00,
+	// 1057 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0x41, 0x6f, 0xe3, 0x44,
+	0x14, 0x8e, 0xe9, 0x6e, 0xb7, 0x99, 0x6d, 0x9b, 0xc6, 0x65, 0xbb, 0xde, 0x4a, 0x9b, 0x89, 0x42,
+	0x0e, 0x41, 0xec, 0xa6, 0xa8, 0x70, 0xaa, 0x00, 0x89, 0xb4, 0x82, 0xad, 0x2a, 0xba, 0x65, 0x10,
+	0x97, 0xdd, 0x83, 0x71, 0x3c, 0xd3, 0xac, 0xd5, 0x78, 0xc6, 0xd8, 0xe3, 0xb4, 0x95, 0x38, 0x72,
+	0x80, 0x1b, 0xaa, 0xc4, 0x89, 0xcb, 0xc2, 0xdf, 0xe0, 0xc0, 0xb5, 0xb7, 0xe6, 0x08, 0x1c, 0x46,
+	0xda, 0xf4, 0xe6, 0xa3, 0x8f, 0x9c, 0xd0, 0x8c, 0x1d, 0xc7, 0x76, 0x37, 0x2b, 0x24, 0x6e, 0x9e,
+	0xef, 0x7b, 0xf3, 0x7d, 0xf3, 0x9e, 0xdf, 0x9b, 0x01, 0xed, 0xa1, 0xd3, 0xdf, 0xb2, 0x19, 0x3d,
+	0x76, 0x06, 0x5b, 0x98, 0x8c, 0x1c, 0x9b, 0x24, 0x8b, 0xd0, 0xb7, 0xb8, 0xc3, 0x68, 0xd7, 0xf3,
+	0x19, 0x67, 0xfa, 0x62, 0x02, 0x6e, 0x6e, 0xc8, 0x68, 0x05, 0xd9, 0x6c, 0xb8, 0xd5, 0x27, 0x5e,
+	0xc2, 0x6f, 0x3e, 0xc8, 0xa9, 0xb0, 0x7e, 0x40, 0xfc, 0x11, 0xc1, 0x29, 0x55, 0x25, 0x67, 0x3c,
+	0xf9, 0x6c, 0xfd, 0x55, 0x07, 0xeb, 0x7b, 0xca, 0x63, 0x37, 0xef, 0xa1, 0xff, 0xa1, 0x81, 0x6a,
+	0xe2, 0x6d, 0x3a, 0xd8, 0xd0, 0x9a, 0x5a, 0x67, 0xb9, 0xf7, 0xab, 0x76, 0x29, 0x60, 0xe5, 0x6f,
+	0x01, 0x3f, 0x1c, 0x38, 0xfc, 0x45, 0xd8, 0xef, 0xda, 0xcc, 0xdd, 0x0a, 0xce, 0xa9, 0xcd, 0x5f,
+	0x38, 0x74, 0x90, 0xfb, 0xca, 0x9f, 0xa8, 0x9b, 0xa8, 0xef, 0xef, 0x4d, 0x04, 0x5c, 0x9a, 0x7e,
+	0x47, 0x02, 0x2e, 0xe1, 0xf4, 0x3b, 0x16, 0xb0, 0x71, 0xe6, 0x0e, 0x77, 0x5a, 0x0e, 0x7e, 0x64,
+	0x71, 0xee, 0xb7, 0x9a, 0x94, 0x61, 0x72, 0x6c, 0x85, 0x43, 0xbe, 0xd3, 0xe2, 0x7e, 0x48, 0x5a,
+	0xd1, 0x55, 0xfb, 0x4e, 0x4a, 0xc6, 0x57, 0xed, 0x6c, 0xe3, 0x0f, 0xe3, 0xb6, 0x76, 0x31, 0x6e,
+	0x67, 0xa2, 0x2f, 0xc7, 0x6d, 0x0d, 0x4d, 0x59, 0xac, 0x1f, 0x81, 0x5b, 0xd4, 0x72, 0x89, 0xf1,
+	0x56, 0x53, 0xeb, 0x54, 0x7b, 0x1f, 0x45, 0x02, 0xaa, 0x75, 0x2c, 0xe0, 0x03, 0x65, 0x27, 0x17,
+	0x4a, 0xf3, 0x11, 0x73, 0x1d, 0x4e, 0x5c, 0x8f, 0x9f, 0x4b, 0xa7, 0xf5, 0xd7, 0xe0, 0x48, 0xed,
+	0xd4, 0x9f, 0x83, 0xaa, 0x85, 0xb1, 0x4f, 0x82, 0x80, 0x04, 0xc6, 0x42, 0x73, 0xa1, 0x53, 0xed,
+	0x7d, 0x1c, 0x09, 0x38, 0x03, 0x63, 0x01, 0xef, 0x2b, 0xed, 0x14, 0x29, 0x2a, 0xd7, 0x6f, 0xa0,
+	0x68, 0xb6, 0x55, 0x1f, 0x81, 0xbb, 0x36, 0x73, 0x3d, 0xb9, 0x72, 0x18, 0x35, 0x6e, 0x35, 0xb5,
+	0xce, 0xea, 0xf6, 0xbd, 0x6e, 0x56, 0xc6, 0xdd, 0x19, 0xa9, 0x5c, 0xf3, 0xd1, 0xb1, 0x80, 0x1b,
+	0xca, 0x37, 0x87, 0x25, 0xb5, 0x8c, 0xae, 0xda, 0x6b, 0x65, 0x10, 0xe5, 0xb7, 0xea, 0x04, 0x54,
+	0x6d, 0xe2, 0x73, 0x53, 0xd5, 0xea, 0xb6, 0xaa, 0xd5, 0x13, 0xf9, 0x7b, 0x24, 0x78, 0x98, 0xd4,
+	0xeb, 0x61, 0xa2, 0x9d, 0x02, 0xaf, 0xa9, 0xd9, 0xfd, 0x39, 0x1c, 0xca, 0x54, 0xf4, 0x67, 0x00,
+	0x38, 0x94, 0xfb, 0x0c, 0x87, 0x36, 0xf1, 0x8d, 0xc5, 0xa6, 0xd6, 0x59, 0xea, 0xed, 0x44, 0x02,
+	0xe6, 0xd0, 0x58, 0xc0, 0x7b, 0x49, 0x23, 0x64, 0x50, 0x96, 0x44, 0xad, 0x84, 0xa1, 0xdc, 0x3e,
+	0xfd, 0x37, 0x0d, 0x6c, 0x06, 0x27, 0x8e, 0x67, 0x4e, 0x31, 0xd9, 0xc1, 0xa6, 0x4f, 0x5c, 0x36,
+	0xb2, 0x86, 0x81, 0x71, 0x47, 0x99, 0xe1, 0x48, 0x40, 0x43, 0x46, 0xed, 0xe7, 0x82, 0x50, 0x1a,
+	0x13, 0x0b, 0xf8, 0x8e, 0xb2, 0x9e, 0x17, 0x90, 0x1d, 0xe4, 0xe1, 0x1b, 0x23, 0xd0, 0x5c, 0x07,
+	0xfd, 0x77, 0x0d, 0xac, 0x64, 0x67, 0xc6, 0x66, 0xff, 0xdc, 0x58, 0x52, 0x43, 0xf5, 0xf3, 0xff,
+	0x1a, 0xaa, 0x48, 0xc0, 0xe5, 0x99, 0x6a, 0xef, 0x3c, 0x16, 0xb0, 0x53, 0xac, 0x21, 0xee, 0x9d,
+	0xcf, 0x1f, 0xab, 0xfa, 0x8d, 0x30, 0x39, 0x54, 0x6a, 0x90, 0x0a, 0xb2, 0xfa, 0x36, 0x58, 0xf4,
+	0xac, 0x30, 0x20, 0xd8, 0xa8, 0xaa, 0x6a, 0x6e, 0x46, 0x02, 0xa6, 0x48, 0x2c, 0xe0, 0xb2, 0xb2,
+	0x4c, 0x96, 0x2d, 0x94, 0xe2, 0xfa, 0x77, 0x60, 0xcd, 0x1a, 0x0e, 0xd9, 0x29, 0xc1, 0x26, 0x25,
+	0xfc, 0x94, 0xf9, 0x27, 0x81, 0x01, 0xd4, 0xd4, 0x7c, 0x19, 0x09, 0x58, 0x4b, 0xb9, 0xc3, 0x94,
+	0xca, 0xae, 0x81, 0x22, 0x5e, 0x6c, 0x34, 0x63, 0x1e, 0x89, 0xca, 0x72, 0xfa, 0x37, 0x60, 0xdd,
+	0x0a, 0x39, 0x33, 0x2d, 0xdb, 0x26, 0x1e, 0x37, 0x8f, 0xd9, 0x10, 0x13, 0x3f, 0x30, 0xee, 0xaa,
+	0xe3, 0xbf, 0x1f, 0x09, 0x58, 0x97, 0xf4, 0xa7, 0x8a, 0xfd, 0x2c, 0x21, 0x67, 0xe3, 0x5b, 0x66,
+	0x5a, 0xe8, 0x66, 0xb4, 0xfe, 0x14, 0xac, 0xb8, 0xd6, 0x99, 0x19, 0x10, 0x8a, 0xcd, 0x93, 0xbe,
+	0x17, 0x18, 0xcb, 0x4d, 0xad, 0x73, 0xbb, 0xf7, 0x9e, 0x1c, 0x4e, 0xd7, 0x3a, 0xfb, 0x8a, 0x50,
+	0x7c, 0xd0, 0xf7, 0xa4, 0x6a, 0x5d, 0xa9, 0xe6, 0xb0, 0xd6, 0x3f, 0x02, 0x2e, 0x38, 0x94, 0xa3,
+	0x7c, 0xe0, 0x54, 0xd0, 0x27, 0xf6, 0x28, 0x11, 0x5c, 0x29, 0x08, 0x22, 0x62, 0x8f, 0xca, 0x82,
+	0x53, 0xac, 0x20, 0x38, 0x05, 0x75, 0x0a, 0x6a, 0xce, 0x80, 0x32, 0x9f, 0xe0, 0x2c, 0xff, 0xd5,
+	0xe6, 0x42, 0xe7, 0xee, 0xf6, 0x46, 0x37, 0x79, 0x18, 0xba, 0x4f, 0xd3, 0x87, 0x21, 0xc9, 0xa9,
+	0xf7, 0x58, 0xf6, 0x62, 0x24, 0xe0, 0x6a, 0xba, 0x6d, 0x56, 0x98, 0xf5, 0xa4, 0xab, 0xf2, 0x70,
+	0x0b, 0x95, 0xc2, 0xf4, 0x1f, 0x35, 0x50, 0xf3, 0x08, 0xc5, 0x0e, 0x1d, 0x64, 0x86, 0xb5, 0x37,
+	0x1a, 0x3e, 0x91, 0x86, 0x13, 0x01, 0x8d, 0x3d, 0xe2, 0xf9, 0xc4, 0xb6, 0x38, 0xc1, 0x47, 0x89,
+	0x40, 0xaa, 0x19, 0x09, 0xa8, 0x3d, 0xce, 0xee, 0x20, 0x2f, 0xcf, 0xe5, 0x5a, 0xc3, 0xd0, 0xd0,
+	0x6a, 0x81, 0x0b, 0xf4, 0x5f, 0x34, 0x50, 0x4b, 0xaa, 0xf9, 0x6d, 0x48, 0x02, 0x6e, 0x9e, 0x38,
+	0x7d, 0x63, 0x4d, 0xd5, 0x33, 0x98, 0x08, 0xb8, 0xf2, 0x85, 0x2c, 0x93, 0x62, 0x0e, 0x9c, 0x5e,
+	0x24, 0xe0, 0x8a, 0x9b, 0x07, 0xb2, 0x84, 0x0b, 0xe8, 0xb4, 0xc8, 0xd1, 0x55, 0xbb, 0x14, 0x5e,
+	0x06, 0x2e, 0xc6, 0xed, 0xa2, 0x03, 0x2a, 0xf0, 0x7d, 0xfd, 0x13, 0x50, 0x0d, 0x29, 0xf7, 0xc3,
+	0x80, 0x13, 0x6c, 0xd4, 0x55, 0x4f, 0x36, 0xe5, 0x53, 0x92, 0x81, 0xb1, 0x80, 0x35, 0x75, 0x82,
+	0x0c, 0x69, 0xa1, 0x19, 0xab, 0xb2, 0x93, 0x17, 0x1c, 0x27, 0xe6, 0x20, 0x74, 0x4c, 0x8f, 0xf9,
+	0xdc, 0xd0, 0x67, 0xd9, 0x21, 0x45, 0x7d, 0xfe, 0xf5, 0xfe, 0x11, 0xf3, 0xb9, 0xcc, 0xce, 0xcf,
+	0x03, 0x59, 0x76, 0x05, 0x34, 0x9f, 0x5d, 0x31, 0xbc, 0x0c, 0xc8, 0xec, 0x0a, 0x0e, 0x68, 0xca,
+	0x87, 0x8e, 0x5c, 0xea, 0xdf, 0x6b, 0xa0, 0x46, 0x43, 0xd7, 0xb4, 0x19, 0xa5, 0x44, 0x5d, 0x83,
+	0x81, 0xb1, 0xae, 0x4e, 0xf7, 0x7c, 0x22, 0x60, 0x1d, 0x59, 0xa7, 0x87, 0xa1, 0xbb, 0x3b, 0x23,
+	0x65, 0xc7, 0xd1, 0x02, 0x12, 0x0b, 0xf8, 0x76, 0xf2, 0x4a, 0x17, 0xe0, 0xe9, 0x19, 0x2f, 0xc6,
+	0xed, 0x9b, 0x2a, 0xa8, 0xa4, 0xd1, 0x3b, 0xb8, 0x7c, 0xd5, 0xa8, 0x8c, 0x5f, 0x35, 0x2a, 0x97,
+	0x93, 0x86, 0x36, 0x9e, 0x34, 0xb4, 0x9f, 0xae, 0x1b, 0x95, 0x97, 0xd7, 0x0d, 0x6d, 0x7c, 0xdd,
+	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) {
@@ -176,6 +179,13 @@ func (m *DeviceConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if m.RawNumConnections != 0 {
+		i = encodeVarintDeviceconfiguration(dAtA, i, uint64(m.RawNumConnections))
+		i--
+		dAtA[i] = 0x1
+		i--
+		dAtA[i] = 0x98
+	}
 	if m.RemoteGUIPort != 0 {
 		i = encodeVarintDeviceconfiguration(dAtA, i, uint64(m.RemoteGUIPort))
 		i--
@@ -423,6 +433,9 @@ func (m *DeviceConfiguration) ProtoSize() (n int) {
 	if m.RemoteGUIPort != 0 {
 		n += 2 + sovDeviceconfiguration(uint64(m.RemoteGUIPort))
 	}
+	if m.RawNumConnections != 0 {
+		n += 2 + sovDeviceconfiguration(uint64(m.RawNumConnections))
+	}
 	return n
 }
 
@@ -918,6 +931,25 @@ func (m *DeviceConfiguration) Unmarshal(dAtA []byte) error {
 					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:
 			iNdEx = preIndex
 			skippy, err := skipDeviceconfiguration(dAtA[iNdEx:])
diff --git a/lib/config/wrapper.go b/lib/config/wrapper.go
index 20002ac55bd..dcb3a9dea4b 100644
--- a/lib/config/wrapper.go
+++ b/lib/config/wrapper.go
@@ -20,6 +20,7 @@ import (
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/sliceutil"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/thejerf/suture/v4"
 )
@@ -198,9 +199,7 @@ func (w *wrapper) Unsubscribe(c Committer) {
 	w.mut.Lock()
 	for i := range w.subs {
 		if w.subs[i] == c {
-			copy(w.subs[i:], w.subs[i+1:])
-			w.subs[len(w.subs)-1] = nil
-			w.subs = w.subs[:len(w.subs)-1]
+			w.subs = sliceutil.RemoveAndZero(w.subs, i)
 			break
 		}
 	}
diff --git a/lib/connections/quic_dial.go b/lib/connections/quic_dial.go
index 7466f8ca349..2450fe9f73a 100644
--- a/lib/connections/quic_dial.go
+++ b/lib/connections/quic_dial.go
@@ -91,6 +91,7 @@ func (d *quicDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL
 	if isLocal {
 		priority = d.lanPriority
 	}
+
 	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{
 			reconnectInterval: time.Duration(quicInterval) * time.Second,
 			tlsCfg:            tlsCfg,
+			lanChecker:        lanChecker,
 			lanPriority:       opts.ConnectionPriorityQUICLAN,
 			wanPriority:       opts.ConnectionPriorityQUICWAN,
-			lanChecker:        lanChecker,
+			allowsMultiConns:  true,
 		},
 		registry: registry,
 	}
diff --git a/lib/connections/quic_listen.go b/lib/connections/quic_listen.go
index 930376777c2..f5baf6af265 100644
--- a/lib/connections/quic_listen.go
+++ b/lib/connections/quic_listen.go
@@ -105,7 +105,9 @@ func (t *quicListener) serve(ctx context.Context) error {
 	defer quicTransport.Close()
 
 	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)
 	defer t.registry.Unregister(t.uri.Scheme, quicTransport)
diff --git a/lib/connections/registry/registry.go b/lib/connections/registry/registry.go
index a262f2be88c..e86fcb5b7a3 100644
--- a/lib/connections/registry/registry.go
+++ b/lib/connections/registry/registry.go
@@ -12,6 +12,7 @@ package registry
 import (
 	"strings"
 
+	"github.com/syncthing/syncthing/lib/sliceutil"
 	"github.com/syncthing/syncthing/lib/sync"
 )
 
@@ -41,9 +42,7 @@ func (r *Registry) Unregister(scheme string, item interface{}) {
 	candidates := r.available[scheme]
 	for i, existingItem := range candidates {
 		if existingItem == item {
-			candidates[i] = candidates[len(candidates)-1]
-			candidates[len(candidates)-1] = nil
-			r.available[scheme] = candidates[:len(candidates)-1]
+			r.available[scheme] = sliceutil.RemoveAndZero(candidates, i)
 			break
 		}
 	}
diff --git a/lib/connections/service.go b/lib/connections/service.go
index 484fb94ccba..23968fc8066 100644
--- a/lib/connections/service.go
+++ b/lib/connections/service.go
@@ -11,10 +11,14 @@ package connections
 
 import (
 	"context"
+	"crypto/rand"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/base32"
+	"encoding/binary"
 	"errors"
 	"fmt"
+	"io"
 	"math"
 	"net"
 	"net/url"
@@ -23,8 +27,10 @@ import (
 	stdsync "sync"
 	"time"
 
+	"golang.org/x/exp/constraints"
 	"golang.org/x/exp/slices"
 
+	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/connections/registry"
 	"github.com/syncthing/syncthing/lib/discover"
@@ -33,6 +39,7 @@ import (
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/semaphore"
+	"github.com/syncthing/syncthing/lib/sliceutil"
 	"github.com/syncthing/syncthing/lib/stringutil"
 	"github.com/syncthing/syncthing/lib/svcutil"
 	"github.com/syncthing/syncthing/lib/sync"
@@ -66,12 +73,14 @@ var (
 	errDeviceIgnored          = errors.New("device is ignored")
 	errConnLimitReached       = errors.New("connection limit reached")
 	errDevicePaused           = errors.New("device is paused")
+
+	// A connection is being closed to make space for better ones
+	errReplacingConnection = errors.New("replacing connection")
 )
 
 const (
 	perDeviceWarningIntv          = 15 * time.Minute
 	tlsHandshakeTimeout           = 10 * time.Second
-	minConnectionReplaceAge       = 10 * time.Second
 	minConnectionLoopSleep        = 5 * time.Second
 	stdConnectionLoopSleep        = time.Minute
 	worstDialerPriority           = math.MaxInt32
@@ -79,6 +88,7 @@ const (
 	shortLivedConnectionThreshold = 5 * time.Second
 	dialMaxParallel               = 64
 	dialMaxParallelPerDevice      = 8
+	maxNumConnections             = 128 // the maximum number of connections we maintain to any given device
 )
 
 // From go/src/crypto/tls/cipher_suites.go
@@ -150,6 +160,7 @@ type connWithHello struct {
 type service struct {
 	*suture.Supervisor
 	connectionStatusHandler
+	deviceConnectionTracker
 
 	cfg                  config.Wrapper
 	myID                 protocol.DeviceID
@@ -281,21 +292,43 @@ func (s *service) handleConns(ctx context.Context) error {
 
 		_ = c.SetDeadline(time.Now().Add(20 * time.Second))
 		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 {
-			case s.hellos <- &connWithHello{c, hello, err, remoteID, remoteCert}:
+			case s.hellos <- &connWithHello{c, incoming, err, remoteID, remoteCert}:
 			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 {
 	if s.cfg.IgnoredDevice(remoteID) {
 		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.
 		return errConnLimitReached
 	}
@@ -315,31 +348,26 @@ func (s *service) connectionCheckEarly(remoteID protocol.DeviceID, c internalCon
 		return errNetworkNotAllowed
 	}
 
-	// Lower priority is better, just like nice etc.
-	if ct, ok := s.model.Connection(remoteID); ok {
-		if ct.Priority() > c.priority || time.Since(ct.Statistics().StartedAt) > minConnectionReplaceAge {
-			l.Debugf("Switching connections %s (existing: %s new: %s)", remoteID, ct, c)
-		} else {
-			// We should not already be connected to the other party. TODO: This
-			// could use some better handling. If the old connection is dead but
-			// hasn't timed out yet we may want to drop *that* connection and keep
-			// this one. But in case we are two devices connecting to each other
-			// in parallel we don't want to do that or we end up with no
-			// connections still established...
-			return errDeviceAlreadyConnected
-		}
+	currentConns := s.numConnectionsForDevice(cfg.DeviceID)
+	desiredConns := s.desiredConnectionsToDevice(cfg.DeviceID)
+	worstPrio := s.worstConnectionPriority(remoteID)
+	ourUpgradeThreshold := c.priority + s.cfg.Options().ConnectionPriorityUpgradeThreshold
+	if currentConns >= desiredConns && ourUpgradeThreshold >= worstPrio {
+		l.Debugf("Not accepting connection to %s at %s: already have %d connections, desire %d", remoteID, c, currentConns, desiredConns)
+		return errDeviceAlreadyConnected
 	}
 
 	return nil
 }
 
 func (s *service) handleHellos(ctx context.Context) error {
-	var c internalConn
-	var hello protocol.Hello
-	var err error
-	var remoteID protocol.DeviceID
-	var remoteCert *x509.Certificate
 	for {
+		var c internalConn
+		var hello protocol.Hello
+		var err error
+		var remoteID protocol.DeviceID
+		var remoteCert *x509.Certificate
+
 		select {
 		case <-ctx.Done():
 			return ctx.Err()
@@ -416,15 +444,17 @@ func (s *service) handleHellos(ctx context.Context) error {
 		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)
+		s.accountAddedConnection(protoConn, hello, s.cfg.Options().ConnectionPriorityUpgradeThreshold)
 		go func() {
 			<-protoConn.Closed()
+			s.accountRemovedConnection(protoConn)
 			s.dialNowDevicesMut.Lock()
 			s.dialNowDevices[remoteID] = struct{}{}
 			s.scheduleDialNow()
 			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)
 		continue
@@ -518,7 +548,7 @@ func (s *service) dialDevices(ctx context.Context, now time.Time, cfg config.Con
 	allowAdditional := 0 // no limit
 	connectionLimit := cfg.Options.LowestConnectionLimit()
 	if connectionLimit > 0 {
-		current := s.model.NumConnections()
+		current := s.numConnectedDevices()
 		allowAdditional = connectionLimit - current
 		if allowAdditional <= 0 {
 			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
 		// for dialer priority.
 		priorityCutoff := worstDialerPriority
-		connection, connected := s.model.Connection(deviceCfg.DeviceID)
-		if connected {
+		if currentConns := s.numConnectionsForDevice(deviceCfg.DeviceID); currentConns > 0 {
 			// Set the priority cutoff to the current connection's 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
 			// we don't attempt dialers that aren't considered a worthy upgrade.
 			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
-				// 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
 			}
 		}
@@ -625,14 +656,14 @@ func (s *service) resolveDialTargets(ctx context.Context, now time.Time, cfg con
 	deviceID := deviceCfg.DeviceID
 
 	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))
 	for _, addr := range addrs {
 		// Use both device and address, as you might have two devices connected
 		// to the same relay
 		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
 		}
 
@@ -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)
 		priority := dialer.Priority(uri.Host)
-		if priority >= priorityCutoff {
-			l.Debugf("Not dialing using %s as priority is not better than current connection (%d >= %d)", dialerFactory, priority, priorityCutoff)
+		currentConns := s.numConnectionsForDevice(deviceCfg.DeviceID)
+		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
 		}
 
@@ -1272,3 +1312,165 @@ func (r nextDialRegistry) sleepDurationAndCleanup(now time.Time) time.Duration {
 	}
 	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
+}
diff --git a/lib/connections/structs.go b/lib/connections/structs.go
index 1f5bf376b3d..37c8ef48492 100644
--- a/lib/connections/structs.go
+++ b/lib/connections/structs.go
@@ -42,6 +42,7 @@ type internalConn struct {
 	isLocal       bool
 	priority      int
 	establishedAt time.Time
+	connectionID  string // set after Hello exchange
 }
 
 type connType int
@@ -88,12 +89,13 @@ func (t connType) Transport() string {
 }
 
 func newInternalConn(tc tlsConn, connType connType, isLocal bool, priority int) internalConn {
+	now := time.Now()
 	return internalConn{
 		tlsConn:       tc,
 		connType:      connType,
 		isLocal:       isLocal,
 		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
 }
 
+func (c internalConn) ConnectionID() string {
+	return c.connectionID
+}
+
 func (c internalConn) String() string {
 	t := "WAN"
 	if c.isLocal {
 		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 {
@@ -160,6 +166,7 @@ type commonDialer struct {
 	lanChecker        *lanChecker
 	lanPriority       int
 	wanPriority       int
+	allowsMultiConns  bool
 }
 
 func (d *commonDialer) RedialFrequency() time.Duration {
@@ -173,10 +180,15 @@ func (d *commonDialer) Priority(host string) int {
 	return d.wanPriority
 }
 
+func (d *commonDialer) AllowsMultiConns() bool {
+	return d.allowsMultiConns
+}
+
 type genericDialer interface {
 	Dial(context.Context, protocol.DeviceID, *url.URL) (internalConn, error)
 	RedialFrequency() time.Duration
 	Priority(host string) int
+	AllowsMultiConns() bool
 }
 
 type listenerFactory interface {
@@ -212,10 +224,7 @@ type genericListener interface {
 type Model interface {
 	protocol.Model
 	AddConnection(conn protocol.Connection, hello protocol.Hello)
-	NumConnections() int
-	Connection(remoteID protocol.DeviceID) (protocol.Connection, bool)
 	OnHello(protocol.DeviceID, net.Addr, protocol.Hello) error
-	GetHello(protocol.DeviceID) protocol.HelloIntf
 	DeviceStatistics() (map[protocol.DeviceID]stats.DeviceStatistics, error)
 }
 
diff --git a/lib/connections/tcp_dial.go b/lib/connections/tcp_dial.go
index 04a551e4664..870575357ad 100644
--- a/lib/connections/tcp_dial.go
+++ b/lib/connections/tcp_dial.go
@@ -62,6 +62,7 @@ func (d *tcpDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL)
 	if isLocal {
 		priority = d.lanPriority
 	}
+
 	return newInternalConn(tc, connTypeTCPClient, isLocal, priority), nil
 }
 
@@ -73,9 +74,10 @@ func (tcpDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config
 			trafficClass:      opts.TrafficClass,
 			reconnectInterval: time.Duration(opts.ReconnectIntervalS) * time.Second,
 			tlsCfg:            tlsCfg,
+			lanChecker:        lanChecker,
 			lanPriority:       opts.ConnectionPriorityTCPLAN,
 			wanPriority:       opts.ConnectionPriorityTCPWAN,
-			lanChecker:        lanChecker,
+			allowsMultiConns:  true,
 		},
 		registry: registry,
 	}
diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go
index 5be363000b6..0e54cbfeed2 100644
--- a/lib/model/folder_sendrecv.go
+++ b/lib/model/folder_sendrecv.go
@@ -507,7 +507,7 @@ nextFile:
 
 		devices := snap.Availability(fileName)
 		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.
 				f.handleFile(fi, snap, copyChan)
 				continue nextFile
diff --git a/lib/model/mocks/model.go b/lib/model/mocks/model.go
index c0f8c95b883..bf29f47ece9 100644
--- a/lib/model/mocks/model.go
+++ b/lib/model/mocks/model.go
@@ -76,18 +76,16 @@ type Model struct {
 		result1 model.FolderCompletion
 		result2 error
 	}
-	ConnectionStub        func(protocol.DeviceID) (protocol.Connection, bool)
-	connectionMutex       sync.RWMutex
-	connectionArgsForCall []struct {
+	ConnectedToStub        func(protocol.DeviceID) bool
+	connectedToMutex       sync.RWMutex
+	connectedToArgsForCall []struct {
 		arg1 protocol.DeviceID
 	}
-	connectionReturns struct {
-		result1 protocol.Connection
-		result2 bool
+	connectedToReturns struct {
+		result1 bool
 	}
-	connectionReturnsOnCall map[int]struct {
-		result1 protocol.Connection
-		result2 bool
+	connectedToReturnsOnCall map[int]struct {
+		result1 bool
 	}
 	ConnectionStatsStub        func() map[string]interface{}
 	connectionStatsMutex       sync.RWMutex
@@ -262,17 +260,6 @@ type Model struct {
 		result1 map[string][]versioner.FileVersion
 		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)
 	getMtimeMappingMutex       sync.RWMutex
 	getMtimeMappingArgsForCall []struct {
@@ -378,16 +365,6 @@ type Model struct {
 		result3 []db.FileInfoTruncated
 		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
 	onHelloMutex       sync.RWMutex
 	onHelloArgsForCall []struct {
@@ -888,68 +865,65 @@ func (fake *Model) CompletionReturnsOnCall(i int, result1 model.FolderCompletion
 	}{result1, result2}
 }
 
-func (fake *Model) Connection(arg1 protocol.DeviceID) (protocol.Connection, bool) {
-	fake.connectionMutex.Lock()
-	ret, specificReturn := fake.connectionReturnsOnCall[len(fake.connectionArgsForCall)]
-	fake.connectionArgsForCall = append(fake.connectionArgsForCall, struct {
+func (fake *Model) ConnectedTo(arg1 protocol.DeviceID) bool {
+	fake.connectedToMutex.Lock()
+	ret, specificReturn := fake.connectedToReturnsOnCall[len(fake.connectedToArgsForCall)]
+	fake.connectedToArgsForCall = append(fake.connectedToArgsForCall, struct {
 		arg1 protocol.DeviceID
 	}{arg1})
-	stub := fake.ConnectionStub
-	fakeReturns := fake.connectionReturns
-	fake.recordInvocation("Connection", []interface{}{arg1})
-	fake.connectionMutex.Unlock()
+	stub := fake.ConnectedToStub
+	fakeReturns := fake.connectedToReturns
+	fake.recordInvocation("ConnectedTo", []interface{}{arg1})
+	fake.connectedToMutex.Unlock()
 	if stub != nil {
 		return stub(arg1)
 	}
 	if specificReturn {
-		return ret.result1, ret.result2
+		return ret.result1
 	}
-	return fakeReturns.result1, fakeReturns.result2
+	return fakeReturns.result1
 }
 
-func (fake *Model) ConnectionCallCount() int {
-	fake.connectionMutex.RLock()
-	defer fake.connectionMutex.RUnlock()
-	return len(fake.connectionArgsForCall)
+func (fake *Model) ConnectedToCallCount() int {
+	fake.connectedToMutex.RLock()
+	defer fake.connectedToMutex.RUnlock()
+	return len(fake.connectedToArgsForCall)
 }
 
-func (fake *Model) ConnectionCalls(stub func(protocol.DeviceID) (protocol.Connection, bool)) {
-	fake.connectionMutex.Lock()
-	defer fake.connectionMutex.Unlock()
-	fake.ConnectionStub = stub
+func (fake *Model) ConnectedToCalls(stub func(protocol.DeviceID) bool) {
+	fake.connectedToMutex.Lock()
+	defer fake.connectedToMutex.Unlock()
+	fake.ConnectedToStub = stub
 }
 
-func (fake *Model) ConnectionArgsForCall(i int) protocol.DeviceID {
-	fake.connectionMutex.RLock()
-	defer fake.connectionMutex.RUnlock()
-	argsForCall := fake.connectionArgsForCall[i]
+func (fake *Model) ConnectedToArgsForCall(i int) protocol.DeviceID {
+	fake.connectedToMutex.RLock()
+	defer fake.connectedToMutex.RUnlock()
+	argsForCall := fake.connectedToArgsForCall[i]
 	return argsForCall.arg1
 }
 
-func (fake *Model) ConnectionReturns(result1 protocol.Connection, result2 bool) {
-	fake.connectionMutex.Lock()
-	defer fake.connectionMutex.Unlock()
-	fake.ConnectionStub = nil
-	fake.connectionReturns = struct {
-		result1 protocol.Connection
-		result2 bool
-	}{result1, result2}
+func (fake *Model) ConnectedToReturns(result1 bool) {
+	fake.connectedToMutex.Lock()
+	defer fake.connectedToMutex.Unlock()
+	fake.ConnectedToStub = nil
+	fake.connectedToReturns = struct {
+		result1 bool
+	}{result1}
 }
 
-func (fake *Model) ConnectionReturnsOnCall(i int, result1 protocol.Connection, result2 bool) {
-	fake.connectionMutex.Lock()
-	defer fake.connectionMutex.Unlock()
-	fake.ConnectionStub = nil
-	if fake.connectionReturnsOnCall == nil {
-		fake.connectionReturnsOnCall = make(map[int]struct {
-			result1 protocol.Connection
-			result2 bool
+func (fake *Model) ConnectedToReturnsOnCall(i int, result1 bool) {
+	fake.connectedToMutex.Lock()
+	defer fake.connectedToMutex.Unlock()
+	fake.ConnectedToStub = nil
+	if fake.connectedToReturnsOnCall == nil {
+		fake.connectedToReturnsOnCall = make(map[int]struct {
+			result1 bool
 		})
 	}
-	fake.connectionReturnsOnCall[i] = struct {
-		result1 protocol.Connection
-		result2 bool
-	}{result1, result2}
+	fake.connectedToReturnsOnCall[i] = struct {
+		result1 bool
+	}{result1}
 }
 
 func (fake *Model) ConnectionStats() map[string]interface{} {
@@ -1797,67 +1771,6 @@ func (fake *Model) GetFolderVersionsReturnsOnCall(i int, result1 map[string][]ve
 	}{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) {
 	fake.getMtimeMappingMutex.Lock()
 	ret, specificReturn := fake.getMtimeMappingReturnsOnCall[len(fake.getMtimeMappingArgsForCall)]
@@ -2331,59 +2244,6 @@ func (fake *Model) NeedFolderFilesReturnsOnCall(i int, result1 []db.FileInfoTrun
 	}{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 {
 	fake.onHelloMutex.Lock()
 	ret, specificReturn := fake.onHelloReturnsOnCall[len(fake.onHelloArgsForCall)]
@@ -3419,8 +3279,8 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.clusterConfigMutex.RUnlock()
 	fake.completionMutex.RLock()
 	defer fake.completionMutex.RUnlock()
-	fake.connectionMutex.RLock()
-	defer fake.connectionMutex.RUnlock()
+	fake.connectedToMutex.RLock()
+	defer fake.connectedToMutex.RUnlock()
 	fake.connectionStatsMutex.RLock()
 	defer fake.connectionStatsMutex.RUnlock()
 	fake.currentFolderFileMutex.RLock()
@@ -3449,8 +3309,6 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.folderStatisticsMutex.RUnlock()
 	fake.getFolderVersionsMutex.RLock()
 	defer fake.getFolderVersionsMutex.RUnlock()
-	fake.getHelloMutex.RLock()
-	defer fake.getHelloMutex.RUnlock()
 	fake.getMtimeMappingMutex.RLock()
 	defer fake.getMtimeMappingMutex.RUnlock()
 	fake.globalDirectoryTreeMutex.RLock()
@@ -3465,8 +3323,6 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.localChangedFolderFilesMutex.RUnlock()
 	fake.needFolderFilesMutex.RLock()
 	defer fake.needFolderFilesMutex.RUnlock()
-	fake.numConnectionsMutex.RLock()
-	defer fake.numConnectionsMutex.RUnlock()
 	fake.onHelloMutex.RLock()
 	defer fake.onHelloMutex.RUnlock()
 	fake.overrideMutex.RLock()
diff --git a/lib/model/model.go b/lib/model/model.go
index 02cf8636dff..970d84c4566 100644
--- a/lib/model/model.go
+++ b/lib/model/model.go
@@ -37,6 +37,7 @@ import (
 	"github.com/syncthing/syncthing/lib/ignore"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/scanner"
 	"github.com/syncthing/syncthing/lib/semaphore"
 	"github.com/syncthing/syncthing/lib/stats"
@@ -108,6 +109,7 @@ type Model interface {
 	DeviceStatistics() (map[protocol.DeviceID]stats.DeviceStatistics, error)
 	FolderStatistics() (map[string]stats.FolderStatistics, error)
 	UsageReportingStats(report *contract.Report, version int, preview bool)
+	ConnectedTo(remoteID protocol.DeviceID) bool
 
 	PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error)
 	PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error)
@@ -124,8 +126,6 @@ type model struct {
 	// constructor parameters
 	cfg            config.Wrapper
 	id             protocol.DeviceID
-	clientName     string
-	clientVersion  string
 	db             *db.Lowlevel
 	protectedFiles []string
 	evLogger       events.Logger
@@ -143,6 +143,7 @@ type model struct {
 	fatalChan       chan error
 	started         chan struct{}
 	keyGen          *protocol.KeyGenerator
+	promotionTimer  *time.Timer
 
 	// fields protected by fmut
 	fmut                           sync.RWMutex
@@ -158,9 +159,11 @@ type model struct {
 
 	// fields protected by pmut
 	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
-	closed              map[protocol.DeviceID]chan struct{}
+	closed              map[string]chan struct{} // connection ID -> closed channel
 	helloMessages       map[protocol.DeviceID]protocol.Hello
 	deviceDownloads     map[protocol.DeviceID]*deviceDownloadState
 	remoteFolderStates  map[protocol.DeviceID]map[string]remoteFolderState // deviceID -> folders
@@ -179,13 +182,11 @@ var folderFactories = make(map[config.FolderType]folderFactory)
 var (
 	errDeviceUnknown    = errors.New("unknown device")
 	errDevicePaused     = errors.New("device is paused")
-	errDeviceRemoved    = errors.New("device has been removed")
 	ErrFolderPaused     = errors.New("folder is paused")
 	ErrFolderNotRunning = errors.New("folder is not running")
 	ErrFolderMissing    = errors.New("no such folder")
 	errNoVersioner      = errors.New("folder has no versioner")
 	// errors about why a connection is closed
-	errReplacingConnection                = errors.New("replacing connection")
 	errStopped                            = errors.New("Syncthing is being stopped")
 	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")
@@ -203,7 +204,7 @@ var (
 // 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
 // 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)
 	m := &model{
 		Supervisor: suture.New("model", spec),
@@ -211,8 +212,6 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
 		// constructor parameters
 		cfg:            cfg,
 		id:             id,
-		clientName:     clientName,
-		clientVersion:  clientVersion,
 		db:             ldb,
 		protectedFiles: protectedFiles,
 		evLogger:       evLogger,
@@ -226,6 +225,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
 		fatalChan:            make(chan error),
 		started:              make(chan struct{}),
 		keyGen:               keyGen,
+		promotionTimer:       time.NewTimer(0),
 
 		// fields protected by fmut
 		fmut:                           sync.NewRWMutex(),
@@ -240,16 +240,19 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
 
 		// fields protected by pmut
 		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),
-		closed:              make(map[protocol.DeviceID]chan struct{}),
+		closed:              make(map[string]chan struct{}),
 		helloMessages:       make(map[protocol.DeviceID]protocol.Hello),
 		deviceDownloads:     make(map[protocol.DeviceID]*deviceDownloadState),
 		remoteFolderStates:  make(map[protocol.DeviceID]map[string]remoteFolderState),
 		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.setConnRequestLimitersPLocked(cfg)
 	}
 	m.Add(m.folderRunners)
 	m.Add(m.progressEmitter)
@@ -272,11 +275,18 @@ func (m *model) serve(ctx context.Context) error {
 
 	close(m.started)
 
-	select {
-	case <-ctx.Done():
-		return ctx.Err()
-	case err := <-m.fatalChan:
-		return svcutil.AsFatalErr(err, svcutil.ExitError)
+	for {
+		select {
+		case <-ctx.Done():
+			l.Debugln(m, "context closed, stopping", ctx.Err())
+			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() {
 	m.pmut.RLock()
-	closed := make([]chan struct{}, 0, len(m.conn))
-	for id, conn := range m.conn {
-		closed = append(closed, m.closed[id])
+	closed := make([]chan struct{}, 0, len(m.connections))
+	for connID, conn := range m.connections {
+		closed = append(closed, m.closed[connID])
 		go conn.Close(errStopped)
 	}
 	m.pmut.RUnlock()
@@ -635,7 +645,7 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
 
 		// Transport stats
 		m.pmut.RLock()
-		for _, conn := range m.conn {
+		for _, conn := range m.connections {
 			report.TransportStats[conn.Transport()]++
 		}
 		m.pmut.RUnlock()
@@ -699,22 +709,28 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
 	}
 }
 
-type ConnectionInfo struct {
-	protocol.Statistics
+type ConnectionStats struct {
+	protocol.Statistics // Total for primary + secondaries
+
 	Connected     bool   `json:"connected"`
 	Paused        bool   `json:"paused"`
-	Address       string `json:"address"`
 	ClientVersion string `json:"clientVersion"`
-	Type          string `json:"type"`
-	IsLocal       bool   `json:"isLocal"`
-	Crypto        string `json:"crypto"`
+
+	Address string `json:"address"` // mirror values from Primary, for compatibility with <1.24.0
+	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.
-func (m *model) NumConnections() int {
-	m.pmut.RLock()
-	defer m.pmut.RUnlock()
-	return len(m.conn)
+type ConnectionInfo struct {
+	protocol.Statistics
+	Address string `json:"address"`
+	Type    string `json:"type"`
+	IsLocal bool   `json:"isLocal"`
+	Crypto  string `json:"crypto"`
 }
 
 // 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{})
 	devs := m.cfg.Devices()
-	conns := make(map[string]ConnectionInfo, len(devs))
+	conns := make(map[string]ConnectionStats, len(devs))
 	for device, deviceCfg := range devs {
+		if device == m.id {
+			continue
+		}
 		hello := m.helloMessages[device]
 		versionString := hello.ClientVersion
 		if hello.ClientName != "syncthing" {
 			versionString = hello.ClientName + " " + hello.ClientVersion
 		}
-		ci := ConnectionInfo{
-			ClientVersion: strings.TrimSpace(versionString),
+		connIDs, ok := m.deviceConnIDs[device]
+		cs := ConnectionStats{
+			Connected:     ok,
 			Paused:        deviceCfg.Paused,
+			ClientVersion: strings.TrimSpace(versionString),
 		}
-		if conn, ok := m.conn[device]; ok {
-			ci.Type = conn.Type()
-			ci.IsLocal = conn.IsLocal()
-			ci.Crypto = conn.Crypto()
-			ci.Connected = ok
-			ci.Statistics = conn.Statistics()
-			if addr := conn.RemoteAddr(); addr != nil {
-				ci.Address = addr.String()
+		if ok {
+			conn := m.connections[connIDs[0]]
+
+			cs.Primary.Type = conn.Type()
+			cs.Primary.IsLocal = conn.IsLocal()
+			cs.Primary.Crypto = conn.Crypto()
+			cs.Primary.Statistics = conn.Statistics()
+			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
@@ -1138,17 +1184,16 @@ func (m *model) handleIndex(conn protocol.Connection, folder string, fs []protoc
 	}
 
 	m.pmut.RLock()
-	indexHandler, ok := m.indexHandlers.Get(deviceID)
+	indexHandler, ok := m.getIndexHandlerPRLocked(conn)
 	m.pmut.RUnlock()
 	if !ok {
-		// This should be impossible, as an index handler always exists for an
-		// open connection, and this method can't be called on a closed
-		// connection
+		// This should be impossible, as an index handler is registered when
+		// we send a cluster config, and that is what triggers index
+		// sending.
 		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)
-		return fmt.Errorf("index handler missing: %s", folder)
+		return fmt.Errorf("%s: %w", folder, ErrFolderNotRunning)
 	}
-
 	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 {
+	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
 	// 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
 	// temporary indexes, subscribe the connection.
 
-	deviceID := conn.DeviceID()
-	l.Debugf("Handling ClusterConfig from %v", deviceID.Short())
-
-	m.pmut.RLock()
-	indexHandlerRegistry, ok := m.indexHandlers.Get(deviceID)
-	m.pmut.RUnlock()
-	if !ok {
-		panic("bug: ClusterConfig called on closed or nonexistent connection")
-	}
+	l.Debugf("Handling ClusterConfig from %v at %s", deviceID.Short(), conn)
+	indexHandlerRegistry := m.ensureIndexHandler(conn)
 
 	deviceCfg, ok := m.cfg.Device(deviceID)
 	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
 	}
 
@@ -1198,11 +1245,11 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
 			}
 		}
 		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
 		}
 		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
 		}
 		ccDeviceInfos[folder.ID] = info
@@ -1260,12 +1307,16 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
 	})
 
 	if len(tempIndexFolders) > 0 {
+		var connOK bool
+		var conn protocol.Connection
 		m.pmut.RLock()
-		conn, ok := m.conn[deviceID]
+		if connIDs, connIDOK := m.deviceConnIDs[deviceID]; connIDOK {
+			conn, connOK = m.connections[connIDs[0]]
+		}
 		m.pmut.RUnlock()
 		// In case we've got ClusterConfig, and the connection disappeared
 		// from infront of our nose.
-		if ok {
+		if connOK {
 			m.progressEmitter.temporaryIndexSubscribe(conn, tempIndexFolders)
 		}
 	}
@@ -1291,6 +1342,57 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
 	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) {
 	var folderDevice config.FolderDeviceConfiguration
 	tempIndexFolders := make([]string, 0, len(folders))
@@ -1539,8 +1641,8 @@ func (m *model) sendClusterConfig(ids []protocol.DeviceID) {
 	ccConns := make([]protocol.Connection, 0, len(ids))
 	m.pmut.RLock()
 	for _, id := range ids {
-		if conn, ok := m.conn[id]; ok {
-			ccConns = append(ccConns, conn)
+		if connIDs, ok := m.deviceConnIDs[id]; ok {
+			ccConns = append(ccConns, m.connections[connIDs[0]])
 		}
 	}
 	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
 func (m *model) Closed(conn protocol.Connection, err error) {
-	device := conn.DeviceID()
+	connID := conn.ConnectionID()
+	deviceID := conn.DeviceID()
+
 	m.pmut.Lock()
-	conn, ok := m.conn[device]
+	conn, ok := m.connections[connID]
 	if !ok {
 		m.pmut.Unlock()
 		return
 	}
 
-	delete(m.conn, device)
-	delete(m.connRequestLimiters, device)
-	delete(m.helloMessages, device)
-	delete(m.deviceDownloads, device)
-	delete(m.remoteFolderStates, device)
-	closed := m.closed[device]
-	delete(m.closed, device)
-	wait := m.indexHandlers.RemoveAndWaitChan(device, 0)
+	closed := m.closed[connID]
+	delete(m.closed, connID)
+	delete(m.connections, connID)
+
+	removedIsPrimary := m.promotedConnID[deviceID] == connID
+	remainingConns := without(m.deviceConnIDs[deviceID], connID)
+	var wait <-chan error
+	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()
-	<-wait
+	if wait != nil {
+		<-wait
+	}
+
+	m.fmut.RLock()
+	m.deviceDidCloseFRLocked(deviceID, time.Since(conn.EstablishedAt()))
+	m.fmut.RUnlock()
 
-	m.progressEmitter.temporaryIndexUnsubscribe(conn)
-	m.deviceDidClose(device, time.Since(conn.EstablishedAt()))
+	k := map[bool]string{false: "secondary", true: "primary"}[removedIsPrimary]
+	l.Infof("Lost %s connection to %s at %s: %v (%d remain)", k, deviceID.Short(), conn, err, len(remainingConns))
 
-	l.Infof("Connection to %s at %s closed: %v", device, conn, err)
-	m.evLogger.Log(events.DeviceDisconnected, map[string]string{
-		"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)
 }
 
@@ -1852,36 +1983,36 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
 	if !ok {
 		// The folder might be already unpaused in the config, but not yet
 		// 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
 	}
 
 	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
 	}
 	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
 	}
 
 	// Make sure the path is valid and in canonical form
 	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
 	}
 
 	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) {
-		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
 	}
 
 	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
 	}
 
@@ -1908,7 +2039,7 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
 	folderFs := folderCfg.Filesystem(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
 	}
 
@@ -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() {
 			// Reject reads for anything that doesn't exist or is something
 			// 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
 		}
 		_, 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() {
 		// Reject reads for anything that doesn't exist or is something
 		// 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
 	}
 
 	n, err := readOffsetIntoBuf(folderFs, name, offset, res.data)
 	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
 	} else if err == io.EOF {
 		// 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
 		// managed to read.
 	} 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
 	}
 
 	if folderCfg.Type != config.FolderTypeReceiveEncrypted && len(hash) > 0 && !scanner.Validate(res.data[:n], 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
 	}
 
@@ -2074,15 +2205,12 @@ func (m *model) GetMtimeMapping(folder string, file string) (fs.MtimeMapping, er
 	return fs.GetMtimeMapping(fcfg.Filesystem(ffs), file)
 }
 
-// Connection returns the current connection for device, and a boolean whether a connection was found.
-func (m *model) Connection(deviceID protocol.DeviceID) (protocol.Connection, bool) {
+// Connection returns if we are connected to the given device.
+func (m *model) ConnectedTo(deviceID protocol.DeviceID) bool {
 	m.pmut.RLock()
-	cn, ok := m.conn[deviceID]
+	_, ok := m.deviceConnIDs[deviceID]
 	m.pmut.RUnlock()
-	if ok {
-		m.deviceWasSeen(deviceID)
-	}
-	return cn, ok
+	return ok
 }
 
 // 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
 }
 
-// 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
 // be sent to the connected peer, thereafter index updates whenever the local
 // folder changes.
 func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
 	deviceID := conn.DeviceID()
-	device, ok := m.cfg.Device(deviceID)
+	deviceCfg, ok := m.cfg.Device(deviceID)
 	if !ok {
 		l.Infoln("Trying to add connection to unknown device")
 		return
 	}
 
-	// The slightly unusual locking sequence here is because we must acquire
-	// 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
+	connID := conn.ConnectionID()
 	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.deviceConnIDs[deviceID] = append(m.deviceConnIDs[deviceID], connID)
+	if m.deviceDownloads[deviceID] == nil {
+		m.deviceDownloads[deviceID] = newDeviceDownloadState()
+	}
 
 	event := map[string]string{
 		"id":            deviceID.String(),
@@ -2284,17 +2367,15 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
 
 	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()
 
-	// Acquires fmut, so has to be done outside of pmut.
-	cm, passwords := m.generateClusterConfig(deviceID)
-	conn.SetFolderPasswords(passwords)
-	conn.ClusterConfig(cm)
-
-	if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
+	if (deviceCfg.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
 		m.cfg.Modify(func(cfg *config.Configuration) {
 			for i := range cfg.Devices {
 				if cfg.Devices[i].DeviceID == deviceID {
@@ -2308,26 +2389,78 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
 	}
 
 	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 {
+	deviceID := conn.DeviceID()
+
 	m.fmut.RLock()
 	cfg, ok := m.folderCfgs[folder]
 	m.fmut.RUnlock()
 
-	device := conn.DeviceID()
-	if !ok || cfg.DisableTempIndexes || !cfg.SharedWith(device) {
+	if !ok || cfg.DisableTempIndexes || !cfg.SharedWith(deviceID) {
 		return nil
 	}
 
 	m.pmut.RLock()
-	downloads := m.deviceDownloads[device]
+	downloads := m.deviceDownloads[deviceID]
 	m.pmut.RUnlock()
 	downloads.Update(folder, updates)
 	state := downloads.GetBlockCounts(folder)
 
 	m.evLogger.Log(events.RemoteDownloadProgress, map[string]interface{}{
-		"device": device.String(),
+		"device": deviceID.String(),
 		"folder": folder,
 		"state":  state,
 	})
@@ -2344,27 +2477,47 @@ func (m *model) deviceWasSeen(deviceID protocol.DeviceID) {
 	}
 }
 
-func (m *model) deviceDidClose(deviceID protocol.DeviceID, duration time.Duration) {
-	m.fmut.RLock()
-	sr, ok := m.deviceStatRefs[deviceID]
-	m.fmut.RUnlock()
-	if ok {
+func (m *model) deviceDidCloseFRLocked(deviceID protocol.DeviceID, duration time.Duration) {
+	if sr, ok := m.deviceStatRefs[deviceID]; ok {
 		_ = 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) {
+	conn, connOK := m.requestConnectionForDevice(deviceID)
+	if !connOK {
+		return nil, fmt.Errorf("requestGlobal: no connection to device: %s", deviceID.Short())
+	}
+
+	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)
+}
+
+// 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()
-	nc, ok := m.conn[deviceID]
-	m.pmut.RUnlock()
+	defer m.pmut.RUnlock()
 
+	connIDs, ok := m.deviceConnIDs[deviceID]
 	if !ok {
-		return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
+		return nil, false
 	}
 
-	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)
+	// 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]
+	}
 
-	return nc.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
+	conn, connOK := m.connections[connID]
+	return conn, connOK
 }
 
 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
 // set of folder passwords for the given peer device
 func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
-	var message protocol.ClusterConfig
-
 	m.fmut.RLock()
 	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()
 	passwords := make(map[string]string, len(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 {
 			continue
 		}
-		_, ok := m.conn[device]
+		_, ok := m.deviceConnIDs[device]
 		if ok {
 			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.
 	fromDevices := from.DeviceMap()
 	toDevices := to.DeviceMap()
@@ -2941,7 +3089,14 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 			l.Infoln("Resuming", deviceID)
 			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
 	removedDevices := make([]protocol.DeviceID, 0, len(fromDevices))
 	m.fmut.Lock()
@@ -2955,14 +3110,18 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 	m.pmut.RLock()
 	for _, id := range closeDevices {
 		delete(clusterConfigDevices, id)
-		if conn, ok := m.conn[id]; ok {
-			go conn.Close(errDevicePaused)
+		if conns, ok := m.deviceConnIDs[id]; ok {
+			for _, connID := range conns {
+				go m.connections[connID].Close(errDevicePaused)
+			}
 		}
 	}
 	for _, id := range removedDevices {
 		delete(clusterConfigDevices, id)
-		if conn, ok := m.conn[id]; ok {
-			go conn.Close(errDeviceRemoved)
+		if conns, ok := m.deviceConnIDs[id]; ok {
+			for _, connID := range conns {
+				go m.connections[connID].Close(errDevicePaused)
+			}
 		}
 	}
 	m.pmut.RUnlock()
@@ -2986,6 +3145,17 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 	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{}) {
 	var removedPendingFolders []map[string]string
 	pendingFolders, err := m.db.PendingFolders()
@@ -3332,3 +3502,12 @@ type redactedError struct {
 	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
+}
diff --git a/lib/protocol/bep.pb.go b/lib/protocol/bep.pb.go
index 38a258a7171..a86589adeef 100644
--- a/lib/protocol/bep.pb.go
+++ b/lib/protocol/bep.pb.go
@@ -211,9 +211,11 @@ func (FileDownloadProgressUpdateType) EnumDescriptor() ([]byte, []int) {
 }
 
 type Hello struct {
-	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"`
-	ClientVersion string `protobuf:"bytes,3,opt,name=client_version,json=clientVersion,proto3" json:"clientVersion" xml:"clientVersion"`
+	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"`
+	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{} }
@@ -288,7 +290,8 @@ func (m *Header) XXX_DiscardUnknown() {
 var xxx_messageInfo_Header proto.InternalMessageInfo
 
 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{} }
@@ -1142,205 +1145,211 @@ func init() {
 func init() { proto.RegisterFile("lib/protocol/bep.proto", fileDescriptor_311ef540e10d9705) }
 
 var fileDescriptor_311ef540e10d9705 = []byte{
-	// 3163 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x5a, 0x4d, 0x6c, 0x1b, 0xc7,
-	0xbd, 0x17, 0xc5, 0x0f, 0x51, 0x23, 0xc9, 0xa6, 0xc6, 0x5f, 0x0c, 0x6d, 0x6b, 0xf9, 0x26, 0xce,
-	0x7b, 0x8a, 0xf2, 0x62, 0x27, 0xca, 0xc7, 0xcb, 0x8b, 0xf3, 0x1c, 0x88, 0x22, 0x25, 0x33, 0x96,
-	0x49, 0x65, 0x28, 0xdb, 0xb1, 0xf1, 0x1e, 0x88, 0x15, 0x77, 0x44, 0x2d, 0x4c, 0xee, 0xf2, 0xed,
-	0x52, 0x5f, 0x41, 0x2f, 0x6d, 0x80, 0x20, 0xd0, 0xa1, 0x28, 0x72, 0x2a, 0x8a, 0x0a, 0x0d, 0x7a,
-	0xe9, 0xad, 0x40, 0x0f, 0xbd, 0xe4, 0xd4, 0xa3, 0x8f, 0x46, 0x80, 0x02, 0x45, 0x0f, 0x0b, 0xc4,
-	0xbe, 0xb4, 0xec, 0x8d, 0xc7, 0x9e, 0x8a, 0xf9, 0xcf, 0xec, 0xec, 0xac, 0x3e, 0x52, 0x39, 0x39,
-	0xf4, 0x64, 0xfe, 0x7f, 0xff, 0xdf, 0xff, 0xbf, 0xb3, 0xf3, 0xff, 0x9a, 0x59, 0x19, 0x5d, 0xec,
-	0xd8, 0xeb, 0x37, 0x7a, 0x9e, 0xdb, 0x77, 0x5b, 0x6e, 0xe7, 0xc6, 0x3a, 0xeb, 0x5d, 0x07, 0x01,
-	0x67, 0x43, 0xac, 0x30, 0xce, 0x76, 0xfb, 0x02, 0x2c, 0xbc, 0xec, 0xb1, 0x9e, 0xeb, 0x0b, 0xfa,
-	0xfa, 0xd6, 0xc6, 0x8d, 0xb6, 0xdb, 0x76, 0x41, 0x80, 0x5f, 0x82, 0x44, 0x9e, 0x25, 0x50, 0xfa,
-	0x36, 0xeb, 0x74, 0x5c, 0xbc, 0x88, 0x26, 0x2c, 0xb6, 0x6d, 0xb7, 0x58, 0xd3, 0x31, 0xbb, 0x2c,
-	0x9f, 0x28, 0x26, 0x66, 0xc7, 0x4b, 0x64, 0x10, 0x18, 0x48, 0xc0, 0x35, 0xb3, 0xcb, 0x86, 0x81,
-	0x91, 0xdb, 0xed, 0x76, 0xde, 0x27, 0x11, 0x44, 0xa8, 0xa6, 0xe7, 0x4e, 0x5a, 0x1d, 0x9b, 0x39,
-	0x7d, 0xe1, 0x64, 0x34, 0x72, 0x22, 0xe0, 0x98, 0x93, 0x08, 0x22, 0x54, 0xd3, 0xe3, 0x3a, 0x3a,
-	0x23, 0x9d, 0x6c, 0x33, 0xcf, 0xb7, 0x5d, 0x27, 0x9f, 0x04, 0x3f, 0xb3, 0x83, 0xc0, 0x98, 0x12,
-	0x9a, 0xfb, 0x42, 0x31, 0x0c, 0x8c, 0x73, 0x9a, 0x2b, 0x89, 0x12, 0x1a, 0x67, 0x91, 0xdf, 0x25,
-	0x50, 0xe6, 0x36, 0x33, 0x2d, 0xe6, 0xe1, 0x05, 0x94, 0xea, 0xef, 0xf5, 0xc4, 0xeb, 0x9d, 0x99,
-	0xbf, 0x70, 0x3d, 0xdc, 0xb8, 0xeb, 0x77, 0x99, 0xef, 0x9b, 0x6d, 0xb6, 0xb6, 0xd7, 0x63, 0xa5,
-	0x8b, 0x83, 0xc0, 0x00, 0xda, 0x30, 0x30, 0x10, 0xf8, 0xe7, 0x02, 0xa1, 0x80, 0x61, 0x0b, 0x4d,
-	0xb4, 0xdc, 0x6e, 0xcf, 0x63, 0x3e, 0xac, 0x6d, 0x14, 0x3c, 0x5d, 0x39, 0xe2, 0x69, 0x31, 0xe2,
-	0x94, 0xae, 0x0d, 0x02, 0x43, 0x37, 0x1a, 0x06, 0xc6, 0xb4, 0x58, 0x77, 0x84, 0x11, 0xaa, 0x33,
-	0xc8, 0xff, 0xa2, 0xa9, 0xc5, 0xce, 0x96, 0xdf, 0x67, 0xde, 0xa2, 0xeb, 0x6c, 0xd8, 0x6d, 0x7c,
-	0x07, 0x8d, 0x6d, 0xb8, 0x1d, 0x8b, 0x79, 0x7e, 0x3e, 0x51, 0x4c, 0xce, 0x4e, 0xcc, 0xe7, 0xa2,
-	0x47, 0x2e, 0x81, 0xa2, 0x64, 0x3c, 0x09, 0x8c, 0x91, 0x41, 0x60, 0x84, 0xc4, 0x61, 0x60, 0x4c,
-	0xc2, 0x63, 0x84, 0x4c, 0x68, 0xa8, 0x20, 0x5f, 0xa7, 0x50, 0x46, 0x18, 0xe1, 0xeb, 0x68, 0xd4,
-	0xb6, 0x64, 0xb8, 0x67, 0x9e, 0x05, 0xc6, 0x68, 0xb5, 0x3c, 0x08, 0x8c, 0x51, 0xdb, 0x1a, 0x06,
-	0x46, 0x16, 0xac, 0x6d, 0x8b, 0x7c, 0xf9, 0xf4, 0xda, 0x68, 0xb5, 0x4c, 0x47, 0x6d, 0x0b, 0x5f,
-	0x47, 0xe9, 0x8e, 0xb9, 0xce, 0x3a, 0x32, 0xb8, 0xf9, 0x41, 0x60, 0x08, 0x60, 0x18, 0x18, 0x13,
-	0xc0, 0x07, 0x89, 0x50, 0x81, 0xe2, 0x9b, 0x68, 0xdc, 0x63, 0xa6, 0xd5, 0x74, 0x9d, 0xce, 0x1e,
-	0x04, 0x32, 0x5b, 0x9a, 0x19, 0x04, 0x46, 0x96, 0x83, 0x75, 0xa7, 0xb3, 0x37, 0x0c, 0x8c, 0x33,
-	0x60, 0x16, 0x02, 0x84, 0x2a, 0x1d, 0x6e, 0x22, 0x6c, 0xb7, 0x1d, 0xd7, 0x63, 0xcd, 0x1e, 0xf3,
-	0xba, 0x36, 0x6c, 0x8d, 0x9f, 0x4f, 0x81, 0x97, 0x37, 0x06, 0x81, 0x31, 0x2d, 0xb4, 0xab, 0x91,
-	0x72, 0x18, 0x18, 0x97, 0xc4, 0xaa, 0x0f, 0x6b, 0x08, 0x3d, 0xca, 0xc6, 0x77, 0xd0, 0x94, 0x7c,
-	0x80, 0xc5, 0x3a, 0xac, 0xcf, 0xf2, 0x69, 0xf0, 0xfd, 0xef, 0x83, 0xc0, 0x98, 0x14, 0x8a, 0x32,
-	0xe0, 0xc3, 0xc0, 0xc0, 0x9a, 0x5b, 0x01, 0x12, 0x1a, 0xe3, 0x60, 0x0b, 0x9d, 0xb7, 0x6c, 0xdf,
-	0x5c, 0xef, 0xb0, 0x66, 0x9f, 0x75, 0x7b, 0x4d, 0xdb, 0xb1, 0xd8, 0x2e, 0xf3, 0xf3, 0x19, 0xf0,
-	0x39, 0x3f, 0x08, 0x0c, 0x2c, 0xf5, 0x6b, 0xac, 0xdb, 0xab, 0x0a, 0xed, 0x30, 0x30, 0xf2, 0xa2,
-	0xa6, 0x8e, 0xa8, 0x08, 0x3d, 0x86, 0x8f, 0xe7, 0x51, 0xa6, 0x67, 0x6e, 0xf9, 0xcc, 0xca, 0x8f,
-	0x81, 0xdf, 0xc2, 0x20, 0x30, 0x24, 0xa2, 0x02, 0x2e, 0x44, 0x42, 0x25, 0xce, 0x93, 0x47, 0x54,
-	0xa9, 0x9f, 0xcf, 0x1d, 0x4e, 0x9e, 0x32, 0x28, 0xa2, 0xe4, 0x91, 0x44, 0xe5, 0x4b, 0xc8, 0x84,
-	0x86, 0x0a, 0xf2, 0x87, 0x0c, 0xca, 0x08, 0x23, 0x5c, 0x52, 0xc9, 0x33, 0x59, 0x9a, 0xe7, 0x0e,
-	0xfe, 0x1c, 0x18, 0x59, 0xa1, 0xab, 0x96, 0x4f, 0x4a, 0xa6, 0x2f, 0x9e, 0x5e, 0x4b, 0x68, 0x09,
-	0x35, 0x87, 0x52, 0x5a, 0xb3, 0x80, 0xda, 0x73, 0x44, 0x9b, 0x10, 0xb5, 0xe7, 0x40, 0x83, 0x00,
-	0x0c, 0x7f, 0x80, 0xc6, 0x4d, 0xcb, 0xe2, 0x35, 0xc2, 0xfc, 0x7c, 0xb2, 0x98, 0xe4, 0x39, 0x3b,
-	0x08, 0x8c, 0x08, 0x1c, 0x06, 0xc6, 0x14, 0x58, 0x49, 0x84, 0xd0, 0x48, 0x87, 0xff, 0x2f, 0x5e,
-	0xb9, 0xa9, 0xc3, 0x3d, 0xe0, 0x87, 0x95, 0x2c, 0xcf, 0xf4, 0x16, 0xf3, 0x64, 0xeb, 0x4b, 0x8b,
-	0x82, 0xe2, 0x99, 0xce, 0x41, 0xd9, 0xf8, 0x44, 0xa6, 0x87, 0x00, 0xa1, 0x4a, 0x87, 0x97, 0xd1,
-	0x64, 0xd7, 0xdc, 0x6d, 0xfa, 0xec, 0xff, 0xb7, 0x98, 0xd3, 0x62, 0x90, 0x33, 0x49, 0xb1, 0x8a,
-	0xae, 0xb9, 0xdb, 0x90, 0xb0, 0x5a, 0x85, 0x86, 0x11, 0xaa, 0x33, 0x70, 0x09, 0x21, 0xdb, 0xe9,
-	0x7b, 0xae, 0xb5, 0xd5, 0x62, 0x9e, 0x4c, 0x11, 0xe8, 0xc0, 0x11, 0xaa, 0x3a, 0x70, 0x04, 0x11,
-	0xaa, 0xe9, 0x71, 0x1b, 0x65, 0x21, 0x77, 0x9b, 0xb6, 0x95, 0xcf, 0x16, 0x13, 0xb3, 0xa9, 0xd2,
-	0x8a, 0x0c, 0xee, 0x18, 0x64, 0x21, 0xc4, 0x36, 0xfc, 0xc9, 0x73, 0x06, 0xd8, 0x55, 0x4b, 0xed,
-	0xbe, 0x94, 0x79, 0xdf, 0x08, 0x69, 0xbf, 0x88, 0x7e, 0xd2, 0x90, 0x8f, 0x7f, 0x84, 0x0a, 0xfe,
-	0x63, 0x9b, 0x57, 0x8a, 0x78, 0x76, 0xdf, 0x76, 0x9d, 0xa6, 0xc7, 0xba, 0xee, 0xb6, 0xd9, 0xf1,
-	0xf3, 0xe3, 0xb0, 0xf8, 0x5b, 0x83, 0xc0, 0xc8, 0x73, 0x56, 0x55, 0x23, 0x51, 0xc9, 0x19, 0x06,
-	0xc6, 0x0c, 0x3c, 0xf1, 0x24, 0x02, 0xa1, 0x27, 0xda, 0xe2, 0x5d, 0xf4, 0x12, 0x73, 0x5a, 0xde,
-	0x5e, 0x0f, 0x1e, 0xdb, 0x33, 0x7d, 0x7f, 0xc7, 0xf5, 0xac, 0x66, 0xdf, 0x7d, 0xcc, 0x9c, 0x3c,
-	0x82, 0xa4, 0xfe, 0x60, 0x10, 0x18, 0x97, 0x22, 0xd2, 0xaa, 0xe4, 0xac, 0x71, 0xca, 0x30, 0x30,
-	0xae, 0xc2, 0xb3, 0x4f, 0xd0, 0x13, 0x7a, 0x92, 0x25, 0xf9, 0x49, 0x02, 0xa5, 0x61, 0x33, 0x78,
-	0x35, 0x8b, 0xa6, 0x2c, 0x5b, 0x30, 0x54, 0xb3, 0x40, 0x8e, 0xb4, 0x6f, 0x89, 0xe3, 0x0a, 0x4a,
-	0x6f, 0xd8, 0x1d, 0xe6, 0xe7, 0x47, 0xa1, 0x96, 0xb1, 0x36, 0x08, 0xec, 0x0e, 0xab, 0x3a, 0x1b,
-	0x6e, 0xe9, 0xb2, 0xac, 0x66, 0x41, 0x54, 0xb5, 0xc4, 0x25, 0x42, 0x05, 0x48, 0xbe, 0x48, 0xa0,
-	0x09, 0x58, 0xc4, 0xbd, 0x9e, 0x65, 0xf6, 0xd9, 0xbf, 0x72, 0x29, 0x9f, 0x4f, 0xa1, 0x6c, 0x68,
-	0xa0, 0x1a, 0x42, 0xe2, 0x14, 0x0d, 0x61, 0x0e, 0xa5, 0x7c, 0xfb, 0x53, 0x06, 0x83, 0x25, 0x29,
-	0xb8, 0x5c, 0x56, 0x5c, 0x2e, 0x10, 0x0a, 0x18, 0xfe, 0x10, 0xa1, 0xae, 0x6b, 0xd9, 0x1b, 0x36,
-	0xb3, 0x9a, 0x3e, 0x14, 0x68, 0xb2, 0x54, 0xe4, 0xdd, 0x23, 0x44, 0x1b, 0xc3, 0xc0, 0x38, 0x2b,
-	0xca, 0x2b, 0x44, 0x08, 0x8d, 0xb4, 0xbc, 0x7f, 0x28, 0x07, 0xeb, 0x7b, 0xf9, 0x49, 0xa8, 0x8c,
-	0x0f, 0xc2, 0xca, 0x68, 0x6c, 0xba, 0x5e, 0x1f, 0xca, 0x41, 0x3d, 0xa6, 0xb4, 0xa7, 0x4a, 0x2d,
-	0x82, 0x08, 0xaf, 0x04, 0x49, 0xa6, 0x1a, 0x15, 0xaf, 0xa0, 0xb1, 0xf0, 0xc0, 0xc3, 0x33, 0x3f,
-	0xd6, 0xa4, 0xef, 0xb3, 0x56, 0xdf, 0xf5, 0x4a, 0xc5, 0xb0, 0x49, 0x6f, 0xab, 0x03, 0x90, 0x28,
-	0xb8, 0xed, 0xf0, 0xe8, 0x13, 0x6a, 0xf0, 0xfb, 0x28, 0xab, 0x9a, 0x09, 0x82, 0x77, 0x85, 0x66,
-	0xe4, 0x47, 0x9d, 0x44, 0x34, 0x23, 0x5f, 0xb5, 0x11, 0xa5, 0xc3, 0x1f, 0xa1, 0xcc, 0x7a, 0xc7,
-	0x6d, 0x3d, 0x0e, 0xa7, 0xc5, 0xb9, 0x68, 0x21, 0x25, 0x8e, 0x43, 0x5c, 0xaf, 0xca, 0xb5, 0x48,
-	0xaa, 0x1a, 0xff, 0x20, 0x12, 0x2a, 0x61, 0x7e, 0x9a, 0xf3, 0xf7, 0xba, 0x1d, 0xdb, 0x79, 0xdc,
-	0xec, 0x9b, 0x5e, 0x9b, 0xf5, 0xf3, 0xd3, 0xd1, 0x69, 0x4e, 0x6a, 0xd6, 0x40, 0xa1, 0x4e, 0x73,
-	0x31, 0x94, 0xd0, 0x38, 0x8b, 0x9f, 0x31, 0x85, 0xeb, 0xe6, 0xa6, 0xe9, 0x6f, 0xe6, 0x31, 0xd4,
-	0x29, 0x74, 0x38, 0x01, 0xdf, 0x36, 0xfd, 0x4d, 0xb5, 0xed, 0x11, 0x44, 0xa8, 0xa6, 0xc7, 0xb7,
-	0xd0, 0xb8, 0xac, 0x4d, 0x66, 0xe5, 0xcf, 0x81, 0x0b, 0x48, 0x05, 0x05, 0xaa, 0x54, 0x50, 0x08,
-	0xa1, 0x91, 0x16, 0x97, 0xe4, 0x39, 0x52, 0x9c, 0xfe, 0x2e, 0x1e, 0x4d, 0xfb, 0x53, 0x1c, 0x24,
-	0x97, 0xd0, 0xc4, 0xe1, 0x53, 0xcd, 0x94, 0xe8, 0xf8, 0xbd, 0xd8, 0x79, 0x46, 0x74, 0xfc, 0x9e,
-	0x7e, 0x92, 0xd1, 0x19, 0xf8, 0x23, 0x2d, 0x2d, 0x1d, 0x3f, 0x3f, 0x51, 0x4c, 0xcc, 0xa6, 0x4b,
-	0xaf, 0xea, 0x79, 0x58, 0xf3, 0x8f, 0xe4, 0x61, 0xcd, 0x27, 0x7f, 0x0f, 0x8c, 0xa4, 0xed, 0xf4,
-	0xa9, 0x46, 0xc3, 0x1b, 0x48, 0xec, 0x52, 0x13, 0xaa, 0x6a, 0x0a, 0x5c, 0x2d, 0x3f, 0x0b, 0x8c,
-	0x49, 0x6a, 0xee, 0x40, 0xe8, 0x1b, 0xf6, 0xa7, 0x8c, 0x6f, 0xd4, 0x7a, 0x28, 0xa8, 0x8d, 0x52,
-	0x48, 0xe8, 0xf8, 0xcb, 0xa7, 0xd7, 0x62, 0x66, 0x34, 0x32, 0xc2, 0xf7, 0x51, 0xb6, 0xd7, 0x31,
-	0xfb, 0x1b, 0xae, 0xd7, 0xcd, 0x9f, 0x81, 0x64, 0xd7, 0xf6, 0x70, 0x55, 0x6a, 0xca, 0x66, 0xdf,
-	0x2c, 0x11, 0x99, 0x66, 0x8a, 0xaf, 0x32, 0x37, 0x04, 0x08, 0x55, 0x3a, 0x5c, 0x46, 0x13, 0x1d,
-	0xb7, 0x65, 0x76, 0x9a, 0x1b, 0x1d, 0xb3, 0xed, 0xe7, 0xff, 0x32, 0x06, 0x9b, 0x0a, 0xd9, 0x01,
-	0xf8, 0x12, 0x87, 0xd5, 0x66, 0x44, 0x10, 0xa1, 0x9a, 0x1e, 0xdf, 0x46, 0x93, 0xb2, 0x8c, 0x44,
-	0x8e, 0xfd, 0x75, 0x0c, 0x32, 0x04, 0x62, 0x23, 0x15, 0x32, 0xcb, 0xa6, 0xf5, 0xea, 0x13, 0x69,
-	0xa6, 0x33, 0xf0, 0xc7, 0xe8, 0xac, 0xed, 0xb8, 0x16, 0x6b, 0xb6, 0x36, 0x4d, 0xa7, 0xcd, 0x78,
-	0x7c, 0x06, 0x63, 0x50, 0x8d, 0x90, 0xff, 0xa0, 0x5b, 0x04, 0x15, 0xc4, 0xe8, 0x9c, 0x9c, 0x9e,
-	0x1a, 0x4a, 0x68, 0x9c, 0x85, 0x77, 0x91, 0x36, 0x56, 0x9a, 0x7d, 0xcf, 0xb4, 0x3b, 0xcc, 0x13,
-	0xf1, 0xfa, 0xdb, 0x18, 0x04, 0xec, 0xc3, 0x41, 0x60, 0x5c, 0x88, 0x38, 0x6b, 0x82, 0x22, 0x83,
-	0x75, 0xf9, 0xd0, 0xc8, 0xd2, 0xb4, 0x2a, 0x23, 0x8e, 0x37, 0xc6, 0xef, 0xf2, 0x53, 0x24, 0x3f,
-	0xe9, 0x5a, 0xf2, 0x48, 0x7b, 0x45, 0x9c, 0x17, 0x01, 0x52, 0xad, 0x48, 0xca, 0x70, 0x60, 0x84,
-	0x5f, 0x98, 0xa2, 0x31, 0xdb, 0xd9, 0x36, 0x3b, 0x76, 0x78, 0x64, 0x7d, 0xef, 0x59, 0x60, 0x20,
-	0x6a, 0xee, 0x54, 0x05, 0x2a, 0x4e, 0x10, 0xf0, 0x53, 0x3b, 0x41, 0x80, 0xcc, 0x4f, 0x10, 0x1a,
-	0x93, 0x86, 0x3c, 0xde, 0x56, 0x1c, 0x37, 0x76, 0x2b, 0xc8, 0x82, 0x6b, 0xd8, 0x56, 0xc7, 0x8d,
-	0xdf, 0x08, 0xc4, 0xb6, 0xc6, 0x50, 0x42, 0xe3, 0xac, 0xf7, 0x53, 0x3f, 0xff, 0xca, 0x18, 0x21,
-	0xdf, 0x26, 0xd0, 0xb8, 0x6a, 0x71, 0x7c, 0xba, 0x40, 0xfc, 0x93, 0x10, 0x7e, 0xa8, 0xe6, 0x4d,
-	0x11, 0x77, 0x51, 0xcd, 0x9b, 0x10, 0x70, 0xc0, 0xf8, 0xf4, 0x74, 0x37, 0x36, 0x7c, 0xd6, 0x87,
-	0xb9, 0x95, 0x14, 0xd3, 0x53, 0x20, 0x6a, 0x7a, 0x0a, 0x91, 0x50, 0x89, 0xe3, 0x37, 0xe5, 0xf4,
-	0x1a, 0x85, 0xb0, 0x5d, 0x3d, 0x7e, 0x7a, 0x85, 0x41, 0x11, 0x43, 0xec, 0x26, 0x1a, 0xdf, 0x61,
-	0xe6, 0x63, 0x91, 0x97, 0xa2, 0x65, 0x40, 0x5f, 0xe7, 0xa0, 0xcc, 0x49, 0x51, 0x1d, 0x21, 0x40,
-	0xa8, 0xd2, 0xc9, 0x77, 0x7c, 0x84, 0x32, 0x62, 0x9c, 0xe0, 0x55, 0x94, 0x6d, 0xb9, 0x5b, 0x4e,
-	0x3f, 0xba, 0x54, 0x4e, 0xeb, 0xa7, 0x61, 0xd0, 0x94, 0xfe, 0x2d, 0x2c, 0xc0, 0x90, 0xaa, 0x62,
-	0x24, 0x01, 0x7e, 0x8c, 0x95, 0x2a, 0xf2, 0x59, 0x02, 0x8d, 0x49, 0x43, 0x7c, 0x5b, 0x5d, 0x0e,
-	0x52, 0xa5, 0xf7, 0x0e, 0x4d, 0xc9, 0xef, 0xbe, 0x68, 0xea, 0x13, 0x52, 0xde, 0x39, 0xb7, 0xcd,
-	0xce, 0x96, 0xd8, 0xa8, 0x94, 0xb8, 0x73, 0x02, 0xa0, 0x86, 0x0e, 0x48, 0x84, 0x0a, 0x94, 0x7c,
-	0x96, 0x42, 0x93, 0x7a, 0x13, 0xe1, 0xed, 0x7a, 0xcb, 0xb1, 0x77, 0x61, 0x31, 0xb1, 0x53, 0xca,
-	0x3d, 0xc7, 0xde, 0x85, 0x36, 0x53, 0x78, 0x12, 0x18, 0x09, 0x1e, 0x00, 0xce, 0x53, 0x01, 0xe0,
-	0x02, 0xa1, 0x80, 0xe1, 0x8f, 0xd1, 0xd8, 0x8e, 0xed, 0x58, 0xee, 0x8e, 0x0f, 0xcb, 0x98, 0xd0,
-	0x6f, 0x0e, 0x0f, 0x84, 0x02, 0x3c, 0x15, 0xa5, 0xa7, 0x90, 0xad, 0xb6, 0x4b, 0xca, 0x84, 0x86,
-	0x1a, 0xbc, 0x8c, 0xd2, 0x1d, 0xdb, 0xd9, 0xda, 0x85, 0x04, 0x8b, 0x8d, 0xd9, 0x4f, 0xcc, 0x7e,
-	0xdf, 0x03, 0x77, 0x57, 0xa4, 0x3b, 0xc1, 0x8c, 0x2e, 0xd9, 0x5c, 0xe2, 0x97, 0x6c, 0xfe, 0x2f,
-	0xbe, 0x83, 0x32, 0x96, 0xe9, 0xed, 0xd8, 0xe2, 0x52, 0x73, 0x82, 0xa7, 0x19, 0xe9, 0x49, 0x52,
-	0xa3, 0x0b, 0x1e, 0x88, 0x84, 0x4a, 0x1c, 0x33, 0x34, 0xb6, 0xe1, 0x31, 0xb6, 0xee, 0x5b, 0x70,
-	0x48, 0x3a, 0xc1, 0xdb, 0xbb, 0xdc, 0x1b, 0xbf, 0x06, 0x2c, 0x79, 0x8c, 0x95, 0x1a, 0x70, 0x0d,
-	0x90, 0x66, 0xea, 0x8d, 0xa5, 0x0c, 0xd7, 0x00, 0x49, 0xa3, 0x21, 0x09, 0x37, 0x51, 0xc6, 0x61,
-	0x7d, 0xfe, 0x94, 0xcc, 0xc9, 0x4f, 0x99, 0x97, 0x4f, 0xc9, 0xd4, 0x58, 0x5f, 0x3c, 0x44, 0x1a,
-	0xa9, 0xd5, 0x0b, 0x91, 0x3f, 0x42, 0x72, 0xa8, 0x64, 0x90, 0xcf, 0x47, 0x51, 0x36, 0x8c, 0x2f,
-	0x3f, 0xfc, 0xb9, 0x3b, 0x0e, 0xf3, 0xf4, 0xaf, 0x5b, 0x30, 0xf1, 0x01, 0x95, 0xd7, 0x33, 0x31,
-	0xc8, 0x14, 0x42, 0x68, 0xa4, 0xe5, 0x0e, 0xda, 0x9e, 0xbb, 0xd5, 0xd3, 0xbf, 0x6c, 0x81, 0x03,
-	0x40, 0x63, 0x0e, 0x14, 0x42, 0x68, 0xa4, 0xc5, 0x37, 0x51, 0x72, 0xcb, 0xb6, 0x20, 0xd4, 0xe9,
-	0xd2, 0xab, 0xcf, 0x02, 0x23, 0x79, 0x0f, 0x2a, 0x80, 0xa3, 0xc3, 0xc0, 0x18, 0x17, 0x09, 0x67,
-	0x5b, 0xda, 0xf8, 0xe4, 0x0c, 0xca, 0xf5, 0xdc, 0xb8, 0x6d, 0x5b, 0x10, 0x5d, 0x69, 0xbc, 0x2c,
-	0x8c, 0xdb, 0x9a, 0x71, 0x3b, 0x6e, 0xbc, 0xcc, 0x8d, 0x39, 0xf6, 0xcb, 0x04, 0x9a, 0xd0, 0x32,
-	0xf4, 0x87, 0xef, 0xc5, 0x0a, 0x3a, 0x23, 0x1c, 0xd8, 0x7e, 0x13, 0x5e, 0x10, 0xf6, 0x43, 0x7e,
-	0x36, 0x01, 0x4d, 0xd5, 0x5f, 0xe6, 0xb8, 0xfa, 0x6c, 0xa2, 0x83, 0x84, 0xc6, 0x38, 0xa4, 0x81,
-	0xc6, 0x55, 0xc0, 0xf1, 0x12, 0xca, 0xec, 0x72, 0x21, 0x6c, 0x48, 0x67, 0x0f, 0x65, 0x45, 0x74,
-	0xec, 0x14, 0x34, 0x55, 0x10, 0x20, 0x12, 0x2a, 0x61, 0xd2, 0x42, 0x69, 0xe0, 0xbf, 0xd0, 0x6d,
-	0x22, 0xd6, 0x67, 0x26, 0xff, 0x79, 0x9f, 0xf9, 0x71, 0x0a, 0x8d, 0x51, 0x7e, 0x68, 0xf6, 0xfb,
-	0xf8, 0x1d, 0xd5, 0xed, 0xd2, 0xa5, 0x57, 0x4e, 0x6a, 0x6f, 0x51, 0x74, 0xc2, 0xaf, 0x1f, 0xd1,
-	0xa5, 0x6b, 0xf4, 0xd4, 0x97, 0xae, 0xf0, 0x95, 0x92, 0xa7, 0x78, 0xa5, 0x68, 0x2c, 0xa5, 0x5e,
-	0x78, 0x2c, 0xa5, 0x4f, 0x3f, 0x96, 0xc2, 0x49, 0x99, 0x39, 0xc5, 0xa4, 0xac, 0xa3, 0x33, 0x1b,
-	0x9e, 0xdb, 0x85, 0x6f, 0x64, 0xae, 0x67, 0x7a, 0x7b, 0xf2, 0x54, 0x00, 0xa3, 0x9b, 0x6b, 0xd6,
-	0x42, 0x85, 0x1a, 0xdd, 0x31, 0x94, 0xd0, 0x38, 0x2b, 0x3e, 0x13, 0xb3, 0x2f, 0x36, 0x13, 0xf1,
-	0x2d, 0x94, 0x15, 0x27, 0x5e, 0xc7, 0x85, 0x6b, 0x57, 0xba, 0xf4, 0x32, 0x6f, 0x65, 0x80, 0xd5,
-	0x5c, 0xd5, 0xca, 0xa4, 0xac, 0x5e, 0x3b, 0x24, 0x90, 0xdf, 0x26, 0x50, 0x96, 0x32, 0xbf, 0xe7,
-	0x3a, 0x3e, 0xfb, 0xbe, 0x49, 0x30, 0x87, 0x52, 0x96, 0xd9, 0x37, 0x65, 0xda, 0xc1, 0xee, 0x71,
-	0x59, 0xed, 0x1e, 0x17, 0x08, 0x05, 0x0c, 0x7f, 0x88, 0x52, 0x2d, 0xd7, 0x12, 0xc1, 0x3f, 0xa3,
-	0x37, 0xcd, 0x8a, 0xe7, 0xb9, 0xde, 0xa2, 0x6b, 0xc9, 0x6b, 0x07, 0x27, 0x29, 0x07, 0x5c, 0x20,
-	0x14, 0x30, 0xf2, 0x9b, 0x04, 0xca, 0x95, 0xdd, 0x1d, 0xa7, 0xe3, 0x9a, 0xd6, 0xaa, 0xe7, 0xb6,
-	0x3d, 0xe6, 0xfb, 0xdf, 0xeb, 0xee, 0xdf, 0x44, 0x63, 0x5b, 0xf0, 0xe5, 0x20, 0xbc, 0xfd, 0x5f,
-	0x8b, 0x5f, 0x83, 0x0e, 0x3f, 0x44, 0x7c, 0x66, 0x88, 0x3e, 0x34, 0x4a, 0x63, 0xe5, 0x5f, 0xc8,
-	0x84, 0x86, 0x0a, 0xf2, 0xeb, 0x24, 0x2a, 0x9c, 0xec, 0x08, 0x77, 0xd1, 0x84, 0x60, 0x36, 0xb5,
-	0x4f, 0xfa, 0xb3, 0xa7, 0x59, 0x03, 0x5c, 0xce, 0xe0, 0x52, 0xb0, 0xa5, 0x64, 0x75, 0x29, 0x88,
-	0x20, 0x42, 0x35, 0xfd, 0x0b, 0x7d, 0xa7, 0xd4, 0xae, 0xf2, 0xc9, 0x1f, 0x7e, 0x95, 0x6f, 0xa0,
-	0x29, 0x91, 0xa2, 0xe1, 0x07, 0xe5, 0x54, 0x31, 0x39, 0x9b, 0x2e, 0x5d, 0xe7, 0xdd, 0x76, 0x5d,
-	0x1c, 0x56, 0xc3, 0x4f, 0xc9, 0xd3, 0x51, 0xb2, 0x0a, 0x30, 0xcc, 0xb6, 0xdc, 0x08, 0x8d, 0x71,
-	0xf1, 0x52, 0xec, 0xa6, 0x27, 0x4a, 0xfd, 0x3f, 0x4e, 0x79, 0xb3, 0xd3, 0x6e, 0x72, 0x24, 0x83,
-	0x52, 0xab, 0xb6, 0xd3, 0x26, 0x37, 0x51, 0x7a, 0xb1, 0xe3, 0xfa, 0xd0, 0x71, 0x3c, 0x66, 0xfa,
-	0xae, 0xa3, 0xa7, 0x92, 0x40, 0x54, 0xa8, 0x85, 0x48, 0xa8, 0xc4, 0xe7, 0xbe, 0x4e, 0xa2, 0x09,
-	0xed, 0x2f, 0x30, 0xf8, 0x7f, 0xd0, 0xe5, 0xbb, 0x95, 0x46, 0x63, 0x61, 0xb9, 0xd2, 0x5c, 0x7b,
-	0xb8, 0x5a, 0x69, 0x2e, 0xae, 0xdc, 0x6b, 0xac, 0x55, 0x68, 0x73, 0xb1, 0x5e, 0x5b, 0xaa, 0x2e,
-	0xe7, 0x46, 0x0a, 0x57, 0xf6, 0x0f, 0x8a, 0x79, 0xcd, 0x22, 0xfe, 0xb7, 0x92, 0xff, 0x44, 0x38,
-	0x66, 0x5e, 0xad, 0x95, 0x2b, 0x9f, 0xe4, 0x12, 0x85, 0xf3, 0xfb, 0x07, 0xc5, 0x9c, 0x66, 0x25,
-	0x3e, 0xc1, 0xfd, 0x37, 0x7a, 0xe9, 0x28, 0xbb, 0x79, 0x6f, 0xb5, 0xbc, 0xb0, 0x56, 0xc9, 0x8d,
-	0x16, 0x0a, 0xfb, 0x07, 0xc5, 0x8b, 0x87, 0x8d, 0x64, 0x0a, 0xbe, 0x81, 0xce, 0xc7, 0x4c, 0x69,
-	0xe5, 0xe3, 0x7b, 0x95, 0xc6, 0x5a, 0x2e, 0x59, 0xb8, 0xb8, 0x7f, 0x50, 0xc4, 0x9a, 0x55, 0x38,
-	0x26, 0xe6, 0xd1, 0x85, 0x43, 0x16, 0x8d, 0xd5, 0x7a, 0xad, 0x51, 0xc9, 0xa5, 0x0a, 0x97, 0xf6,
-	0x0f, 0x8a, 0xe7, 0x62, 0x26, 0xb2, 0xab, 0x2c, 0xa2, 0x99, 0x98, 0x4d, 0xb9, 0xfe, 0xa0, 0xb6,
-	0x52, 0x5f, 0x28, 0x37, 0x57, 0x69, 0x7d, 0x99, 0x56, 0x1a, 0x8d, 0x5c, 0xba, 0x60, 0xec, 0x1f,
-	0x14, 0x2f, 0x6b, 0xc6, 0x47, 0x2a, 0x7c, 0x0e, 0x4d, 0xc7, 0x9c, 0xac, 0x56, 0x6b, 0xcb, 0xb9,
-	0x4c, 0xe1, 0xdc, 0xfe, 0x41, 0xf1, 0xac, 0x66, 0xc7, 0x63, 0x79, 0x64, 0xff, 0x16, 0x57, 0xea,
-	0x8d, 0x4a, 0x6e, 0xec, 0xc8, 0xfe, 0x41, 0xc0, 0xe7, 0x7e, 0x95, 0x40, 0xf8, 0xe8, 0x1f, 0xbd,
-	0xf0, 0x7b, 0x28, 0x1f, 0x3a, 0x59, 0xac, 0xdf, 0x5d, 0xe5, 0xeb, 0xac, 0xd6, 0x6b, 0xcd, 0x5a,
-	0xbd, 0x56, 0xc9, 0x8d, 0xc4, 0x76, 0x55, 0xb3, 0xaa, 0xb9, 0x0e, 0xc3, 0x75, 0x74, 0xe9, 0x38,
-	0xcb, 0x95, 0x47, 0x6f, 0xe7, 0x12, 0x85, 0xf9, 0xfd, 0x83, 0xe2, 0x85, 0xa3, 0x86, 0x2b, 0x8f,
-	0xde, 0xfe, 0xe6, 0xa7, 0xaf, 0x1c, 0xaf, 0x98, 0xe3, 0x07, 0x20, 0x7d, 0x69, 0x6f, 0xa2, 0xf3,
-	0xba, 0xe3, 0xbb, 0x95, 0xb5, 0x85, 0xf2, 0xc2, 0xda, 0x42, 0x6e, 0x44, 0xc4, 0x40, 0xa3, 0xde,
-	0x65, 0x7d, 0x13, 0xda, 0xee, 0x6b, 0x68, 0x3a, 0xf6, 0x16, 0x95, 0xfb, 0x15, 0x1a, 0x66, 0x94,
-	0xbe, 0x7e, 0xb6, 0xcd, 0x3c, 0xfc, 0x3a, 0xc2, 0x3a, 0x79, 0x61, 0xe5, 0xc1, 0xc2, 0xc3, 0x46,
-	0x6e, 0xb4, 0x70, 0x61, 0xff, 0xa0, 0x38, 0xad, 0xb1, 0x17, 0x3a, 0x3b, 0xe6, 0x9e, 0x3f, 0xf7,
-	0xfb, 0x51, 0x34, 0xa9, 0x7f, 0x37, 0xc2, 0xaf, 0xa3, 0x73, 0x4b, 0xd5, 0x15, 0x9e, 0x89, 0x4b,
-	0x75, 0x11, 0x01, 0x2e, 0xe6, 0x46, 0xc4, 0xe3, 0x74, 0x2a, 0xff, 0x8d, 0xff, 0x0b, 0xe5, 0x0f,
-	0xd1, 0xcb, 0x55, 0x5a, 0x59, 0x5c, 0xab, 0xd3, 0x87, 0xb9, 0x44, 0xe1, 0x25, 0xbe, 0x61, 0xba,
-	0x4d, 0xd9, 0xf6, 0xa0, 0x05, 0xed, 0xe1, 0x5b, 0xe8, 0xf2, 0x21, 0xc3, 0xc6, 0xc3, 0xbb, 0x2b,
-	0xd5, 0xda, 0x1d, 0xf1, 0xbc, 0xd1, 0xc2, 0xd5, 0xfd, 0x83, 0xe2, 0x25, 0xdd, 0xb6, 0x21, 0x3e,
-	0xc5, 0x71, 0x28, 0x9b, 0xc0, 0xb7, 0x51, 0xf1, 0x04, 0xfb, 0x68, 0x01, 0xc9, 0x02, 0xd9, 0x3f,
-	0x28, 0x5e, 0x39, 0xc6, 0x89, 0x5a, 0x47, 0x36, 0x81, 0xdf, 0x42, 0x17, 0x8f, 0xf7, 0x14, 0xd6,
-	0xc5, 0x31, 0xf6, 0x73, 0x7f, 0x4c, 0xa0, 0x71, 0x35, 0xf5, 0xf8, 0xa6, 0x55, 0x28, 0xad, 0xf3,
-	0x26, 0x51, 0xae, 0x34, 0x6b, 0xf5, 0x26, 0x48, 0xe1, 0xa6, 0x29, 0x5e, 0xcd, 0x85, 0x9f, 0x3c,
-	0xc7, 0x35, 0xfa, 0x72, 0xa5, 0x56, 0xa1, 0xd5, 0xc5, 0x30, 0xa2, 0x8a, 0xbd, 0xcc, 0x1c, 0xe6,
-	0xd9, 0x2d, 0xfc, 0x36, 0xba, 0x14, 0x77, 0xde, 0xb8, 0xb7, 0x78, 0x3b, 0xdc, 0x25, 0x58, 0xa0,
-	0xf6, 0x80, 0xc6, 0x56, 0x6b, 0x13, 0x02, 0xf3, 0x4e, 0xcc, 0xaa, 0x5a, 0xbb, 0xbf, 0xb0, 0x52,
-	0x2d, 0x0b, 0xab, 0x64, 0x21, 0xbf, 0x7f, 0x50, 0x3c, 0xaf, 0xac, 0xe4, 0x07, 0x0e, 0x6e, 0x36,
-	0xf7, 0x4d, 0x02, 0xcd, 0x7c, 0xf7, 0xf0, 0xc2, 0x0f, 0xd0, 0xab, 0xb0, 0x5f, 0x47, 0x5a, 0x81,
-	0xec, 0x5b, 0x62, 0x0f, 0x17, 0x56, 0x57, 0x2b, 0xb5, 0x72, 0x6e, 0xa4, 0x30, 0xbb, 0x7f, 0x50,
-	0xbc, 0xf6, 0xdd, 0x2e, 0x17, 0x7a, 0x3d, 0xe6, 0x58, 0xa7, 0x74, 0xbc, 0x54, 0xa7, 0xcb, 0x95,
-	0xb5, 0x5c, 0xe2, 0x34, 0x8e, 0x97, 0x5c, 0xaf, 0xcd, 0xfa, 0xa5, 0xbb, 0x4f, 0xbe, 0x9d, 0x19,
-	0x79, 0xfa, 0xed, 0xcc, 0xc8, 0x93, 0x67, 0x33, 0x89, 0xa7, 0xcf, 0x66, 0x12, 0x3f, 0x7b, 0x3e,
-	0x33, 0xf2, 0xd5, 0xf3, 0x99, 0xc4, 0xd3, 0xe7, 0x33, 0x23, 0x7f, 0x7a, 0x3e, 0x33, 0xf2, 0xe8,
-	0xb5, 0xb6, 0xdd, 0xdf, 0xdc, 0x5a, 0xbf, 0xde, 0x72, 0xbb, 0x37, 0xfc, 0x3d, 0xa7, 0xd5, 0xdf,
-	0xb4, 0x9d, 0xb6, 0xf6, 0x4b, 0xff, 0xcf, 0x0f, 0xeb, 0x19, 0xf8, 0xf5, 0xd6, 0x3f, 0x02, 0x00,
-	0x00, 0xff, 0xff, 0x68, 0x4a, 0x6e, 0xeb, 0x13, 0x21, 0x00, 0x00,
+	// 3251 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x5a, 0x4b, 0x6c, 0x23, 0x47,
+	0x7a, 0x16, 0x9f, 0xa2, 0x4a, 0x8f, 0xa1, 0x6a, 0x5e, 0x34, 0x67, 0xac, 0x66, 0x6a, 0x67, 0x13,
+	0x59, 0x9b, 0x1d, 0xaf, 0xb5, 0xde, 0x8d, 0x63, 0x3b, 0x36, 0xc4, 0x87, 0x34, 0x5c, 0x6b, 0x48,
+	0xb9, 0xa8, 0x19, 0xaf, 0x07, 0x08, 0x88, 0x16, 0xbb, 0x44, 0x35, 0x86, 0xec, 0x66, 0xba, 0x9b,
+	0x7a, 0x2c, 0x72, 0x49, 0x16, 0x58, 0x2c, 0x74, 0x08, 0x82, 0x3d, 0x05, 0xc1, 0x0a, 0x59, 0xe4,
+	0x92, 0x5b, 0x80, 0x1c, 0x72, 0xd9, 0x53, 0x8e, 0x73, 0x1c, 0x2c, 0x10, 0x20, 0xc8, 0xa1, 0x01,
+	0xcf, 0x5c, 0x12, 0xe6, 0xc6, 0x63, 0x0e, 0x41, 0x50, 0x7f, 0x55, 0x57, 0x57, 0xeb, 0xe1, 0x68,
+	0xec, 0x43, 0x4e, 0xc3, 0xfa, 0xfe, 0xef, 0xff, 0xab, 0xba, 0xea, 0x7f, 0x55, 0x69, 0xd0, 0x9d,
+	0x81, 0xbd, 0xf7, 0xee, 0xc8, 0x73, 0x03, 0xb7, 0xe7, 0x0e, 0xde, 0xdd, 0x63, 0xa3, 0x87, 0x30,
+	0xc0, 0x85, 0x08, 0x2b, 0xcf, 0xb1, 0xe3, 0x40, 0x80, 0xe5, 0xef, 0x78, 0x6c, 0xe4, 0xfa, 0x82,
+	0xbe, 0x37, 0xde, 0x7f, 0xb7, 0xef, 0xf6, 0x5d, 0x18, 0xc0, 0x2f, 0x41, 0x22, 0xff, 0x93, 0x46,
+	0xb9, 0x47, 0x6c, 0x30, 0x70, 0x71, 0x0d, 0xcd, 0x5b, 0xec, 0xd0, 0xee, 0xb1, 0xae, 0x63, 0x0e,
+	0x59, 0x29, 0x55, 0x49, 0xad, 0xce, 0x55, 0xc9, 0x24, 0x34, 0x90, 0x80, 0x5b, 0xe6, 0x90, 0x4d,
+	0x43, 0xa3, 0x78, 0x3c, 0x1c, 0x7c, 0x48, 0x62, 0x88, 0x50, 0x4d, 0xce, 0x8d, 0xf4, 0x06, 0x36,
+	0x73, 0x02, 0x61, 0x24, 0x1d, 0x1b, 0x11, 0x70, 0xc2, 0x48, 0x0c, 0x11, 0xaa, 0xc9, 0x71, 0x1b,
+	0x2d, 0x49, 0x23, 0x87, 0xcc, 0xf3, 0x6d, 0xd7, 0x29, 0x65, 0xc0, 0xce, 0xea, 0x24, 0x34, 0x16,
+	0x85, 0xe4, 0xa9, 0x10, 0x4c, 0x43, 0xe3, 0xa6, 0x66, 0x4a, 0xa2, 0x84, 0x26, 0x59, 0xf8, 0x19,
+	0xba, 0xe1, 0x8c, 0x87, 0xdd, 0x9e, 0xeb, 0x38, 0xac, 0x17, 0xd8, 0xae, 0xe3, 0x97, 0xb2, 0x95,
+	0xd4, 0x6a, 0xae, 0xfa, 0xde, 0x24, 0x34, 0x96, 0x9c, 0xf1, 0xb0, 0x16, 0x4b, 0xa6, 0xa1, 0x71,
+	0x0b, 0x4c, 0x26, 0x61, 0xf2, 0xdf, 0xa1, 0x91, 0xb1, 0x9d, 0x80, 0x9e, 0xa3, 0xe3, 0x4f, 0xd0,
+	0x5c, 0x60, 0x0f, 0x99, 0x1f, 0x98, 0xc3, 0x51, 0x29, 0x57, 0x49, 0xad, 0x66, 0xaa, 0x95, 0x49,
+	0x68, 0xc4, 0xe0, 0x34, 0x34, 0x6e, 0x80, 0x41, 0x85, 0x10, 0x1a, 0x4b, 0xc9, 0x3f, 0xa5, 0x50,
+	0xfe, 0x11, 0x33, 0x2d, 0xe6, 0xe1, 0x0d, 0x94, 0x0d, 0x4e, 0x46, 0x62, 0xeb, 0x97, 0xd6, 0x6f,
+	0x3f, 0x8c, 0x0e, 0xf5, 0xe1, 0x63, 0xe6, 0xfb, 0x66, 0x9f, 0xed, 0x9e, 0x8c, 0x58, 0xf5, 0xce,
+	0x24, 0x34, 0x80, 0x36, 0x0d, 0x0d, 0x24, 0xec, 0x9e, 0x8c, 0x18, 0xa1, 0x80, 0x61, 0x0b, 0xcd,
+	0xf7, 0xdc, 0xe1, 0xc8, 0x63, 0x3e, 0xec, 0x5b, 0x1a, 0x2c, 0xdd, 0xbf, 0x60, 0xa9, 0x16, 0x73,
+	0xaa, 0x0f, 0x26, 0xa1, 0xa1, 0x2b, 0x4d, 0x43, 0x63, 0x59, 0xec, 0x69, 0x8c, 0x11, 0xaa, 0x33,
+	0xc8, 0xaf, 0x53, 0x68, 0xb1, 0x36, 0x18, 0xfb, 0x01, 0xf3, 0x6a, 0xae, 0xb3, 0x6f, 0xf7, 0xf1,
+	0x67, 0x68, 0x76, 0xdf, 0x1d, 0x58, 0xcc, 0xf3, 0x4b, 0xa9, 0x4a, 0x66, 0x75, 0x7e, 0xbd, 0x18,
+	0xcf, 0xb9, 0x09, 0x82, 0xaa, 0xf1, 0x22, 0x34, 0x66, 0x26, 0xa1, 0x11, 0x11, 0xa7, 0xa1, 0xb1,
+	0x00, 0xf3, 0x88, 0x31, 0xa1, 0x91, 0x80, 0x6f, 0xa9, 0xcf, 0x7a, 0xae, 0x63, 0x99, 0xde, 0x09,
+	0x7c, 0x42, 0x41, 0x6c, 0xa9, 0x02, 0xd5, 0x96, 0x2a, 0x84, 0xd0, 0x58, 0x4a, 0x7e, 0x9b, 0x45,
+	0x79, 0x31, 0x29, 0x7e, 0x88, 0xd2, 0xb6, 0x25, 0x7d, 0x79, 0xe5, 0x55, 0x68, 0xa4, 0x9b, 0xf5,
+	0x49, 0x68, 0xa4, 0x6d, 0x6b, 0x1a, 0x1a, 0x05, 0x30, 0x61, 0x5b, 0xe4, 0x57, 0x2f, 0x1f, 0xa4,
+	0x9b, 0x75, 0x9a, 0xb6, 0x2d, 0xfc, 0x10, 0xe5, 0x06, 0xe6, 0x1e, 0x1b, 0x48, 0xcf, 0x2d, 0x4d,
+	0x42, 0x43, 0x00, 0xd3, 0xd0, 0x98, 0x07, 0x3e, 0x8c, 0x08, 0x15, 0x28, 0xfe, 0x08, 0xcd, 0x79,
+	0xcc, 0xb4, 0xba, 0xae, 0x33, 0x38, 0x01, 0x2f, 0x2d, 0x54, 0x57, 0x26, 0xa1, 0x51, 0xe0, 0x60,
+	0xdb, 0x19, 0xf0, 0x95, 0x2e, 0x81, 0x5a, 0x04, 0x10, 0xaa, 0x64, 0xb8, 0x8b, 0xb0, 0xdd, 0x77,
+	0x5c, 0x8f, 0x75, 0x47, 0xcc, 0x1b, 0xda, 0xb0, 0xb7, 0xc2, 0x33, 0x0b, 0xd5, 0x1f, 0x4c, 0x42,
+	0x63, 0x59, 0x48, 0x77, 0x62, 0xe1, 0x34, 0x34, 0xee, 0x8a, 0x55, 0x9f, 0x97, 0x10, 0x7a, 0x91,
+	0x8d, 0x3f, 0x43, 0x8b, 0x72, 0x02, 0x8b, 0x0d, 0x58, 0xc0, 0xc0, 0x3f, 0x0b, 0xd5, 0xdf, 0x9f,
+	0x84, 0xc6, 0x82, 0x10, 0xd4, 0x01, 0x9f, 0x86, 0x06, 0xd6, 0xcc, 0x0a, 0x90, 0xd0, 0x04, 0x07,
+	0x5b, 0xe8, 0x96, 0x65, 0xfb, 0xe6, 0xde, 0x80, 0x75, 0x03, 0x36, 0x1c, 0x75, 0x6d, 0xc7, 0x62,
+	0xc7, 0xcc, 0x2f, 0xe5, 0xc1, 0xe6, 0xfa, 0x24, 0x34, 0xb0, 0x94, 0xef, 0xb2, 0xe1, 0xa8, 0x29,
+	0xa4, 0xd3, 0xd0, 0x28, 0x89, 0x84, 0x71, 0x41, 0x44, 0xe8, 0x25, 0x7c, 0xbc, 0x8e, 0xf2, 0x23,
+	0x73, 0xec, 0x33, 0xab, 0x34, 0x0b, 0x76, 0xcb, 0x93, 0xd0, 0x90, 0x88, 0x72, 0x18, 0x31, 0x24,
+	0x54, 0xe2, 0xdc, 0xf9, 0x44, 0x0a, 0xf2, 0x4b, 0xc5, 0xf3, 0xce, 0x57, 0x07, 0x41, 0xec, 0x7c,
+	0x92, 0xa8, 0x6c, 0x89, 0x31, 0xa1, 0x91, 0x80, 0xfc, 0x4b, 0x1e, 0xe5, 0x85, 0x12, 0xae, 0x2a,
+	0xe7, 0x59, 0xa8, 0xae, 0x73, 0x03, 0xff, 0x1e, 0x1a, 0x05, 0x21, 0x6b, 0xd6, 0xaf, 0x72, 0xa6,
+	0x5f, 0xbe, 0x7c, 0x90, 0xd2, 0x1c, 0x6a, 0x0d, 0x65, 0xb5, 0x4c, 0x08, 0xc1, 0xeb, 0x88, 0x1c,
+	0x28, 0x82, 0xd7, 0x81, 0xec, 0x07, 0x18, 0xfe, 0x18, 0xcd, 0x99, 0x96, 0xc5, 0x83, 0x8c, 0xf9,
+	0xa5, 0x4c, 0x25, 0xc3, 0x7d, 0x96, 0xfb, 0xbd, 0x02, 0xa7, 0xa1, 0xb1, 0x08, 0x5a, 0x12, 0x21,
+	0x34, 0x96, 0xe1, 0x3f, 0x4d, 0x86, 0x7e, 0xf6, 0x7c, 0x12, 0xf9, 0x76, 0x31, 0xcf, 0x3d, 0xbd,
+	0xc7, 0x3c, 0x99, 0xd7, 0x73, 0x22, 0xa0, 0xb8, 0xa7, 0x73, 0x50, 0x66, 0x75, 0xe1, 0xe9, 0x11,
+	0x40, 0xa8, 0x92, 0xe1, 0x2d, 0xb4, 0x30, 0x34, 0x8f, 0xbb, 0x3e, 0xfb, 0xb3, 0x31, 0x73, 0x7a,
+	0x0c, 0x7c, 0x26, 0x23, 0x56, 0x31, 0x34, 0x8f, 0x3b, 0x12, 0x56, 0xab, 0xd0, 0x30, 0x42, 0x75,
+	0x06, 0xae, 0x22, 0x64, 0x3b, 0x81, 0xe7, 0x5a, 0xe3, 0x1e, 0xf3, 0xa4, 0x8b, 0x40, 0x79, 0x89,
+	0x51, 0x55, 0x5e, 0x62, 0x88, 0x50, 0x4d, 0x8e, 0xfb, 0xa8, 0x00, 0xbe, 0xdb, 0xb5, 0xad, 0x52,
+	0xa1, 0x92, 0x5a, 0xcd, 0x56, 0xb7, 0xe5, 0xe1, 0xce, 0x82, 0x17, 0xc2, 0xd9, 0x46, 0x3f, 0xb9,
+	0xcf, 0x00, 0xbb, 0x69, 0xa9, 0xdd, 0x97, 0x63, 0x9e, 0x37, 0x22, 0xda, 0xdf, 0xc6, 0x3f, 0x69,
+	0xc4, 0xc7, 0x7f, 0x8e, 0xca, 0xfe, 0x73, 0x9b, 0x47, 0x8a, 0x98, 0x9b, 0x17, 0x8c, 0xae, 0xc7,
+	0x86, 0xee, 0xa1, 0x39, 0xf0, 0x4b, 0x73, 0xb0, 0xf8, 0x4f, 0x26, 0xa1, 0x51, 0xe2, 0xac, 0xa6,
+	0x46, 0xa2, 0x92, 0x33, 0x0d, 0x8d, 0x15, 0x91, 0xe7, 0xae, 0x20, 0x10, 0x7a, 0xa5, 0x2e, 0x3e,
+	0x46, 0x6f, 0x31, 0xa7, 0xe7, 0x9d, 0x8c, 0x60, 0xda, 0x91, 0xe9, 0xfb, 0x47, 0xae, 0x67, 0x75,
+	0x03, 0xf7, 0x39, 0x73, 0x4a, 0x08, 0x9c, 0xfa, 0xe3, 0x49, 0x68, 0xdc, 0x8d, 0x49, 0x3b, 0x92,
+	0xb3, 0xcb, 0x29, 0xd3, 0xd0, 0x78, 0x1b, 0xe6, 0xbe, 0x42, 0x4e, 0xe8, 0x55, 0x9a, 0xe4, 0x2f,
+	0x53, 0x28, 0x07, 0x9b, 0xc1, 0xa3, 0x59, 0x24, 0x75, 0x99, 0x82, 0x21, 0x9a, 0x05, 0x72, 0x21,
+	0xfd, 0x4b, 0x1c, 0x37, 0x50, 0x6e, 0xdf, 0x1e, 0x30, 0xbf, 0x94, 0x86, 0x58, 0xc6, 0x5a, 0x21,
+	0xb1, 0x07, 0xac, 0xe9, 0xec, 0xbb, 0xd5, 0x7b, 0x32, 0x9a, 0x05, 0x51, 0xc5, 0x12, 0x1f, 0x11,
+	0x2a, 0x40, 0xf2, 0xcb, 0x14, 0x9a, 0x87, 0x45, 0x3c, 0x19, 0x59, 0x66, 0xc0, 0xfe, 0x3f, 0x97,
+	0xf2, 0x8b, 0x45, 0x54, 0x88, 0x14, 0x54, 0x42, 0x48, 0x5d, 0x23, 0x21, 0xac, 0xa1, 0xac, 0x6f,
+	0xff, 0x8c, 0x41, 0x61, 0xc9, 0x08, 0x2e, 0x1f, 0x2b, 0x2e, 0x1f, 0x10, 0x0a, 0x18, 0xfe, 0x14,
+	0xa1, 0xa1, 0x6b, 0xd9, 0xfb, 0x36, 0xb3, 0xba, 0xbe, 0xde, 0x88, 0x44, 0x68, 0x47, 0x55, 0x4d,
+	0x85, 0x10, 0x1a, 0x4b, 0x79, 0xfe, 0x50, 0x06, 0xf6, 0x4e, 0x4a, 0x0b, 0x10, 0x19, 0x1f, 0x47,
+	0x91, 0xd1, 0x39, 0x70, 0xbd, 0x00, 0xc2, 0x41, 0x4d, 0x53, 0x3d, 0x51, 0xa1, 0x16, 0x43, 0x84,
+	0x47, 0x82, 0x24, 0x53, 0x8d, 0x8a, 0xb7, 0xd1, 0x6c, 0xd4, 0xcd, 0x71, 0xcf, 0x4f, 0x24, 0xe9,
+	0xa7, 0xac, 0x17, 0xb8, 0x5e, 0xb5, 0x12, 0x25, 0xe9, 0x43, 0xd5, 0xdd, 0x89, 0x80, 0x3b, 0x8c,
+	0xfa, 0xba, 0x48, 0x82, 0x3f, 0x44, 0x05, 0x95, 0x4c, 0x10, 0x7c, 0x2b, 0x24, 0x23, 0x3f, 0xce,
+	0x24, 0x4b, 0xb2, 0x41, 0x88, 0xd2, 0x88, 0x92, 0xe1, 0x9f, 0xa0, 0xfc, 0xde, 0xc0, 0xed, 0x3d,
+	0x8f, 0xaa, 0xc5, 0xcd, 0x78, 0x21, 0x55, 0x8e, 0xc3, 0xb9, 0xbe, 0x2d, 0xd7, 0x22, 0xa9, 0xaa,
+	0xfc, 0xc3, 0x90, 0x50, 0x09, 0xf3, 0x56, 0xd5, 0x3f, 0x19, 0x0e, 0x6c, 0xe7, 0x79, 0x37, 0x30,
+	0xbd, 0x3e, 0x0b, 0x4a, 0xcb, 0x71, 0xab, 0x2a, 0x25, 0xbb, 0x20, 0x50, 0xad, 0x6a, 0x02, 0x25,
+	0x34, 0xc9, 0xe2, 0x0d, 0xb4, 0x30, 0xdd, 0x3d, 0x30, 0xfd, 0x83, 0x12, 0x86, 0x38, 0x85, 0x0c,
+	0x27, 0xe0, 0x47, 0xa6, 0x7f, 0xa0, 0xb6, 0x3d, 0x86, 0x08, 0xd5, 0xe4, 0xbc, 0x81, 0x92, 0xb1,
+	0xc9, 0xac, 0xd2, 0x4d, 0x30, 0x01, 0xae, 0xa0, 0x40, 0xe5, 0x0a, 0x0a, 0x21, 0x34, 0x96, 0xe2,
+	0xaa, 0x6c, 0x44, 0x45, 0xfb, 0x78, 0xe7, 0xa2, 0xdb, 0x5f, 0xa3, 0x13, 0xdd, 0x44, 0xf3, 0xe7,
+	0xbb, 0x9a, 0x45, 0x91, 0xf1, 0x47, 0x89, 0x7e, 0x46, 0x64, 0xfc, 0x91, 0xde, 0xc9, 0xe8, 0x0c,
+	0xfc, 0x13, 0xcd, 0x2d, 0x1d, 0xbf, 0x34, 0x0f, 0x7d, 0xfb, 0x3b, 0xba, 0x1f, 0xb6, 0xfc, 0x0b,
+	0x7e, 0xd8, 0x8a, 0xfb, 0x75, 0x8d, 0x86, 0xf7, 0x91, 0xd8, 0xa5, 0x2e, 0x44, 0xd5, 0x22, 0x98,
+	0xda, 0x7a, 0x15, 0x1a, 0x0b, 0xd4, 0x3c, 0x82, 0xa3, 0xef, 0xd8, 0x3f, 0x63, 0x7c, 0xa3, 0xf6,
+	0xa2, 0x81, 0xda, 0x28, 0x85, 0x44, 0x86, 0x7f, 0xf5, 0xf2, 0x41, 0x42, 0x8d, 0xc6, 0x4a, 0xf8,
+	0x29, 0x2a, 0x8c, 0x06, 0x66, 0xb0, 0xef, 0x7a, 0xc3, 0xd2, 0x12, 0x38, 0xbb, 0xb6, 0x87, 0x3b,
+	0x52, 0x52, 0x37, 0x03, 0xb3, 0x4a, 0xa4, 0x9b, 0x29, 0xbe, 0xf2, 0xdc, 0x08, 0x20, 0x54, 0xc9,
+	0x70, 0x1d, 0xcd, 0x0f, 0xdc, 0x9e, 0x39, 0xe8, 0xee, 0x0f, 0xcc, 0xbe, 0x5f, 0xfa, 0x8f, 0x59,
+	0xd8, 0x54, 0xf0, 0x0e, 0xc0, 0x37, 0x39, 0xac, 0x36, 0x23, 0x86, 0x08, 0xd5, 0xe4, 0xf8, 0x11,
+	0x5a, 0x90, 0x61, 0x24, 0x7c, 0xec, 0x3f, 0x67, 0xc1, 0x43, 0xe0, 0x6c, 0xa4, 0x40, 0x7a, 0xd9,
+	0xb2, 0x1e, 0x7d, 0xc2, 0xcd, 0x74, 0x06, 0xfe, 0x1c, 0xdd, 0xb0, 0x1d, 0xd7, 0x62, 0xdd, 0xde,
+	0x81, 0xe9, 0xf4, 0x19, 0x3f, 0x9f, 0xc9, 0x2c, 0x44, 0x23, 0xf8, 0x3f, 0xc8, 0x6a, 0x20, 0x82,
+	0x33, 0xba, 0x29, 0xab, 0xa7, 0x86, 0x12, 0x9a, 0x64, 0xe1, 0x63, 0xa4, 0x95, 0x95, 0x6e, 0xe0,
+	0x99, 0xf6, 0x80, 0x79, 0xe2, 0xbc, 0xfe, 0x6b, 0x16, 0x0e, 0xec, 0xd3, 0x49, 0x68, 0xdc, 0x8e,
+	0x39, 0xbb, 0x82, 0x22, 0x0f, 0xeb, 0xde, 0xb9, 0x92, 0xa5, 0x49, 0x95, 0x47, 0x5c, 0xae, 0x8c,
+	0x7f, 0xcc, 0xbb, 0x48, 0xde, 0xe9, 0x5a, 0xb2, 0xa5, 0xbd, 0x2f, 0xfa, 0x45, 0x80, 0x54, 0x2a,
+	0x92, 0x63, 0x68, 0x18, 0xe1, 0x17, 0xa6, 0x68, 0xd6, 0x76, 0x0e, 0xcd, 0x81, 0x1d, 0xb5, 0xac,
+	0x1f, 0xbc, 0x0a, 0x0d, 0x44, 0xcd, 0xa3, 0xa6, 0x40, 0x45, 0x07, 0x01, 0x3f, 0xb5, 0x0e, 0x02,
+	0xc6, 0xbc, 0x83, 0xd0, 0x98, 0x34, 0xe2, 0xf1, 0xb4, 0xe2, 0xb8, 0x89, 0x5b, 0x41, 0x01, 0x4c,
+	0xc3, 0xb6, 0x3a, 0x6e, 0xf2, 0x46, 0x20, 0xb6, 0x35, 0x81, 0x12, 0x9a, 0x64, 0x7d, 0x98, 0xfd,
+	0x9b, 0xdf, 0x18, 0x33, 0xe4, 0xab, 0x14, 0x9a, 0x53, 0x29, 0x8e, 0x57, 0x17, 0x38, 0xff, 0x0c,
+	0x1c, 0x3f, 0x44, 0xf3, 0x81, 0x38, 0x77, 0x11, 0xcd, 0x07, 0x70, 0xe0, 0x80, 0xf1, 0xea, 0xe9,
+	0xee, 0xef, 0xfb, 0x2c, 0x80, 0xba, 0x95, 0x11, 0xd5, 0x53, 0x20, 0xaa, 0x7a, 0x8a, 0x21, 0xa1,
+	0x12, 0xc7, 0xef, 0xc9, 0xea, 0x95, 0x86, 0x63, 0x7b, 0xfb, 0xf2, 0xea, 0x15, 0x1d, 0x8a, 0x28,
+	0x62, 0x1f, 0xa1, 0xb9, 0x23, 0x66, 0x3e, 0x17, 0x7e, 0x29, 0x52, 0x06, 0xe4, 0x75, 0x0e, 0x4a,
+	0x9f, 0x14, 0xd1, 0x11, 0x01, 0x84, 0x2a, 0x99, 0xfc, 0xc6, 0x67, 0x28, 0x2f, 0xca, 0x09, 0xde,
+	0x41, 0x85, 0x9e, 0x3b, 0x76, 0x82, 0xf8, 0x52, 0xba, 0xac, 0x77, 0xc3, 0x20, 0xa9, 0xfe, 0x5e,
+	0x14, 0x80, 0x11, 0x55, 0x9d, 0x91, 0x04, 0x78, 0x1b, 0x2b, 0x45, 0xe4, 0xe7, 0x29, 0x34, 0x2b,
+	0x15, 0xf1, 0x23, 0x75, 0x39, 0xc8, 0x56, 0x3f, 0x38, 0x57, 0x25, 0xbf, 0xfe, 0xa2, 0xa9, 0x57,
+	0x48, 0x79, 0xe7, 0x3c, 0x34, 0x07, 0x63, 0xb1, 0x51, 0x59, 0x71, 0xe7, 0x04, 0x40, 0x15, 0x1d,
+	0x18, 0x11, 0x2a, 0x50, 0xf2, 0xf3, 0x2c, 0x5a, 0xd0, 0x93, 0x08, 0x4f, 0xd7, 0x63, 0xc7, 0x3e,
+	0x86, 0xc5, 0x24, 0xba, 0x94, 0x27, 0x8e, 0x7d, 0x0c, 0x69, 0xa6, 0xfc, 0x22, 0x34, 0x52, 0xfc,
+	0x00, 0x38, 0x4f, 0x1d, 0x00, 0x1f, 0x10, 0x0a, 0x18, 0xfe, 0x1c, 0xcd, 0x1e, 0xd9, 0x8e, 0xe5,
+	0x1e, 0xf9, 0xb0, 0x8c, 0x79, 0xfd, 0xe6, 0xf0, 0x85, 0x10, 0x80, 0xa5, 0x8a, 0xb4, 0x14, 0xb1,
+	0xd5, 0x76, 0xc9, 0x31, 0xa1, 0x91, 0x04, 0x6f, 0xa1, 0xdc, 0xc0, 0x76, 0xc6, 0xc7, 0xe0, 0x60,
+	0x89, 0x32, 0xfb, 0x53, 0x33, 0x08, 0x3c, 0x30, 0x77, 0x5f, 0x9a, 0x13, 0xcc, 0xf8, 0x92, 0xcd,
+	0x47, 0xfc, 0x92, 0xcd, 0xff, 0xc5, 0x9f, 0xa1, 0xbc, 0x65, 0x7a, 0x47, 0xb6, 0xb8, 0xd4, 0x5c,
+	0x61, 0x69, 0x45, 0x5a, 0x92, 0xd4, 0xf8, 0x82, 0x07, 0x43, 0x42, 0x25, 0x8e, 0x19, 0x9a, 0xdd,
+	0xf7, 0x18, 0xdb, 0xf3, 0x2d, 0x68, 0x92, 0xae, 0xb0, 0xf6, 0x63, 0x6e, 0x8d, 0x5f, 0x03, 0x36,
+	0x3d, 0xc6, 0xaa, 0x1d, 0xb8, 0x06, 0x48, 0x35, 0xf5, 0xc5, 0x72, 0x0c, 0xd7, 0x00, 0x49, 0xa3,
+	0x11, 0x09, 0x77, 0x51, 0xde, 0x61, 0x01, 0x9f, 0x25, 0x7f, 0xf5, 0x2c, 0xeb, 0x72, 0x96, 0x7c,
+	0x8b, 0x05, 0x62, 0x12, 0xa9, 0xa4, 0x56, 0x2f, 0x86, 0x7c, 0x0a, 0xc9, 0xa1, 0x92, 0x41, 0x7e,
+	0x91, 0x46, 0x85, 0xe8, 0x7c, 0x79, 0xf3, 0xe7, 0x1e, 0x39, 0xcc, 0xd3, 0x9f, 0xee, 0xa0, 0xe2,
+	0x03, 0x2a, 0xaf, 0x67, 0xa2, 0x90, 0x29, 0x84, 0xd0, 0x58, 0xca, 0x0d, 0xf4, 0x3d, 0x77, 0x3c,
+	0xd2, 0x9f, 0xed, 0xc0, 0x00, 0xa0, 0x09, 0x03, 0x0a, 0x21, 0x34, 0x96, 0xe2, 0x8f, 0x50, 0x66,
+	0x6c, 0x5b, 0x70, 0xd4, 0xb9, 0xea, 0x3b, 0xaf, 0x42, 0x23, 0xf3, 0x04, 0x22, 0x80, 0xa3, 0xd3,
+	0xd0, 0x98, 0x13, 0x0e, 0x67, 0x5b, 0x5a, 0xf9, 0xe4, 0x0c, 0xca, 0xe5, 0x5c, 0xb9, 0x6f, 0x5b,
+	0xf2, 0x4d, 0x0e, 0x94, 0xb7, 0x84, 0x72, 0x5f, 0x53, 0xee, 0x27, 0x95, 0xb7, 0xb8, 0x32, 0xc7,
+	0x7e, 0x9d, 0x42, 0xf3, 0x9a, 0x87, 0x7e, 0xfb, 0xbd, 0xd8, 0x46, 0x4b, 0xc2, 0x80, 0xed, 0x77,
+	0xe1, 0x03, 0xe5, 0x1b, 0x14, 0x3c, 0x9b, 0x80, 0xa4, 0xe9, 0x6f, 0x71, 0x5c, 0x3d, 0x9b, 0xe8,
+	0x20, 0xa1, 0x09, 0x0e, 0xe9, 0xa0, 0x39, 0x75, 0xe0, 0x78, 0x13, 0xe5, 0x8f, 0xf9, 0x20, 0x4a,
+	0x48, 0x37, 0xce, 0x79, 0x45, 0xdc, 0x76, 0x0a, 0x9a, 0x0a, 0x08, 0x18, 0x12, 0x2a, 0x61, 0xd2,
+	0x43, 0x39, 0xe0, 0xbf, 0xd1, 0x6d, 0x22, 0x91, 0x67, 0x16, 0xfe, 0xef, 0x3c, 0xf3, 0x17, 0x59,
+	0x34, 0x4b, 0x79, 0xd3, 0xec, 0x07, 0xf8, 0x47, 0x2a, 0xdb, 0xe5, 0xaa, 0xdf, 0xbd, 0x2a, 0xbd,
+	0xc5, 0xa7, 0x13, 0xbd, 0x7e, 0xc4, 0x97, 0xae, 0xf4, 0xb5, 0x2f, 0x5d, 0xd1, 0x27, 0x65, 0xae,
+	0xf1, 0x49, 0x71, 0x59, 0xca, 0xbe, 0x71, 0x59, 0xca, 0x5d, 0xbf, 0x2c, 0x45, 0x95, 0x32, 0x7f,
+	0x8d, 0x4a, 0xd9, 0x46, 0x4b, 0xfb, 0x9e, 0x3b, 0x84, 0x37, 0x32, 0xd7, 0x33, 0xbd, 0x13, 0xd9,
+	0x15, 0x40, 0xe9, 0xe6, 0x92, 0xdd, 0x48, 0xa0, 0x4a, 0x77, 0x02, 0x25, 0x34, 0xc9, 0x4a, 0xd6,
+	0xc4, 0xc2, 0x9b, 0xd5, 0x44, 0xfc, 0x09, 0x2a, 0x88, 0x8e, 0xd7, 0x71, 0xe1, 0xda, 0x95, 0xab,
+	0x7e, 0x87, 0xa7, 0x32, 0xc0, 0x5a, 0xae, 0x4a, 0x65, 0x72, 0xac, 0x3e, 0x3b, 0x22, 0x90, 0x7f,
+	0x4c, 0xa1, 0x02, 0x65, 0xfe, 0xc8, 0x75, 0x7c, 0xf6, 0x4d, 0x9d, 0x60, 0x0d, 0x65, 0x2d, 0x33,
+	0x30, 0xa5, 0xdb, 0xc1, 0xee, 0xf1, 0xb1, 0xda, 0x3d, 0x3e, 0x20, 0x14, 0x30, 0xfc, 0x29, 0xca,
+	0xf6, 0x5c, 0x4b, 0x1c, 0xfe, 0x92, 0x9e, 0x34, 0x1b, 0x9e, 0xe7, 0x7a, 0x35, 0xd7, 0x92, 0xd7,
+	0x0e, 0x4e, 0x52, 0x06, 0xf8, 0x80, 0x50, 0xc0, 0xc8, 0x3f, 0xa4, 0x50, 0xb1, 0xee, 0x1e, 0x39,
+	0x03, 0xd7, 0xb4, 0x76, 0x3c, 0xb7, 0xef, 0x31, 0xdf, 0xff, 0x46, 0x77, 0xff, 0x2e, 0x9a, 0x1d,
+	0xc3, 0xcb, 0x41, 0x74, 0xfb, 0x7f, 0x90, 0xbc, 0x06, 0x9d, 0x9f, 0x44, 0x3c, 0x33, 0xc4, 0x0f,
+	0x8d, 0x52, 0x59, 0xd9, 0x17, 0x63, 0x42, 0x23, 0x01, 0xf9, 0xfb, 0x0c, 0x2a, 0x5f, 0x6d, 0x08,
+	0x0f, 0xd1, 0xbc, 0x60, 0x76, 0xb5, 0xbf, 0x09, 0xac, 0x5e, 0x67, 0x0d, 0x70, 0x39, 0x83, 0x4b,
+	0xc1, 0x58, 0x8d, 0xd5, 0xa5, 0x20, 0x86, 0x08, 0xd5, 0xe4, 0x6f, 0xf4, 0x4e, 0xa9, 0x5d, 0xe5,
+	0x33, 0xdf, 0xfe, 0x2a, 0xdf, 0x41, 0x8b, 0xc2, 0x45, 0xa3, 0x07, 0xe5, 0x6c, 0x25, 0xb3, 0x9a,
+	0xab, 0x3e, 0xe4, 0xd9, 0x76, 0x4f, 0x34, 0xab, 0xd1, 0x53, 0xf2, 0x72, 0xec, 0xac, 0x02, 0x8c,
+	0xbc, 0xad, 0x38, 0x43, 0x13, 0x5c, 0xbc, 0x99, 0xb8, 0xe9, 0x89, 0x50, 0xff, 0x83, 0x6b, 0xde,
+	0xec, 0xb4, 0x9b, 0x1c, 0xc9, 0xa3, 0xec, 0x8e, 0xed, 0xf4, 0xc9, 0x47, 0x28, 0x57, 0x1b, 0xb8,
+	0x3e, 0x64, 0x1c, 0x8f, 0x99, 0xbe, 0xeb, 0xe8, 0xae, 0x24, 0x10, 0x75, 0xd4, 0x62, 0x48, 0xa8,
+	0xc4, 0xd7, 0x7e, 0x9b, 0x41, 0xf3, 0xda, 0x9f, 0x70, 0xf0, 0x9f, 0xa0, 0x7b, 0x8f, 0x1b, 0x9d,
+	0xce, 0xc6, 0x56, 0xa3, 0xbb, 0xfb, 0xe5, 0x4e, 0xa3, 0x5b, 0xdb, 0x7e, 0xd2, 0xd9, 0x6d, 0xd0,
+	0x6e, 0xad, 0xdd, 0xda, 0x6c, 0x6e, 0x15, 0x67, 0xca, 0xf7, 0x4f, 0xcf, 0x2a, 0x25, 0x4d, 0x23,
+	0xf9, 0xb7, 0x96, 0x3f, 0x44, 0x38, 0xa1, 0xde, 0x6c, 0xd5, 0x1b, 0x3f, 0x2d, 0xa6, 0xca, 0xb7,
+	0x4e, 0xcf, 0x2a, 0x45, 0x4d, 0x4b, 0x3c, 0xc1, 0xfd, 0x31, 0x7a, 0xeb, 0x22, 0xbb, 0xfb, 0x64,
+	0xa7, 0xbe, 0xb1, 0xdb, 0x28, 0xa6, 0xcb, 0xe5, 0xd3, 0xb3, 0xca, 0x9d, 0xf3, 0x4a, 0xd2, 0x05,
+	0x7f, 0x80, 0x6e, 0x25, 0x54, 0x69, 0xe3, 0xf3, 0x27, 0x8d, 0xce, 0x6e, 0x31, 0x53, 0xbe, 0x73,
+	0x7a, 0x56, 0xc1, 0x9a, 0x56, 0x54, 0x26, 0xd6, 0xd1, 0xed, 0x73, 0x1a, 0x9d, 0x9d, 0x76, 0xab,
+	0xd3, 0x28, 0x66, 0xcb, 0x77, 0x4f, 0xcf, 0x2a, 0x37, 0x13, 0x2a, 0x32, 0xab, 0xd4, 0xd0, 0x4a,
+	0x42, 0xa7, 0xde, 0xfe, 0xa2, 0xb5, 0xdd, 0xde, 0xa8, 0x77, 0x77, 0x68, 0x7b, 0x8b, 0x36, 0x3a,
+	0x9d, 0x62, 0xae, 0x6c, 0x9c, 0x9e, 0x55, 0xee, 0x69, 0xca, 0x17, 0x22, 0x7c, 0x0d, 0x2d, 0x27,
+	0x8c, 0xec, 0x34, 0x5b, 0x5b, 0xc5, 0x7c, 0xf9, 0xe6, 0xe9, 0x59, 0xe5, 0x86, 0xa6, 0xc7, 0xcf,
+	0xf2, 0xc2, 0xfe, 0xd5, 0xb6, 0xdb, 0x9d, 0x46, 0x71, 0xf6, 0xc2, 0xfe, 0xc1, 0x81, 0xaf, 0xfd,
+	0x5d, 0x0a, 0xe1, 0x8b, 0x7f, 0x35, 0xc3, 0x1f, 0xa0, 0x52, 0x64, 0xa4, 0xd6, 0x7e, 0xbc, 0xc3,
+	0xd7, 0xd9, 0x6c, 0xb7, 0xba, 0xad, 0x76, 0xab, 0x51, 0x9c, 0x49, 0xec, 0xaa, 0xa6, 0xd5, 0x72,
+	0x1d, 0x86, 0xdb, 0xe8, 0xee, 0x65, 0x9a, 0xdb, 0xcf, 0xde, 0x2f, 0xa6, 0xca, 0xeb, 0xa7, 0x67,
+	0x95, 0xdb, 0x17, 0x15, 0xb7, 0x9f, 0xbd, 0xff, 0xbb, 0xbf, 0xfa, 0xee, 0xe5, 0x82, 0x35, 0xde,
+	0x00, 0xe9, 0x4b, 0x7b, 0x0f, 0xdd, 0xd2, 0x0d, 0x3f, 0x6e, 0xec, 0x6e, 0xd4, 0x37, 0x76, 0x37,
+	0x8a, 0x33, 0xe2, 0x0c, 0x34, 0xea, 0x63, 0x16, 0x98, 0x90, 0x76, 0xbf, 0x87, 0x96, 0x13, 0x5f,
+	0xd1, 0x78, 0xda, 0xa0, 0x91, 0x47, 0xe9, 0xeb, 0x67, 0x87, 0xcc, 0xc3, 0xdf, 0x47, 0x58, 0x27,
+	0x6f, 0x6c, 0x7f, 0xb1, 0xf1, 0x65, 0xa7, 0x98, 0x2e, 0xdf, 0x3e, 0x3d, 0xab, 0x2c, 0x6b, 0xec,
+	0x8d, 0xc1, 0x91, 0x79, 0xe2, 0xaf, 0xfd, 0x73, 0x1a, 0x2d, 0xe8, 0xef, 0x46, 0xf8, 0xfb, 0xe8,
+	0xe6, 0x66, 0x73, 0x9b, 0x7b, 0xe2, 0x66, 0x5b, 0x9c, 0x00, 0x1f, 0x16, 0x67, 0xc4, 0x74, 0x3a,
+	0x95, 0xff, 0xc6, 0x7f, 0x84, 0x4a, 0xe7, 0xe8, 0xf5, 0x26, 0x6d, 0xd4, 0x76, 0xdb, 0xf4, 0xcb,
+	0x62, 0xaa, 0xfc, 0x16, 0xdf, 0x30, 0x5d, 0xa7, 0x6e, 0x7b, 0x90, 0x82, 0x4e, 0xf0, 0x27, 0xe8,
+	0xde, 0x39, 0xc5, 0xce, 0x97, 0x8f, 0xb7, 0x9b, 0xad, 0xcf, 0xc4, 0x7c, 0xe9, 0xf2, 0xdb, 0xa7,
+	0x67, 0x95, 0xbb, 0xba, 0x6e, 0x47, 0x3c, 0xc5, 0x71, 0xa8, 0x90, 0xc2, 0x8f, 0x50, 0xe5, 0x0a,
+	0xfd, 0x78, 0x01, 0x99, 0x32, 0x39, 0x3d, 0xab, 0xdc, 0xbf, 0xc4, 0x88, 0x5a, 0x47, 0x21, 0x85,
+	0x7f, 0x88, 0xee, 0x5c, 0x6e, 0x29, 0x8a, 0x8b, 0x4b, 0xf4, 0xd7, 0xfe, 0x35, 0x85, 0xe6, 0x54,
+	0xd5, 0xe3, 0x9b, 0xd6, 0xa0, 0xb4, 0xcd, 0x93, 0x44, 0xbd, 0xd1, 0x6d, 0xb5, 0xbb, 0x30, 0x8a,
+	0x36, 0x4d, 0xf1, 0x5a, 0x2e, 0xfc, 0xe4, 0x3e, 0xae, 0xd1, 0xb7, 0x1a, 0xad, 0x06, 0x6d, 0xd6,
+	0xa2, 0x13, 0x55, 0xec, 0x2d, 0xe6, 0x30, 0xcf, 0xee, 0xe1, 0xf7, 0xd1, 0xdd, 0xa4, 0xf1, 0xce,
+	0x93, 0xda, 0xa3, 0x68, 0x97, 0x60, 0x81, 0xda, 0x04, 0x9d, 0x71, 0xef, 0x00, 0x0e, 0xe6, 0x47,
+	0x09, 0xad, 0x66, 0xeb, 0xe9, 0xc6, 0x76, 0xb3, 0x2e, 0xb4, 0x32, 0xe5, 0xd2, 0xe9, 0x59, 0xe5,
+	0x96, 0xd2, 0x92, 0x0f, 0x1c, 0x5c, 0x6d, 0xed, 0x77, 0x29, 0xb4, 0xf2, 0xf5, 0xc5, 0x0b, 0x7f,
+	0x81, 0xde, 0x81, 0xfd, 0xba, 0x90, 0x0a, 0x64, 0xde, 0x12, 0x7b, 0xb8, 0xb1, 0xb3, 0xd3, 0x68,
+	0xd5, 0x8b, 0x33, 0xe5, 0xd5, 0xd3, 0xb3, 0xca, 0x83, 0xaf, 0x37, 0xb9, 0x31, 0x1a, 0x31, 0xc7,
+	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) {
@@ -1363,6 +1372,16 @@ func (m *Hello) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = 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 {
 		i -= len(m.ClientVersion)
 		copy(dAtA[i:], m.ClientVersion)
@@ -1440,6 +1459,16 @@ func (m *ClusterConfig) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if m.Secondary {
+		i--
+		if m.Secondary {
+			dAtA[i] = 1
+		} else {
+			dAtA[i] = 0
+		}
+		i--
+		dAtA[i] = 0x10
+	}
 	if len(m.Folders) > 0 {
 		for iNdEx := len(m.Folders) - 1; iNdEx >= 0; iNdEx-- {
 			{
@@ -2612,6 +2641,12 @@ func (m *Hello) ProtoSize() (n int) {
 	if l > 0 {
 		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
 }
 
@@ -2642,6 +2677,9 @@ func (m *ClusterConfig) ProtoSize() (n int) {
 			n += 1 + l + sovBep(uint64(l))
 		}
 	}
+	if m.Secondary {
+		n += 2
+	}
 	return n
 }
 
@@ -3258,6 +3296,44 @@ func (m *Hello) Unmarshal(dAtA []byte) error {
 			}
 			m.ClientVersion = string(dAtA[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:
 			iNdEx = preIndex
 			skippy, err := skipBep(dAtA[iNdEx:])
@@ -3430,6 +3506,26 @@ func (m *ClusterConfig) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			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:
 			iNdEx = preIndex
 			skippy, err := skipBep(dAtA[iNdEx:])
diff --git a/lib/protocol/encryption.go b/lib/protocol/encryption.go
index 1149aeb28e6..ce883fdf0aa 100644
--- a/lib/protocol/encryption.go
+++ b/lib/protocol/encryption.go
@@ -43,12 +43,12 @@ const (
 // receives encrypted metadata and requests from the untrusted device, so it
 // must decrypt those and answer requests by encrypting the data.
 type encryptedModel struct {
-	model      contextLessModel
+	model      rawModel
 	folderKeys *folderKeyRegistry
 	keyGen     *KeyGenerator
 }
 
-func newEncryptedModel(model contextLessModel, folderKeys *folderKeyRegistry, keyGen *KeyGenerator) encryptedModel {
+func newEncryptedModel(model rawModel, folderKeys *folderKeyRegistry, keyGen *KeyGenerator) encryptedModel {
 	return encryptedModel{
 		model:      model,
 		folderKeys: folderKeys,
diff --git a/lib/protocol/hello.go b/lib/protocol/hello.go
index 82d66746b0d..8a9c8c864e7 100644
--- a/lib/protocol/hello.go
+++ b/lib/protocol/hello.go
@@ -8,14 +8,6 @@ import (
 	"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 (
 	// ErrTooOldVersion is returned by ExchangeHello when the other side
 	// 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")
 )
 
-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 {
 		return Hello{}, err
 	}
@@ -80,7 +75,7 @@ func readHello(c io.Reader) (Hello, error) {
 	return Hello{}, ErrUnknownMagic
 }
 
-func writeHello(c io.Writer, h HelloIntf) error {
+func writeHello(c io.Writer, h Hello) error {
 	msg, err := h.Marshal()
 	if err != nil {
 		return err
diff --git a/lib/protocol/mocks/connection.go b/lib/protocol/mocks/connection.go
index 3c4f60b2c4c..de1d7c2870e 100644
--- a/lib/protocol/mocks/connection.go
+++ b/lib/protocol/mocks/connection.go
@@ -31,6 +31,16 @@ type Connection struct {
 	clusterConfigArgsForCall []struct {
 		arg1 protocol.ClusterConfig
 	}
+	ConnectionIDStub        func() string
+	connectionIDMutex       sync.RWMutex
+	connectionIDArgsForCall []struct {
+	}
+	connectionIDReturns struct {
+		result1 string
+	}
+	connectionIDReturnsOnCall map[int]struct {
+		result1 string
+	}
 	CryptoStub        func() string
 	cryptoMutex       sync.RWMutex
 	cryptoArgsForCall []struct {
@@ -315,6 +325,59 @@ func (fake *Connection) ClusterConfigArgsForCall(i int) protocol.ClusterConfig {
 	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 {
 	fake.cryptoMutex.Lock()
 	ret, specificReturn := fake.cryptoReturnsOnCall[len(fake.cryptoArgsForCall)]
@@ -1162,6 +1225,8 @@ func (fake *Connection) Invocations() map[string][][]interface{} {
 	defer fake.closedMutex.RUnlock()
 	fake.clusterConfigMutex.RLock()
 	defer fake.clusterConfigMutex.RUnlock()
+	fake.connectionIDMutex.RLock()
+	defer fake.connectionIDMutex.RUnlock()
 	fake.cryptoMutex.RLock()
 	defer fake.cryptoMutex.RUnlock()
 	fake.deviceIDMutex.RLock()
diff --git a/lib/protocol/mocks/connection_info.go b/lib/protocol/mocks/connection_info.go
index 9e9dd472826..5f176fc3806 100644
--- a/lib/protocol/mocks/connection_info.go
+++ b/lib/protocol/mocks/connection_info.go
@@ -10,6 +10,16 @@ import (
 )
 
 type ConnectionInfo struct {
+	ConnectionIDStub        func() string
+	connectionIDMutex       sync.RWMutex
+	connectionIDArgsForCall []struct {
+	}
+	connectionIDReturns struct {
+		result1 string
+	}
+	connectionIDReturnsOnCall map[int]struct {
+		result1 string
+	}
 	CryptoStub        func() string
 	cryptoMutex       sync.RWMutex
 	cryptoArgsForCall []struct {
@@ -94,6 +104,59 @@ type ConnectionInfo struct {
 	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 {
 	fake.cryptoMutex.Lock()
 	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{} {
 	fake.invocationsMutex.RLock()
 	defer fake.invocationsMutex.RUnlock()
+	fake.connectionIDMutex.RLock()
+	defer fake.connectionIDMutex.RUnlock()
 	fake.cryptoMutex.RLock()
 	defer fake.cryptoMutex.RUnlock()
 	fake.establishedAtMutex.RLock()
diff --git a/lib/protocol/nativemodel_darwin.go b/lib/protocol/nativemodel_darwin.go
index b51513df70f..1cf6f5caf83 100644
--- a/lib/protocol/nativemodel_darwin.go
+++ b/lib/protocol/nativemodel_darwin.go
@@ -9,27 +9,27 @@ package protocol
 
 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 {
-	contextLessModel
+	rawModel
 }
 
 func (m nativeModel) Index(folder string, files []FileInfo) error {
 	for i := range files {
 		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 {
 	for i := range files {
 		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) {
 	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)
 }
diff --git a/lib/protocol/nativemodel_unix.go b/lib/protocol/nativemodel_unix.go
index c714309c6b4..08bcc193cab 100644
--- a/lib/protocol/nativemodel_unix.go
+++ b/lib/protocol/nativemodel_unix.go
@@ -7,4 +7,4 @@ package protocol
 
 // 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 }
diff --git a/lib/protocol/nativemodel_windows.go b/lib/protocol/nativemodel_windows.go
index efc47d17237..e0aada52a67 100644
--- a/lib/protocol/nativemodel_windows.go
+++ b/lib/protocol/nativemodel_windows.go
@@ -13,20 +13,20 @@ import (
 	"strings"
 )
 
-func makeNative(m contextLessModel) contextLessModel { return nativeModel{m} }
+func makeNative(m rawModel) rawModel { return nativeModel{m} }
 
 type nativeModel struct {
-	contextLessModel
+	rawModel
 }
 
 func (m nativeModel) Index(folder string, files []FileInfo) error {
 	files = fixupFiles(files)
-	return m.contextLessModel.Index(folder, files)
+	return m.rawModel.Index(folder, files)
 }
 
 func (m nativeModel) IndexUpdate(folder string, files []FileInfo) error {
 	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) {
@@ -36,7 +36,7 @@ func (m nativeModel) Request(folder, name string, blockNo, size int32, offset in
 	}
 
 	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 {
diff --git a/lib/protocol/protocol.go b/lib/protocol/protocol.go
index 9189151c2d9..8e5f653d9f8 100644
--- a/lib/protocol/protocol.go
+++ b/lib/protocol/protocol.go
@@ -136,9 +136,9 @@ type Model interface {
 	DownloadProgress(conn Connection, folder string, updates []FileDownloadProgressUpdate) error
 }
 
-// contextLessModel is the Model interface, but without the initial
-// Connection parameter. Internal use only.
-type contextLessModel interface {
+// rawModel is the Model interface, but without the initial Connection
+// parameter. Internal use only.
+type rawModel interface {
 	Index(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)
@@ -177,6 +177,7 @@ type ConnectionInfo interface {
 	String() string
 	Crypto() string
 	EstablishedAt() time.Time
+	ConnectionID() string
 }
 
 type rawConnection struct {
@@ -184,8 +185,9 @@ type rawConnection struct {
 
 	deviceID  DeviceID
 	idString  string
-	model     contextLessModel
+	model     rawModel
 	startTime time.Time
+	started   chan struct{}
 
 	cr     *countingReader
 	cw     *countingWriter
@@ -263,7 +265,7 @@ func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, closer
 	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()
 	cr := &countingReader{Reader: reader, 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,
 		idString:              deviceID.String(),
 		model:                 receiver,
+		started:               make(chan struct{}),
 		cr:                    cr,
 		cw:                    cw,
 		closer:                closer,
@@ -315,6 +318,7 @@ func (c *rawConnection) Start() {
 		c.loopWG.Done()
 	}()
 	c.startTime = time.Now().Truncate(time.Second)
+	close(c.started)
 }
 
 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.
 func (c *rawConnection) internalClose(err error) {
 	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 {
-			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)
 
@@ -975,7 +979,11 @@ func (c *rawConnection) internalClose(err error) {
 		}
 		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)
 	})
@@ -1108,7 +1116,7 @@ func messageContext(msg message) (string, error) {
 
 // connectionWrappingModel takes the Model interface from the model package,
 // 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 {
 	conn  Connection
 	model Model
diff --git a/lib/sliceutil/sliceutil.go b/lib/sliceutil/sliceutil.go
new file mode 100644
index 00000000000..f16c6762218
--- /dev/null
+++ b/lib/sliceutil/sliceutil.go
@@ -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]
+}
diff --git a/lib/syncthing/syncthing.go b/lib/syncthing/syncthing.go
index 4d1354e8d93..be65f83cd46 100644
--- a/lib/syncthing/syncthing.go
+++ b/lib/syncthing/syncthing.go
@@ -249,7 +249,7 @@ func (a *App) startup() error {
 	}
 
 	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 {
 		m.StartDeadlockDetector(time.Duration(a.opts.DeadlockTimeoutS) * time.Second)
diff --git a/proto/lib/config/deviceconfiguration.proto b/proto/lib/config/deviceconfiguration.proto
index 616329b7678..46afeed80c9 100644
--- a/proto/lib/config/deviceconfiguration.proto
+++ b/proto/lib/config/deviceconfiguration.proto
@@ -10,7 +10,7 @@ import "ext.proto";
 message DeviceConfiguration {
     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"];
-    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"];
     string                  cert_name                  = 5 [(ext.xml) = "certName,attr,omitempty"];
     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"];
     bool                    untrusted                  = 17;
     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
 }
diff --git a/proto/lib/protocol/bep.proto b/proto/lib/protocol/bep.proto
index 7e76464a265..ad7f5d0b893 100644
--- a/proto/lib/protocol/bep.proto
+++ b/proto/lib/protocol/bep.proto
@@ -8,9 +8,11 @@ import "repos/protobuf/gogoproto/gogo.proto";
 // --- Pre-auth ---
 
 message Hello {
-    string device_name    = 1;
-    string client_name    = 2;
-    string client_version = 3;
+    string device_name     = 1;
+    string client_name     = 2;
+    string client_version  = 3;
+    int32  num_connections = 4;
+    int64  timestamp       = 5;
 }
 
 // --- Header ---
@@ -41,7 +43,8 @@ enum MessageCompression {
 // Cluster Config
 
 message ClusterConfig {
-    repeated Folder folders = 1;
+    repeated Folder folders   = 1;
+    bool            secondary = 2;
 }
 
 message Folder {


Test patch:
diff --git a/cmd/stdiscosrv/database_test.go b/cmd/stdiscosrv/database_test.go
index 2596b6b1704..14b15059d45 100644
--- a/cmd/stdiscosrv/database_test.go
+++ b/cmd/stdiscosrv/database_test.go
@@ -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}},
-			b: []DatabaseAddress{{Address: "d", Expires: 15}, {Address: "b", Expires: 15}}, // gets reordered
+			b: []DatabaseAddress{{Address: "b", Expires: 15}, {Address: "d", Expires: 15}},
 		},
 	}
 
diff --git a/lib/model/fakeconns_test.go b/lib/model/fakeconns_test.go
index 6aa6d196a2f..9255d6eab2c 100644
--- a/lib/model/fakeconns_test.go
+++ b/lib/model/fakeconns_test.go
@@ -34,6 +34,7 @@ func newFakeConnection(id protocol.DeviceID, model Model) *fakeConnection {
 		return f.fileData[name], nil
 	})
 	f.DeviceIDReturns(id)
+	f.ConnectionIDReturns(rand.String(16))
 	f.CloseCalls(func(err error) {
 		f.closeOnce.Do(func() {
 			close(f.closed)
diff --git a/lib/model/folder_recvonly_test.go b/lib/model/folder_recvonly_test.go
index a30b6b2403d..b32f6dd339d 100644
--- a/lib/model/folder_recvonly_test.go
+++ b/lib/model/folder_recvonly_test.go
@@ -530,7 +530,7 @@ func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder, context.Cancel
 	cfg.Folders = []config.FolderConfiguration{fcfg}
 	replace(t, w, cfg)
 
-	m := newModel(t, w, myID, "syncthing", "dev", nil)
+	m := newModel(t, w, myID, nil)
 	m.ServeBackground()
 	<-m.started
 	must(t, m.ScanFolder("ro"))
diff --git a/lib/model/model_test.go b/lib/model/model_test.go
index 508173285c4..3fbe60d57d4 100644
--- a/lib/model/model_test.go
+++ b/lib/model/model_test.go
@@ -270,7 +270,7 @@ func TestDeviceRename(t *testing.T) {
 	cfg, cfgCancel := newConfigWrapper(rawCfg)
 	defer cfgCancel()
 
-	m := newModel(t, cfg, myID, "syncthing", "dev", nil)
+	m := newModel(t, cfg, myID, nil)
 
 	if cfg.Devices()[device1].Name != "" {
 		t.Errorf("Device already has a name")
@@ -422,7 +422,7 @@ func TestClusterConfig(t *testing.T) {
 
 	wrapper, cancel := newConfigWrapper(cfg)
 	defer cancel()
-	m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
+	m := newModel(t, wrapper, myID, nil)
 	m.ServeBackground()
 	defer cleanupModel(m)
 
@@ -903,7 +903,7 @@ func TestIssue5063(t *testing.T) {
 	defer cancel()
 
 	m.pmut.Lock()
-	for _, c := range m.conn {
+	for _, c := range m.connections {
 		conn := c.(*fakeConnection)
 		conn.CloseCalls(func(_ error) {})
 		defer m.Closed(c, errStopped) // to unblock deferred m.Stop()
@@ -1626,7 +1626,7 @@ func TestROScanRecovery(t *testing.T) {
 		},
 	})
 	defer cancel()
-	m := newModel(t, cfg, myID, "syncthing", "dev", nil)
+	m := newModel(t, cfg, myID, nil)
 
 	set := newFileSet(t, "default", m.db)
 	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@@ -1673,7 +1673,7 @@ func TestRWScanRecovery(t *testing.T) {
 		},
 	})
 	defer cancel()
-	m := newModel(t, cfg, myID, "syncthing", "dev", nil)
+	m := newModel(t, cfg, myID, nil)
 
 	set := newFileSet(t, "default", m.db)
 	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@@ -2073,7 +2073,7 @@ func TestIssue4357(t *testing.T) {
 	// Create a separate wrapper not to pollute other tests.
 	wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion})
 	defer cancel()
-	m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
+	m := newModel(t, wrapper, myID, nil)
 	m.ServeBackground()
 	defer cleanupModel(m)
 
@@ -2125,7 +2125,7 @@ func TestIssue4357(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.Drop(device1)
@@ -2231,7 +2231,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
 		t.Error("device still in config")
 	}
 
-	if _, ok := m.conn[device2]; ok {
+	if _, ok := m.deviceConnIDs[device2]; ok {
 		t.Error("conn not missing")
 	}
 
@@ -2374,7 +2374,7 @@ func TestCustomMarkerName(t *testing.T) {
 
 	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.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@@ -2736,7 +2736,7 @@ func TestIssue4094(t *testing.T) {
 	// Create a separate wrapper not to pollute other tests.
 	wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion})
 	defer cancel()
-	m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
+	m := newModel(t, wrapper, myID, nil)
 	m.ServeBackground()
 	defer cleanupModel(m)
 
@@ -2971,6 +2971,7 @@ func TestConnCloseOnRestart(t *testing.T) {
 	br := &testutil.BlockingRW{}
 	nw := &testutil.NoopRW{}
 	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.pmut.RLock()
 	if len(m.closed) != 1 {
@@ -3639,6 +3640,7 @@ func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSec
 	})
 	m.AddConnection(fc1, protocol.Hello{})
 	m.AddConnection(fc2, protocol.Hello{})
+	m.promoteConnections()
 
 	// Initial CCs
 	select {
@@ -3690,7 +3692,7 @@ func TestIssue6961(t *testing.T) {
 	must(t, err)
 	waiter.Wait()
 	// 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, err = db.NewLowlevel(backend.OpenMemory(), m.evLogger, db.WithRecheckInterval(time.Millisecond))
 	if err != nil {
@@ -3952,7 +3954,7 @@ func TestCCFolderNotRunning(t *testing.T) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
 	tfs := fcfg.Filesystem(nil)
-	m := newModel(t, w, myID, "syncthing", "dev", nil)
+	m := newModel(t, w, myID, nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	// A connection can happen before all the folders are started.
diff --git a/lib/model/requests_test.go b/lib/model/requests_test.go
index 497342481b5..aa285c4cebf 100644
--- a/lib/model/requests_test.go
+++ b/lib/model/requests_test.go
@@ -1214,7 +1214,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
 
 	// Initialise db with an entry and then stop everything again
 	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())
 	m.ServeBackground()
 	m.ScanFolders()
@@ -1223,7 +1223,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
 
 	// Add connection (sends incoming cluster config) before starting the new model
 	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,
 		stopped:  make(chan struct{}),
 	}
diff --git a/lib/model/testutils_test.go b/lib/model/testutils_test.go
index a558d9a9d62..ccd85286850 100644
--- a/lib/model/testutils_test.go
+++ b/lib/model/testutils_test.go
@@ -39,7 +39,9 @@ func init() {
 	device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
 	device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
 	device1Conn.DeviceIDReturns(device1)
+	device1Conn.ConnectionIDReturns(rand.String(16))
 	device2Conn.DeviceIDReturns(device2)
+	device2Conn.ConnectionIDReturns(rand.String(16))
 
 	cfg := config.New(myID)
 	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 {
 	t.Helper()
-	m := newModel(t, w, myID, "syncthing", "dev", nil)
+	m := newModel(t, w, myID, nil)
 	m.ServeBackground()
 	<-m.started
 
@@ -144,14 +146,14 @@ type testModel 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()
 	evLogger := events.NewLogger()
 	ldb, err := db.NewLowlevel(backend.OpenMemory(), evLogger)
 	if err != nil {
 		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())
 	go evLogger.Serve(ctx)
 	return &testModel{
diff --git a/lib/protocol/hello_test.go b/lib/protocol/hello_test.go
index 6576a639ecb..ce3bfc98477 100644
--- a/lib/protocol/hello_test.go
+++ b/lib/protocol/hello_test.go
@@ -35,10 +35,11 @@ func TestVersion14Hello(t *testing.T) {
 
 	conn := &readWriter{outBuf, inBuf}
 
-	send := &Hello{
+	send := Hello{
 		DeviceName:    "this device",
 		ClientName:    "other client",
 		ClientVersion: "v0.14.6",
+		Timestamp:     1234567890,
 	}
 
 	res, err := ExchangeHello(conn, send)
@@ -80,10 +81,11 @@ func TestOldHelloMsgs(t *testing.T) {
 
 		conn := &readWriter{outBuf, inBuf}
 
-		send := &Hello{
+		send := Hello{
 			DeviceName:    "this device",
 			ClientName:    "other client",
 			ClientVersion: "v1.0.0",
+			Timestamp:     1234567890,
 		}
 
 		_, err := ExchangeHello(conn, send)
diff --git a/lib/protocol/mocked_connection_info_test.go b/lib/protocol/mocked_connection_info_test.go
index aab43306ff1..85f46871c87 100644
--- a/lib/protocol/mocked_connection_info_test.go
+++ b/lib/protocol/mocked_connection_info_test.go
@@ -8,6 +8,16 @@ import (
 )
 
 type mockedConnectionInfo struct {
+	ConnectionIDStub        func() string
+	connectionIDMutex       sync.RWMutex
+	connectionIDArgsForCall []struct {
+	}
+	connectionIDReturns struct {
+		result1 string
+	}
+	connectionIDReturnsOnCall map[int]struct {
+		result1 string
+	}
 	CryptoStub        func() string
 	cryptoMutex       sync.RWMutex
 	cryptoArgsForCall []struct {
@@ -92,6 +102,59 @@ type mockedConnectionInfo struct {
 	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 {
 	fake.cryptoMutex.Lock()
 	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{} {
 	fake.invocationsMutex.RLock()
 	defer fake.invocationsMutex.RUnlock()
+	fake.connectionIDMutex.RLock()
+	defer fake.connectionIDMutex.RUnlock()
 	fake.cryptoMutex.RLock()
 	defer fake.cryptoMutex.RUnlock()
 	fake.establishedAtMutex.RLock()
diff --git a/lib/sliceutil/sliceutil_test.go b/lib/sliceutil/sliceutil_test.go
new file mode 100644
index 00000000000..41387cd2bae
--- /dev/null
+++ b/lib/sliceutil/sliceutil_test.go
@@ -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")
+		}
+	}
+}
diff --git a/test/h1/config.xml b/test/h1/config.xml
index a658d1bdb3a..80c02b902f3 100644
--- a/test/h1/config.xml
+++ b/test/h1/config.xml
@@ -1,13 +1,17 @@
-<configuration version="32">
-    <folder id="default" label="" path="s1/" 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>
-        <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" introducedBy=""></device>
-        <device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT" introducedBy=""></device>
+<configuration version="37">
+    <folder id="default" label="" path="s1?files=10000" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
+        <filesystemType>fake</filesystemType>
+        <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy="">
+            <encryptionPassword></encryptionPassword>
+        </device>
+        <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>1</copiers>
         <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
@@ -24,51 +28,21 @@
         <markerName>.stfolder</markerName>
         <copyOwnershipFromParent>false</copyOwnershipFromParent>
         <modTimeWindowS>0</modTimeWindowS>
-        <maxConcurrentWrites>0</maxConcurrentWrites>
+        <maxConcurrentWrites>2</maxConcurrentWrites>
         <disableFsync>false</disableFsync>
         <blockPullOrder>standard</blockPullOrder>
         <copyRangeMethod>standard</copyRangeMethod>
         <caseSensitiveFS>false</caseSensitiveFS>
         <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 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="">
         <address>tcp://127.0.0.1:22001</address>
         <paused>false</paused>
@@ -76,30 +50,21 @@
         <maxSendKbps>0</maxSendKbps>
         <maxRecvKbps>0</maxRecvKbps>
         <maxRequestKiB>0</maxRequestKiB>
+        <untrusted>false</untrusted>
+        <remoteGUIPort>0</remoteGUIPort>
+        <numConnections>3</numConnections>
     </device>
     <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>quic://127.0.0.1:22002</address>
         <paused>false</paused>
         <autoAcceptFolders>false</autoAcceptFolders>
         <maxSendKbps>0</maxSendKbps>
         <maxRecvKbps>0</maxRecvKbps>
         <maxRequestKiB>0</maxRequestKiB>
-    </device>
-    <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
-        <address>tcp://127.0.0.1:22003</address>
-        <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>
+        <untrusted>false</untrusted>
+        <remoteGUIPort>0</remoteGUIPort>
+        <numConnections>3</numConnections>
     </device>
     <gui enabled="true" tls="false" debugging="true">
         <address>127.0.0.1:8081</address>
@@ -111,6 +76,7 @@
     <ldap></ldap>
     <options>
         <listenAddress>tcp://127.0.0.1:22001</listenAddress>
+        <listenAddress>quic://127.0.0.1:22001</listenAddress>
         <globalAnnounceServer>default</globalAnnounceServer>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <localAnnounceEnabled>true</localAnnounceEnabled>
@@ -132,7 +98,6 @@
         <urURL>https://data.syncthing.net/newdata</urURL>
         <urPostInsecurely>false</urPostInsecurely>
         <urInitialDelayS>1800</urInitialDelayS>
-        <restartOnWakeup>true</restartOnWakeup>
         <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
         <upgradeToPreReleases>false</upgradeToPreReleases>
         <keepTemporariesH>24</keepTemporariesH>
@@ -144,7 +109,6 @@
         <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
         <tempIndexMinBlocks>10</tempIndexMinBlocks>
         <trafficClass>0</trafficClass>
-        <defaultFolderPath>~</defaultFolderPath>
         <setLowPriority>true</setLowPriority>
         <maxFolderConcurrency>0</maxFolderConcurrency>
         <crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL>
@@ -155,5 +119,70 @@
         <databaseTuning>auto</databaseTuning>
         <maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB>
         <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>
+    <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>
diff --git a/test/h2/config.xml b/test/h2/config.xml
index 6b555207049..f98bd751a06 100644
--- a/test/h2/config.xml
+++ b/test/h2/config.xml
@@ -1,14 +1,19 @@
-<configuration version="32">
-    <folder id="default" label="" path="s2" 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>
-        <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" introducedBy=""></device>
+<configuration version="37">
+    <folder id="default" label="" path="s2" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
+        <filesystemType>fake</filesystemType>
+        <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy="">
+            <encryptionPassword></encryptionPassword>
+        </device>
+        <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>1</copiers>
+        <copiers>8</copiers>
         <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
         <hashers>0</hashers>
         <order>random</order>
@@ -23,80 +28,32 @@
         <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="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>
+        <maxConcurrentWrites>8</maxConcurrentWrites>
         <disableFsync>false</disableFsync>
         <blockPullOrder>standard</blockPullOrder>
         <copyRangeMethod>standard</copyRangeMethod>
         <caseSensitiveFS>false</caseSensitiveFS>
         <junctionsAsDirs>true</junctionsAsDirs>
+        <syncOwnership>false</syncOwnership>
+        <sendOwnership>false</sendOwnership>
+        <syncXattrs>false</syncXattrs>
+        <sendXattrs>false</sendXattrs>
+        <xattrFilter>
+            <maxSingleEntrySize>0</maxSingleEntrySize>
+            <maxTotalSize>0</maxTotalSize>
+        </xattrFilter>
     </folder>
     <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>quic://127.0.0.1:22001</address>
         <paused>false</paused>
         <autoAcceptFolders>false</autoAcceptFolders>
-        <maxSendKbps>0</maxSendKbps>
-        <maxRecvKbps>0</maxRecvKbps>
+        <maxSendKbps>800</maxSendKbps>
+        <maxRecvKbps>800</maxRecvKbps>
         <maxRequestKiB>0</maxRequestKiB>
+        <untrusted>false</untrusted>
+        <remoteGUIPort>0</remoteGUIPort>
+        <numConnections>3</numConnections>
     </device>
     <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>
@@ -105,14 +62,9 @@
         <maxSendKbps>0</maxSendKbps>
         <maxRecvKbps>0</maxRecvKbps>
         <maxRequestKiB>0</maxRequestKiB>
-    </device>
-    <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
-        <address>tcp://127.0.0.1:22003</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>
     <gui enabled="true" tls="false" debugging="true">
         <address>127.0.0.1:8082</address>
@@ -121,15 +73,15 @@
     </gui>
     <ldap></ldap>
     <options>
-        <listenAddress>dynamic+https://relays.syncthing.net/endpoint</listenAddress>
         <listenAddress>tcp://127.0.0.1:22002</listenAddress>
+        <listenAddress>quic://127.0.0.1:22002</listenAddress>
         <globalAnnounceServer>default</globalAnnounceServer>
         <globalAnnounceEnabled>false</globalAnnounceEnabled>
         <localAnnounceEnabled>true</localAnnounceEnabled>
         <localAnnouncePort>21027</localAnnouncePort>
         <localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
-        <maxSendKbps>0</maxSendKbps>
-        <maxRecvKbps>0</maxRecvKbps>
+        <maxSendKbps>1000</maxSendKbps>
+        <maxRecvKbps>1000</maxRecvKbps>
         <reconnectionIntervalS>5</reconnectionIntervalS>
         <relaysEnabled>true</relaysEnabled>
         <relayReconnectIntervalM>10</relayReconnectIntervalM>
@@ -144,19 +96,17 @@
         <urURL>https://data.syncthing.net/newdata</urURL>
         <urPostInsecurely>false</urPostInsecurely>
         <urInitialDelayS>1800</urInitialDelayS>
-        <restartOnWakeup>true</restartOnWakeup>
         <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
         <upgradeToPreReleases>false</upgradeToPreReleases>
         <keepTemporariesH>24</keepTemporariesH>
         <cacheIgnoredFiles>false</cacheIgnoredFiles>
         <progressUpdateIntervalS>5</progressUpdateIntervalS>
-        <limitBandwidthInLan>false</limitBandwidthInLan>
+        <limitBandwidthInLan>true</limitBandwidthInLan>
         <minHomeDiskFree unit="%">1</minHomeDiskFree>
         <releasesURL>https://upgrades.syncthing.net/meta.json</releasesURL>
         <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
         <tempIndexMinBlocks>10</tempIndexMinBlocks>
         <trafficClass>0</trafficClass>
-        <defaultFolderPath>~</defaultFolderPath>
         <setLowPriority>true</setLowPriority>
         <maxFolderConcurrency>0</maxFolderConcurrency>
         <crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL>
@@ -167,5 +117,70 @@
         <databaseTuning>auto</databaseTuning>
         <maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB>
         <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>
+    <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>