all: Add untrusted folders behind feature flag (ref #62) (#7055)

This commit is contained in:
Simon Frei
2020-11-09 15:33:32 +01:00
committed by GitHub
parent 4db5ea5893
commit 31559e908b
59 changed files with 6952 additions and 1111 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ func (f *fakeConnection) IndexUpdate(ctx context.Context, folder string, fs []pr
return nil
}
func (f *fakeConnection) Request(ctx context.Context, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
func (f *fakeConnection) Request(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
f.mut.Lock()
defer f.mut.Unlock()
if f.requestFn != nil {
+45 -18
View File
@@ -459,7 +459,8 @@ func (f *folder) scanSubdirs(subDirs []string) error {
scanCtx, scanCancel := context.WithCancel(f.ctx)
defer scanCancel()
mtimefs := f.fset.MtimeFS()
fchan := scanner.Walk(scanCtx, scanner.Config{
scanConfig := scanner.Config{
Folder: f.ID,
Subs: subDirs,
Matcher: f.ignores,
@@ -474,7 +475,13 @@ func (f *folder) scanSubdirs(subDirs []string) error {
LocalFlags: f.localFlags,
ModTimeWindow: f.modTimeWindow,
EventLogger: f.evLogger,
})
}
var fchan chan scanner.ScanResult
if f.Type == config.FolderTypeReceiveEncrypted {
fchan = scanner.WalkWithoutHashing(scanCtx, scanConfig)
} else {
fchan = scanner.Walk(scanCtx, scanConfig)
}
batch := newFileInfoBatch(func(fs []protocol.FileInfo) error {
if err := f.getHealthErrorWithoutIgnores(); err != nil {
@@ -485,13 +492,19 @@ func (f *folder) scanSubdirs(subDirs []string) error {
return nil
})
// Schedule a pull after scanning, but only if we actually detected any
// changes.
changes := 0
defer func() {
if changes > 0 {
f.SchedulePull()
}
}()
var batchAppend func(protocol.FileInfo, *db.Snapshot)
// Resolve items which are identical with the global state.
if f.localFlags&protocol.FlagLocalReceiveOnly == 0 {
batchAppend = func(fi protocol.FileInfo, _ *db.Snapshot) {
batch.append(fi)
}
} else {
switch f.Type {
case config.FolderTypeReceiveOnly:
batchAppend = func(fi protocol.FileInfo, snap *db.Snapshot) {
switch gf, ok := snap.GetGlobal(fi.Name); {
case !ok:
@@ -509,16 +522,28 @@ func (f *folder) scanSubdirs(subDirs []string) error {
}
batch.append(fi)
}
}
// Schedule a pull after scanning, but only if we actually detected any
// changes.
changes := 0
defer func() {
if changes > 0 {
f.SchedulePull()
case config.FolderTypeReceiveEncrypted:
batchAppend = func(fi protocol.FileInfo, _ *db.Snapshot) {
// This is a "virtual" parent directory of encrypted files.
// We don't track it, but check if anything still exists
// within and delete it otherwise.
if fi.IsDirectory() && protocol.IsEncryptedParent(fi.Name) {
if names, err := mtimefs.DirNames(fi.Name); err == nil && len(names) == 0 {
mtimefs.Remove(fi.Name)
}
changes--
return
}
// Any local change must not be sent as index entry to
// remotes and show up as an error in the UI.
fi.LocalFlags = protocol.FlagLocalReceiveOnly
batch.append(fi)
}
}()
default:
batchAppend = func(fi protocol.FileInfo, _ *db.Snapshot) {
batch.append(fi)
}
}
f.clearScanErrors(subDirs)
alreadyUsed := make(map[string]struct{})
@@ -540,7 +565,9 @@ func (f *folder) scanSubdirs(subDirs []string) error {
batchAppend(res.File, snap)
changes++
if f.localFlags&protocol.FlagLocalReceiveOnly == 0 {
switch f.Type {
case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
default:
if nf, ok := f.findRename(snap, mtimefs, res.File, alreadyUsed); ok {
batchAppend(nf, snap)
changes++
@@ -648,7 +675,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
l.Debugln("marking file as deleted", nf)
batchAppend(nf, snap)
changes++
case file.IsDeleted() && file.IsReceiveOnlyChanged() && f.localFlags&protocol.FlagLocalReceiveOnly != 0 && len(snap.Availability(file.Name)) == 0:
case file.IsDeleted() && file.IsReceiveOnlyChanged() && f.Type == config.FolderTypeReceiveOnly && len(snap.Availability(file.Name)) == 0:
file.Version = protocol.Vector{}
file.LocalFlags &^= protocol.FlagLocalReceiveOnly
l.Debugln("marking deleted item that doesn't exist anywhere as not receive-only", file)
+111
View File
@@ -0,0 +1,111 @@
// Copyright (C) 2018 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 model
import (
"fmt"
"sort"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/versioner"
)
func init() {
folderFactories[config.FolderTypeReceiveEncrypted] = newReceiveEncryptedFolder
}
type receiveEncryptedFolder struct {
*sendReceiveFolder
}
func newReceiveEncryptedFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem, evLogger events.Logger, ioLimiter *byteSemaphore) service {
return &receiveEncryptedFolder{newSendReceiveFolder(model, fset, ignores, cfg, ver, fs, evLogger, ioLimiter).(*sendReceiveFolder)}
}
func (f *receiveEncryptedFolder) Revert() {
f.doInSync(func() error { f.revert(); return nil })
}
func (f *receiveEncryptedFolder) revert() {
l.Infof("Reverting unexpected items in folder %v (receive-encrypted)", f.Description())
f.setState(FolderScanning)
defer f.setState(FolderIdle)
batch := newFileInfoBatch(func(fs []protocol.FileInfo) error {
f.updateLocalsFromScanning(fs)
return nil
})
snap := f.fset.Snapshot()
defer snap.Release()
var iterErr error
var dirs []string
snap.WithHaveTruncated(protocol.LocalDeviceID, func(intf protocol.FileIntf) bool {
if iterErr = batch.flushIfFull(); iterErr != nil {
return false
}
fit := intf.(db.FileInfoTruncated)
if !fit.IsReceiveOnlyChanged() || intf.IsDeleted() {
return true
}
if fit.IsDirectory() {
dirs = append(dirs, fit.Name)
return true
}
if err := f.inWritableDir(f.fs.Remove, fit.Name); err != nil && !fs.IsNotExist(err) {
f.newScanError(fit.Name, fmt.Errorf("deleting unexpected item: %w", err))
}
fi := fit.ConvertToDeletedFileInfo(f.shortID)
// Set version to zero, such that we pull the global version in case
// this is a valid filename that was erroneously changed locally.
// Should already be zero from scanning, but lets be safe.
fi.Version = protocol.Vector{}
// Purposely not removing FlagLocalReceiveOnly as the deleted
// item should still not be sent in index updates. However being
// deleted, it will not show up as an unexpected file in the UI
// anymore.
batch.append(fi)
return true
})
f.revertHandleDirs(dirs, snap)
if iterErr == nil {
iterErr = batch.flush()
}
if iterErr != nil {
l.Infoln("Failed to delete unexpected items:", iterErr)
}
}
func (f *receiveEncryptedFolder) revertHandleDirs(dirs []string, snap *db.Snapshot) {
if len(dirs) == 0 {
return
}
scanChan := make(chan string)
go f.pullScannerRoutine(scanChan)
defer close(scanChan)
sort.Sort(sort.Reverse(sort.StringSlice(dirs)))
for _, dir := range dirs {
if err := f.deleteDirOnDisk(dir, snap, scanChan); err != nil {
f.newScanError(dir, fmt.Errorf("deleting unexpected dir: %w", err))
}
}
}
+94 -56
View File
@@ -686,16 +686,22 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
// user can then clean up as they like...
// This can also occur if an entire tree structure was deleted, but only
// a leave has been scanned.
//
// And if this is an encrypted folder:
// Encrypted files have made-up filenames with two synthetic parent
// directories which don't have any meaning. Create those if necessary.
if _, err := f.fs.Lstat(parent); !fs.IsNotExist(err) {
l.Debugf("%v parent not missing %v", f, file)
return true
}
l.Debugf("%v resurrecting parent directory of %v", f, file)
l.Debugf("%v creating parent directory of %v", f, file)
if err := f.fs.MkdirAll(parent, 0755); err != nil {
f.newPullError(file, errors.Wrap(err, "resurrecting parent dir"))
f.newPullError(file, errors.Wrap(err, "creating parent dir"))
return false
}
scanChan <- parent
if f.Type != config.FolderTypeReceiveEncrypted {
scanChan <- parent
}
return true
}
@@ -1248,38 +1254,11 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
continue
}
f.model.progressEmitter.Register(state.sharedPullerState)
var file fs.File
var weakHashFinder *weakhash.Finder
blocksPercentChanged := 0
if tot := len(state.file.Blocks); tot > 0 {
blocksPercentChanged = (tot - state.have) * 100 / tot
if f.Type != config.FolderTypeReceiveEncrypted {
f.model.progressEmitter.Register(state.sharedPullerState)
}
if blocksPercentChanged >= f.WeakHashThresholdPct {
hashesToFind := make([]uint32, 0, len(state.blocks))
for _, block := range state.blocks {
if block.WeakHash != 0 {
hashesToFind = append(hashesToFind, block.WeakHash)
}
}
if len(hashesToFind) > 0 {
file, err = f.fs.Open(state.file.Name)
if err == nil {
weakHashFinder, err = weakhash.NewFinder(f.ctx, file, state.file.BlockSize(), hashesToFind)
if err != nil {
l.Debugln("weak hasher", err)
}
}
} else {
l.Debugf("not weak hashing %s. file did not contain any weak hashes", state.file.Name)
}
} else {
l.Debugf("not weak hashing %s. not enough changed %.02f < %d", state.file.Name, blocksPercentChanged, f.WeakHashThresholdPct)
}
weakHashFinder, file := f.initWeakHashFinder(state)
blocks:
for _, block := range state.blocks {
@@ -1305,25 +1284,28 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
buf = protocol.BufferPool.Upgrade(buf, int(block.Size))
found, err := weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
if verifyBuffer(buf, block) != nil {
return true
}
var found bool
if f.Type != config.FolderTypeReceiveEncrypted {
found, err = weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
if f.verifyBuffer(buf, block) != nil {
return true
}
err = f.limitedWriteAt(dstFd, buf, block.Offset)
err = f.limitedWriteAt(dstFd, buf, block.Offset)
if err != nil {
state.fail(errors.Wrap(err, "dst write"))
}
if offset == block.Offset {
state.copiedFromOrigin()
} else {
state.copiedFromOriginShifted()
}
return false
})
if err != nil {
state.fail(errors.Wrap(err, "dst write"))
l.Debugln("weak hasher iter", err)
}
if offset == block.Offset {
state.copiedFromOrigin()
} else {
state.copiedFromOriginShifted()
}
return false
})
if err != nil {
l.Debugln("weak hasher iter", err)
}
if !found {
@@ -1341,9 +1323,14 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
return false
}
if err := verifyBuffer(buf, block); err != nil {
l.Debugln("Finder failed to verify buffer", err)
return false
// Hash is not SHA256 as it's an encrypted hash token. In that
// case we can't verify the block integrity so we'll take it on
// trust. (The other side can and will verify.)
if f.Type != config.FolderTypeReceiveEncrypted {
if err := f.verifyBuffer(buf, block); err != nil {
l.Debugln("Finder failed to verify buffer", err)
return false
}
}
if f.CopyRangeMethod != fs.CopyRangeMethodStandard {
@@ -1390,7 +1377,49 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
}
}
func verifyBuffer(buf []byte, block protocol.BlockInfo) error {
func (f *sendReceiveFolder) initWeakHashFinder(state copyBlocksState) (*weakhash.Finder, fs.File) {
if f.Type == config.FolderTypeReceiveEncrypted {
l.Debugln("not weak hashing due to folder type", f.Type)
return nil, nil
}
blocksPercentChanged := 0
if tot := len(state.file.Blocks); tot > 0 {
blocksPercentChanged = (tot - state.have) * 100 / tot
}
if blocksPercentChanged < f.WeakHashThresholdPct {
l.Debugf("not weak hashing %s. not enough changed %.02f < %d", state.file.Name, blocksPercentChanged, f.WeakHashThresholdPct)
return nil, nil
}
hashesToFind := make([]uint32, 0, len(state.blocks))
for _, block := range state.blocks {
if block.WeakHash != 0 {
hashesToFind = append(hashesToFind, block.WeakHash)
}
}
if len(hashesToFind) == 0 {
l.Debugf("not weak hashing %s. file did not contain any weak hashes", state.file.Name)
return nil, nil
}
file, err := f.fs.Open(state.file.Name)
if err != nil {
l.Debugln("weak hasher", err)
return nil, nil
}
weakHashFinder, err := weakhash.NewFinder(f.ctx, file, state.file.BlockSize(), hashesToFind)
if err != nil {
l.Debugln("weak hasher", err)
return nil, file
}
return weakHashFinder, file
}
func (f *sendReceiveFolder) verifyBuffer(buf []byte, block protocol.BlockInfo) error {
if len(buf) != int(block.Size) {
return fmt.Errorf("length mismatch %d != %d", len(buf), block.Size)
}
@@ -1487,7 +1516,8 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
// leastBusy can select another device when someone else asks.
activity.using(selected)
var buf []byte
buf, lastError = f.model.requestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary)
blockNo := int(state.block.Offset / int64(state.file.BlockSize()))
buf, lastError = f.model.requestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, blockNo, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary)
activity.done(selected)
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "returned error:", lastError)
@@ -1496,7 +1526,13 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
// Verify that the received block matches the desired hash, if not
// try pulling it from another device.
lastError = verifyBuffer(buf, state.block)
// For receive-only folders, the hash is not SHA256 as it's an
// encrypted hash token. In that case we can't verify the block
// integrity so we'll take it on trust. (The other side can and
// will verify.)
if f.Type != config.FolderTypeReceiveEncrypted {
lastError = f.verifyBuffer(buf, state.block)
}
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "hash mismatch")
continue
@@ -1595,7 +1631,9 @@ func (f *sendReceiveFolder) finisherRoutine(snap *db.Snapshot, in <-chan *shared
blockStatsMut.Unlock()
}
f.model.progressEmitter.Deregister(state)
if f.Type != config.FolderTypeReceiveEncrypted {
f.model.progressEmitter.Deregister(state)
}
f.evLogger.Log(events.ItemFinished, map[string]interface{}{
"folder": f.folderID,
+2 -2
View File
@@ -123,9 +123,9 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
}
res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
if haveFcfg && fcfg.Type == config.FolderTypeReceiveOnly {
if haveFcfg && (fcfg.Type == config.FolderTypeReceiveOnly || fcfg.Type == config.FolderTypeReceiveEncrypted) {
// Add statistics for things that have changed locally in a receive
// only folder.
// only or receive encrypted folder.
res["receiveOnlyChangedFiles"] = ro.Files
res["receiveOnlyChangedDirectories"] = ro.Directories
res["receiveOnlyChangedSymlinks"] = ro.Symlinks
+18 -9
View File
@@ -23,15 +23,17 @@ import (
type indexSender struct {
suture.Service
conn protocol.Connection
folder string
fset *db.FileSet
prevSequence int64
evLogger events.Logger
connClosed chan struct{}
token suture.ServiceToken
pauseChan chan struct{}
resumeChan chan *db.FileSet
conn protocol.Connection
folder string
folderIsReceiveEncrypted bool
dev string
fset *db.FileSet
prevSequence int64
evLogger events.Logger
connClosed chan struct{}
token suture.ServiceToken
pauseChan chan struct{}
resumeChan chan *db.FileSet
}
func (s *indexSender) serve(ctx context.Context) {
@@ -169,6 +171,13 @@ func (s *indexSender) sendIndexTo(ctx context.Context) error {
f = fi.(protocol.FileInfo)
// If this is a folder receiving encrypted files only, we
// mustn't ever send locally changed file infos. Those aren't
// encrypted and thus would be a protocol error at the remote.
if s.folderIsReceiveEncrypted && fi.IsReceiveOnlyChanged() {
return true
}
// Mark the file as invalid if any of the local bad stuff flags are set.
f.RawInvalid = f.IsInvalid()
// If the file is marked LocalReceive (i.e., changed locally on a
+365 -125
View File
@@ -11,6 +11,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"path/filepath"
"reflect"
@@ -131,15 +132,17 @@ type model struct {
folderIOLimiter *byteSemaphore
// fields protected by fmut
fmut sync.RWMutex
folderCfgs map[string]config.FolderConfiguration // folder -> cfg
folderFiles map[string]*db.FileSet // folder -> files
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
folderRunners map[string]service // folder -> puller or scanner
folderRunnerToken map[string]suture.ServiceToken // folder -> token for folder runner
folderRestartMuts syncMutexMap // folder -> restart mutex
folderVersioners map[string]versioner.Versioner // folder -> versioner (may be nil)
fmut sync.RWMutex
folderCfgs map[string]config.FolderConfiguration // folder -> cfg
folderFiles map[string]*db.FileSet // folder -> files
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
folderRunners map[string]service // folder -> puller or scanner
folderRunnerToken map[string]suture.ServiceToken // folder -> token for folder runner
folderRestartMuts syncMutexMap // folder -> restart mutex
folderVersioners map[string]versioner.Versioner // folder -> versioner (may be nil)
folderEncryptionPasswordTokens map[string][]byte // folder -> encryption token (may be missing, and only for encryption type folders)
folderEncryptionFailures map[string]map[protocol.DeviceID]error // folder -> device -> error regarding encryption consistency (may be missing)
// fields protected by pmut
pmut sync.RWMutex
@@ -171,11 +174,18 @@ var (
errNetworkNotAllowed = errors.New("network not allowed")
errNoVersioner = errors.New("folder has no versioner")
// errors about why a connection is closed
errIgnoredFolderRemoved = errors.New("folder no longer ignored")
errReplacingConnection = errors.New("replacing connection")
errStopped = errors.New("Syncthing is being stopped")
errMissingRemoteInClusterConfig = errors.New("remote device missing in cluster config")
errMissingLocalInClusterConfig = errors.New("local device missing in cluster config")
errIgnoredFolderRemoved = errors.New("folder no longer ignored")
errReplacingConnection = errors.New("replacing connection")
errStopped = errors.New("Syncthing is being stopped")
errEncryptionInvConfigLocal = errors.New("can't encrypt data for a device when the folder type is receiveEncrypted")
errEncryptionInvConfigRemote = errors.New("remote has encrypted data and encrypts that data for us - this is impossible")
errEncryptionNotEncryptedLocal = errors.New("folder is announced as encrypted, but not configured thus")
errEncryptionNotEncryptedRemote = errors.New("folder is configured to be encrypted but not announced thus")
errEncryptionNotEncryptedUntrusted = errors.New("device is untrusted, but configured to receive not encrypted data")
errEncryptionPassword = errors.New("different encryption passwords used")
errEncryptionReceivedToken = errors.New("resetting connection to send info on new encrypted folder (new cluster config)")
errMissingRemoteInClusterConfig = errors.New("remote device missing in cluster config")
errMissingLocalInClusterConfig = errors.New("local device missing in cluster config")
)
// NewModel creates and starts a new model. The model starts in read-only mode,
@@ -207,14 +217,16 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
folderIOLimiter: newByteSemaphore(cfg.Options().MaxFolderConcurrency()),
// fields protected by fmut
fmut: sync.NewRWMutex(),
folderCfgs: make(map[string]config.FolderConfiguration),
folderFiles: make(map[string]*db.FileSet),
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
folderIgnores: make(map[string]*ignore.Matcher),
folderRunners: make(map[string]service),
folderRunnerToken: make(map[string]suture.ServiceToken),
folderVersioners: make(map[string]versioner.Versioner),
fmut: sync.NewRWMutex(),
folderCfgs: make(map[string]config.FolderConfiguration),
folderFiles: make(map[string]*db.FileSet),
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
folderIgnores: make(map[string]*ignore.Matcher),
folderRunners: make(map[string]service),
folderRunnerToken: make(map[string]suture.ServiceToken),
folderVersioners: make(map[string]versioner.Versioner),
folderEncryptionPasswordTokens: make(map[string][]byte),
folderEncryptionFailures: make(map[string]map[protocol.DeviceID]error),
// fields protected by pmut
pmut: sync.NewRWMutex(),
@@ -339,6 +351,14 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
ffs := fset.MtimeFS()
if cfg.Type == config.FolderTypeReceiveEncrypted {
if encryptionToken, err := readEncryptionToken(cfg); err == nil {
m.folderEncryptionPasswordTokens[folder] = encryptionToken
} else if !fs.IsNotExist(err) {
l.Warnf("Failed to read encryption token: %v", err)
}
}
// These are our metadata files, and they should always be hidden.
_ = ffs.Hide(config.DefaultMarkerName)
_ = ffs.Hide(".stversions")
@@ -1026,8 +1046,6 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
// Also, collect a list of folders we do share, and if he's interested in
// temporary indexes, subscribe the connection.
tempIndexFolders := make([]string, 0, len(cm.Folders))
m.pmut.RLock()
indexSenderRegistry, ok := m.indexSenders[deviceID]
m.pmut.RUnlock()
@@ -1042,11 +1060,37 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
return errDeviceUnknown
}
// Assemble the device information from the connected device about
// themselves and us for all folders.
ccDeviceInfos := make(map[string]*indexSenderStartInfo, len(cm.Folders))
for _, folder := range cm.Folders {
info := &indexSenderStartInfo{}
for _, dev := range folder.Devices {
if dev.ID == m.id {
info.local = dev
} else if dev.ID == deviceID {
info.remote = dev
}
if info.local.ID != protocol.EmptyDeviceID && info.remote.ID != protocol.EmptyDeviceID {
break
}
}
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())
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())
return errMissingLocalInClusterConfig
}
ccDeviceInfos[folder.ID] = info
}
// Needs to happen outside of the fmut, as can cause CommitConfiguration
if deviceCfg.AutoAcceptFolders {
changedFolders := make([]config.FolderConfiguration, 0, len(cm.Folders))
for _, folder := range cm.Folders {
if fcfg, fchanged := m.handleAutoAccepts(deviceCfg, folder); fchanged {
if fcfg, fchanged := m.handleAutoAccepts(deviceID, folder, ccDeviceInfos[folder.ID]); fchanged {
changedFolders = append(changedFolders, fcfg)
}
}
@@ -1061,91 +1105,16 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
}
}
paused := make(map[string]struct{}, len(cm.Folders))
seenFolders := make(map[string]struct{}, len(cm.Folders))
for _, folder := range cm.Folders {
seenFolders[folder.ID] = struct{}{}
cfg, ok := m.cfg.Folder(folder.ID)
if !ok || !cfg.SharedWith(deviceID) {
indexSenderRegistry.remove(folder.ID)
if deviceCfg.IgnoredFolder(folder.ID) {
l.Infof("Ignoring folder %s from device %s since we are configured to", folder.Description(), deviceID)
continue
}
m.cfg.AddOrUpdatePendingFolder(folder.ID, folder.Label, deviceID)
changed = true
m.evLogger.Log(events.FolderRejected, map[string]string{
"folder": folder.ID,
"folderLabel": folder.Label,
"device": deviceID.String(),
})
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
continue
}
deviceInfos := &indexSenderStartInfo{}
for _, dev := range folder.Devices {
if dev.ID == m.id {
deviceInfos.local = dev
} else if dev.ID == deviceID {
deviceInfos.remote = dev
}
if deviceInfos.local.ID != protocol.EmptyDeviceID && deviceInfos.remote.ID != protocol.EmptyDeviceID {
break
}
}
if deviceInfos.remote.ID == protocol.EmptyDeviceID {
l.Infof("Device %v sent cluster-config without the device info for the remote on folder %v", deviceID, folder.Description())
return errMissingRemoteInClusterConfig
}
if deviceInfos.local.ID == protocol.EmptyDeviceID {
l.Infof("Device %v sent cluster-config without the device info for us locally on folder %v", deviceID, folder.Description())
return errMissingLocalInClusterConfig
}
if folder.Paused {
indexSenderRegistry.remove(folder.ID)
paused[cfg.ID] = struct{}{}
continue
}
if cfg.Paused {
indexSenderRegistry.addPaused(cfg, deviceInfos)
continue
}
m.fmut.RLock()
fs, ok := m.folderFiles[folder.ID]
m.fmut.RUnlock()
if !ok {
// Shouldn't happen because !cfg.Paused, but might happen
// if the folder is about to be unpaused, but not yet.
continue
}
if !folder.DisableTempIndexes {
tempIndexFolders = append(tempIndexFolders, folder.ID)
}
indexSenderRegistry.add(cfg, fs, deviceInfos)
// We might already have files that we need to pull so let the
// folder runner know that it should recheck the index data.
m.fmut.RLock()
if runner := m.folderRunners[folder.ID]; runner != nil {
defer runner.SchedulePull()
}
m.fmut.RUnlock()
changedHere, tempIndexFolders, paused, err := m.ccHandleFolders(cm.Folders, deviceCfg, ccDeviceInfos, indexSenderRegistry)
if err != nil {
return err
}
indexSenderRegistry.removeAllExcept(seenFolders)
changed = changed || changedHere
m.pmut.Lock()
m.remotePausedFolders[deviceID] = paused
m.pmut.Unlock()
// This breaks if we send multiple CM messages during the same connection.
if len(tempIndexFolders) > 0 {
m.pmut.RLock()
conn, ok := m.conn[deviceID]
@@ -1184,6 +1153,212 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
return nil
}
func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*indexSenderStartInfo, indexSenders *indexSenderRegistry) (bool, []string, map[string]struct{}, error) {
var changed bool
var folderDevice config.FolderDeviceConfiguration
tempIndexFolders := make([]string, 0, len(folders))
paused := make(map[string]struct{}, len(folders))
seenFolders := make(map[string]struct{}, len(folders))
deviceID := deviceCfg.DeviceID
for _, folder := range folders {
seenFolders[folder.ID] = struct{}{}
cfg, ok := m.cfg.Folder(folder.ID)
if ok {
folderDevice, ok = cfg.Device(deviceID)
}
if !ok {
indexSenders.remove(folder.ID)
if deviceCfg.IgnoredFolder(folder.ID) {
l.Infof("Ignoring folder %s from device %s since we are configured to", folder.Description(), deviceID)
continue
}
m.cfg.AddOrUpdatePendingFolder(folder.ID, folder.Label, deviceID)
changed = true
m.evLogger.Log(events.FolderRejected, map[string]string{
"folder": folder.ID,
"folderLabel": folder.Label,
"device": deviceID.String(),
})
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
continue
}
if folder.Paused {
indexSenders.remove(folder.ID)
paused[cfg.ID] = struct{}{}
continue
}
if cfg.Paused {
indexSenders.addPaused(cfg, ccDeviceInfos[folder.ID])
continue
}
m.fmut.RLock()
fs, ok := m.folderFiles[folder.ID]
m.fmut.RUnlock()
if !ok {
// Shouldn't happen because !cfg.Paused, but might happen
// if the folder is about to be unpaused, but not yet.
l.Debugln("ccH: no fset", folder.ID)
continue
}
if err := m.ccCheckEncryption(cfg, folderDevice, ccDeviceInfos[folder.ID], deviceCfg.Untrusted); err != nil {
sameError := false
if devs, ok := m.folderEncryptionFailures[folder.ID]; ok {
sameError = devs[deviceID] == err
} else {
m.folderEncryptionFailures[folder.ID] = make(map[protocol.DeviceID]error)
}
m.folderEncryptionFailures[folder.ID][deviceID] = err
msg := fmt.Sprintf("Failure checking encryption consistency with device %v for folder %v: %v", deviceID, cfg.Description(), err)
if sameError || err == errEncryptionReceivedToken {
l.Debugln(msg)
} else {
l.Warnln(msg)
}
return changed, tempIndexFolders, paused, err
}
if devErrs, ok := m.folderEncryptionFailures[folder.ID]; ok {
if len(devErrs) == 1 {
delete(m.folderEncryptionFailures, folder.ID)
} else {
delete(m.folderEncryptionFailures[folder.ID], deviceID)
}
}
// Handle indexes
if !folder.DisableTempIndexes {
tempIndexFolders = append(tempIndexFolders, folder.ID)
}
indexSenders.add(cfg, fs, ccDeviceInfos[folder.ID])
// We might already have files that we need to pull so let the
// folder runner know that it should recheck the index data.
m.fmut.RLock()
if runner := m.folderRunners[folder.ID]; runner != nil {
defer runner.SchedulePull()
}
m.fmut.RUnlock()
}
indexSenders.removeAllExcept(seenFolders)
return changed, tempIndexFolders, paused, nil
}
func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice config.FolderDeviceConfiguration, ccDeviceInfos *indexSenderStartInfo, deviceUntrusted bool) error {
hasTokenRemote := len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0
hasTokenLocal := len(ccDeviceInfos.local.EncryptionPasswordToken) > 0
isEncryptedRemote := folderDevice.EncryptionPassword != ""
isEncryptedLocal := fcfg.Type == config.FolderTypeReceiveEncrypted
if !isEncryptedRemote && !isEncryptedLocal && deviceUntrusted {
return errEncryptionNotEncryptedUntrusted
}
if !(hasTokenRemote || hasTokenLocal || isEncryptedRemote || isEncryptedLocal) {
// Noone cares about encryption here
return nil
}
if isEncryptedRemote && isEncryptedLocal {
// Should never happen, but config racyness and be safe.
return errEncryptionInvConfigLocal
}
if hasTokenRemote && hasTokenLocal {
return errEncryptionInvConfigRemote
}
if !(hasTokenRemote || hasTokenLocal) {
return errEncryptionNotEncryptedRemote
}
if !(isEncryptedRemote || isEncryptedLocal) {
return errEncryptionNotEncryptedLocal
}
if isEncryptedRemote {
passwordToken := protocol.PasswordToken(fcfg.ID, folderDevice.EncryptionPassword)
match := false
if hasTokenLocal {
match = bytes.Equal(passwordToken, ccDeviceInfos.local.EncryptionPasswordToken)
} else {
// hasTokenRemote == true
match = bytes.Equal(passwordToken, ccDeviceInfos.remote.EncryptionPasswordToken)
}
if !match {
return errEncryptionPassword
}
return nil
}
// isEncryptedLocal == true
var ccToken []byte
if hasTokenLocal {
ccToken = ccDeviceInfos.local.EncryptionPasswordToken
} else {
// hasTokenRemote == true
ccToken = ccDeviceInfos.remote.EncryptionPasswordToken
}
m.fmut.RLock()
token, ok := m.folderEncryptionPasswordTokens[fcfg.ID]
m.fmut.RUnlock()
if !ok {
var err error
token, err = readEncryptionToken(fcfg)
if err != nil && !fs.IsNotExist(err) {
return err
}
if err == nil {
m.fmut.Lock()
m.folderEncryptionPasswordTokens[fcfg.ID] = token
m.fmut.Unlock()
} else {
if err := writeEncryptionToken(ccToken, fcfg); err != nil {
return err
}
m.fmut.Lock()
m.folderEncryptionPasswordTokens[fcfg.ID] = ccToken
m.fmut.Unlock()
// We can only announce ourselfs once we have the token,
// thus we need to resend CCs now that we have it.
m.resendClusterConfig(fcfg.DeviceIDs())
return nil
}
}
if !bytes.Equal(token, ccToken) {
return errEncryptionPassword
}
return nil
}
func (m *model) resendClusterConfig(ids []protocol.DeviceID) {
if len(ids) == 0 {
return
}
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)
}
}
m.pmut.RUnlock()
// Generating cluster-configs acquires fmut -> must happen outside of pmut.
for _, conn := range ccConns {
cm := m.generateClusterConfig(conn.ID())
go conn.ClusterConfig(cm)
}
}
// handleIntroductions handles adding devices/folders that are shared by an introducer device
func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (map[string]config.FolderConfiguration, map[protocol.DeviceID]config.DeviceConfiguration, folderDeviceSet, bool) {
changed := false
@@ -1295,7 +1470,7 @@ func (m *model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
// handleAutoAccepts handles adding and sharing folders for devices that have
// AutoAcceptFolders set to true.
func (m *model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) (config.FolderConfiguration, bool) {
func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *indexSenderStartInfo) (config.FolderConfiguration, bool) {
if cfg, ok := m.cfg.Folder(folder.ID); !ok {
defaultPath := m.cfg.Options().DefaultFolderPath
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
@@ -1310,25 +1485,40 @@ func (m *model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder p
fcfg := config.NewFolderConfiguration(m.id, folder.ID, folder.Label, fs.FilesystemTypeBasic, filepath.Join(defaultPath, path))
fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{
DeviceID: deviceCfg.DeviceID,
DeviceID: deviceID,
})
l.Infof("Auto-accepted %s folder %s at path %s", deviceCfg.DeviceID, folder.Description(), fcfg.Path)
if len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0 || len(ccDeviceInfos.local.EncryptionPasswordToken) > 0 {
fcfg.Type = config.FolderTypeReceiveEncrypted
}
l.Infof("Auto-accepted %s folder %s at path %s", deviceID, folder.Description(), fcfg.Path)
return fcfg, true
}
l.Infof("Failed to auto-accept folder %s from %s due to path conflict", folder.Description(), deviceCfg.DeviceID)
l.Infof("Failed to auto-accept folder %s from %s due to path conflict", folder.Description(), deviceID)
return config.FolderConfiguration{}, false
} else {
for _, device := range cfg.DeviceIDs() {
if device == deviceCfg.DeviceID {
if device == deviceID {
// Already shared nothing todo.
return config.FolderConfiguration{}, false
}
}
if cfg.Type == config.FolderTypeReceiveEncrypted {
if len(ccDeviceInfos.remote.EncryptionPasswordToken) == 0 && len(ccDeviceInfos.local.EncryptionPasswordToken) == 0 {
l.Infof("Failed to auto-accept device %s on existing folder %s as the remote wants to send us unencrypted data, but the folder type is receive-encrypted", folder.Description(), deviceID)
return config.FolderConfiguration{}, false
}
} else {
if len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0 || len(ccDeviceInfos.local.EncryptionPasswordToken) > 0 {
l.Infof("Failed to auto-accept device %s on existing folder %s as the remote wants to send us encrypted data, but the folder type is not receive-encrypted", folder.Description(), deviceID)
return config.FolderConfiguration{}, false
}
}
cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{
DeviceID: deviceCfg.DeviceID,
DeviceID: deviceID,
})
l.Infof("Shared %s with %s due to auto-accept", folder.ID, deviceCfg.DeviceID)
l.Infof("Shared %s with %s due to auto-accept", folder.ID, deviceID)
return cfg, true
}
}
@@ -1422,7 +1612,7 @@ func (r *requestResponse) Wait() {
// Request returns the specified data segment by reading it from local disk.
// Implements the protocol.Model interface.
func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
func (m *model) Request(deviceID protocol.DeviceID, folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
if size < 0 || offset < 0 {
return nil, protocol.ErrInvalid
}
@@ -1520,12 +1710,15 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size in
if err := readOffsetIntoBuf(folderFs, name, offset, res.data); 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)
return nil, protocol.ErrNoSuchFile
} else if err == io.EOF && len(hash) == 0 {
// Read beyond end of file when we can't verify the hash -- this is
// a padded read for an encrypted file. It's fine.
} 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)
return nil, protocol.ErrGeneric
}
if !scanner.Validate(res.data, hash, weakHash) {
if len(hash) > 0 && !scanner.Validate(res.data, 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)
return nil, protocol.ErrNoSuchFile
@@ -1862,7 +2055,7 @@ func (m *model) deviceWasSeen(deviceID protocol.DeviceID) {
}
}
func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
m.pmut.RLock()
nc, ok := m.conn[deviceID]
m.pmut.RUnlock()
@@ -1871,9 +2064,9 @@ func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, f
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
}
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x wh=%x ft=%t", m, deviceID, folder, name, offset, size, hash, weakHash, fromTemporary)
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)
return nc.Request(ctx, folder, name, offset, size, hash, weakHash, fromTemporary)
return nc.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
}
func (m *model) ScanFolders() map[string]error {
@@ -1974,6 +2167,17 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
continue
}
var encryptionToken []byte
var hasEncryptionToken bool
if folderCfg.Type == config.FolderTypeReceiveEncrypted {
if encryptionToken, hasEncryptionToken = m.folderEncryptionPasswordTokens[folderCfg.ID]; !hasEncryptionToken {
// We haven't gotten a token for us yet and without
// one the other side can't validate us - pretend
// we don't have the folder yet.
continue
}
}
protocolFolder := protocol.Folder{
ID: folderCfg.ID,
Label: folderCfg.Label,
@@ -2001,6 +2205,12 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
Introducer: deviceCfg.Introducer,
}
if deviceCfg.DeviceID == m.id && hasEncryptionToken {
protocolDevice.EncryptionPasswordToken = encryptionToken
} else if device.EncryptionPassword != "" {
protocolDevice.EncryptionPasswordToken = protocol.PasswordToken(folderCfg.ID, device.EncryptionPassword)
}
if fs != nil {
if deviceCfg.DeviceID == m.id {
protocolDevice.IndexID = fs.IndexID(protocol.LocalDeviceID)
@@ -2375,18 +2585,13 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
go conn.Close(errDeviceRemoved)
}
}
ccConns := make([]protocol.Connection, 0, len(clusterConfigDevices))
for id := range clusterConfigDevices {
if conn, ok := m.conn[id]; ok {
ccConns = append(ccConns, conn)
}
}
m.pmut.RUnlock()
// Generating cluster-configs acquires fmut -> must happen outside of pmut.
for _, conn := range ccConns {
cm := m.generateClusterConfig(conn.ID())
go conn.ClusterConfig(cm)
ids := make([]protocol.DeviceID, 0, len(clusterConfigDevices))
for id := range clusterConfigDevices {
ids = append(ids, id)
}
m.resendClusterConfig(ids)
m.globalRequestLimiter.setCapacity(1024 * to.Options.MaxConcurrentIncomingRequestKiB())
m.folderIOLimiter.setCapacity(to.Options.MaxFolderConcurrency())
@@ -2600,3 +2805,38 @@ func addDeviceIDsToMap(m map[protocol.DeviceID]struct{}, s []protocol.DeviceID)
}
return m
}
func encryptionTokenPath(cfg config.FolderConfiguration) string {
return filepath.Join(cfg.MarkerName, "syncthing-encryption_password_token")
}
type storedEncryptionToken struct {
FolderID string
Token []byte
}
func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
fd, err := cfg.Filesystem().Open(encryptionTokenPath(cfg))
if err != nil {
return nil, err
}
defer fd.Close()
var stored storedEncryptionToken
if err := json.NewDecoder(fd).Decode(&stored); err != nil {
return nil, err
}
return stored.Token, nil
}
func writeEncryptionToken(token []byte, cfg config.FolderConfiguration) error {
tokenName := encryptionTokenPath(cfg)
fd, err := cfg.Filesystem().OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0666)
if err != nil {
return err
}
defer fd.Close()
return json.NewEncoder(fd).Encode(storedEncryptionToken{
FolderID: cfg.ID,
Token: token,
})
}
+309 -129
View File
@@ -132,12 +132,35 @@ func newState(cfg config.Configuration) *model {
return m
}
func createClusterConfig(remote protocol.DeviceID, ids ...string) protocol.ClusterConfig {
cc := protocol.ClusterConfig{
Folders: make([]protocol.Folder, len(ids)),
}
for i, id := range ids {
cc.Folders[i] = protocol.Folder{
ID: id,
Label: id,
}
}
return addFolderDevicesToClusterConfig(cc, remote)
}
func addFolderDevicesToClusterConfig(cc protocol.ClusterConfig, remote protocol.DeviceID) protocol.ClusterConfig {
for i := range cc.Folders {
cc.Folders[i].Devices = []protocol.Device{
{ID: myID},
{ID: remote},
}
}
return cc
}
func TestRequest(t *testing.T) {
m := setupModel(defaultCfgWrapper)
defer cleanupModel(m)
// Existing, shared file
res, err := m.Request(device1, "default", "foo", 6, 0, nil, 0, false)
res, err := m.Request(device1, "default", "foo", 0, 6, 0, nil, 0, false)
if err != nil {
t.Fatal(err)
}
@@ -147,33 +170,37 @@ func TestRequest(t *testing.T) {
}
// Existing, nonshared file
_, err = m.Request(device2, "default", "foo", 6, 0, nil, 0, false)
_, err = m.Request(device2, "default", "foo", 0, 6, 0, nil, 0, false)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Nonexistent file
_, err = m.Request(device1, "default", "nonexistent", 6, 0, nil, 0, false)
_, err = m.Request(device1, "default", "nonexistent", 0, 6, 0, nil, 0, false)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Shared folder, but disallowed file name
_, err = m.Request(device1, "default", "../walk.go", 6, 0, nil, 0, false)
_, err = m.Request(device1, "default", "../walk.go", 0, 6, 0, nil, 0, false)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Negative offset
_, err = m.Request(device1, "default", "foo", -4, 0, nil, 0, false)
_, err = m.Request(device1, "default", "foo", 0, -4, 0, nil, 0, false)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Larger block than available
_, err = m.Request(device1, "default", "foo", 42, 0, nil, 0, false)
_, err = m.Request(device1, "default", "foo", 0, 42, 0, []byte("hash necessary but not checked"), 0, false)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
t.Error("Unexpected nil error on read past end of file")
}
_, err = m.Request(device1, "default", "foo", 0, 42, 0, nil, 0, false)
if err != nil {
t.Error("Unexpected error when large read should be permitted")
}
}
@@ -259,7 +286,7 @@ func BenchmarkRequestOut(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
data, err := m.requestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 32, nil, 0, false)
data, err := m.requestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 0, 32, nil, 0, false)
if err != nil {
b.Error(err)
}
@@ -283,7 +310,7 @@ func BenchmarkRequestInSingleFile(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 128<<10, 0, nil, 0, false); err != nil {
if _, err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 0, 128<<10, 0, nil, 0, false); err != nil {
b.Error(err)
}
}
@@ -854,14 +881,7 @@ func TestIssue5063(t *testing.T) {
wg := sync.WaitGroup{}
addAndVerify := func(id string) {
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("expected shared", id)
}
@@ -904,14 +924,7 @@ func TestAutoAcceptRejected(t *testing.T) {
defer cleanupModel(m)
id := srand.String(8)
defer os.RemoveAll(id)
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
if cfg, ok := m.cfg.Folder(id); ok && cfg.SharedWith(device1) {
t.Error("unexpected shared", id)
@@ -924,14 +937,7 @@ func TestAutoAcceptNewFolder(t *testing.T) {
defer cleanupModel(m)
id := srand.String(8)
defer os.RemoveAll(id)
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("expected shared", id)
}
@@ -942,28 +948,14 @@ func TestAutoAcceptNewFolderFromTwoDevices(t *testing.T) {
defer cleanupModel(m)
id := srand.String(8)
defer os.RemoveAll(id)
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("expected shared", id)
}
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device2) {
t.Error("unexpected expected shared", id)
}
m.ClusterConfig(device2, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device2, createClusterConfig(device2, id))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device2) {
t.Error("expected shared", id)
}
@@ -976,28 +968,14 @@ func TestAutoAcceptNewFolderFromOnlyOneDevice(t *testing.T) {
id := srand.String(8)
defer os.RemoveAll(id)
defer cleanupModel(m)
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("expected shared", id)
}
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device2) {
t.Error("unexpected expected shared", id)
}
m.ClusterConfig(device2, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device2, createClusterConfig(device2, id))
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device2) {
t.Error("unexpected shared", id)
}
@@ -1053,18 +1031,7 @@ func TestAutoAcceptMultipleFolders(t *testing.T) {
defer os.RemoveAll(id2)
m := newState(defaultAutoAcceptCfg)
defer cleanupModel(m)
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id1,
Label: id1,
},
{
ID: id2,
Label: id2,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id1, id2))
if fcfg, ok := m.cfg.Folder(id1); !ok || !fcfg.SharedWith(device1) {
t.Error("expected shared", id1)
}
@@ -1092,14 +1059,7 @@ func TestAutoAcceptExistingFolder(t *testing.T) {
if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device1) {
t.Error("missing folder, or shared", id)
}
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) || fcfg.Path != idOther {
t.Error("missing folder, or unshared, or path changed", id)
@@ -1125,18 +1085,7 @@ func TestAutoAcceptNewAndExistingFolder(t *testing.T) {
if fcfg, ok := m.cfg.Folder(id1); !ok || fcfg.SharedWith(device1) {
t.Error("missing folder, or shared", id1)
}
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id1,
Label: id1,
},
{
ID: id2,
Label: id2,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id1, id2))
for i, id := range []string{id1, id2} {
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
@@ -1166,14 +1115,7 @@ func TestAutoAcceptAlreadyShared(t *testing.T) {
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("missing folder, or not shared", id)
}
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("missing folder, or not shared", id)
@@ -1212,14 +1154,14 @@ func TestAutoAcceptPrefersLabel(t *testing.T) {
defer os.RemoveAll(id)
defer os.RemoveAll(label)
defer cleanupModel(m)
m.ClusterConfig(device1, protocol.ClusterConfig{
m.ClusterConfig(device1, addFolderDevicesToClusterConfig(protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: label,
},
},
})
}, device1))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) || !strings.HasSuffix(fcfg.Path, label) {
t.Error("expected shared, or wrong path", id, label, fcfg.Path)
}
@@ -1237,14 +1179,14 @@ func TestAutoAcceptFallsBackToID(t *testing.T) {
defer os.RemoveAll(label)
defer os.RemoveAll(id)
defer cleanupModel(m)
m.ClusterConfig(device1, protocol.ClusterConfig{
m.ClusterConfig(device1, addFolderDevicesToClusterConfig(protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: label,
},
},
})
}, device1))
if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) || !strings.HasSuffix(fcfg.Path, id) {
t.Error("expected shared, or wrong path", id, label, fcfg.Path)
}
@@ -1276,14 +1218,7 @@ func TestAutoAcceptPausedWhenFolderConfigChanged(t *testing.T) {
t.Fatal("folder running?")
}
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
m.generateClusterConfig(device1)
if fcfg, ok := m.cfg.Folder(id); !ok {
@@ -1333,14 +1268,7 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
t.Fatal("folder running?")
}
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.ClusterConfig(device1, createClusterConfig(device1, id))
m.generateClusterConfig(device1)
if fcfg, ok := m.cfg.Folder(id); !ok {
@@ -1361,6 +1289,113 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
}
}
func TestAutoAcceptEnc(t *testing.T) {
tcfg := defaultAutoAcceptCfg.Copy()
m := newState(tcfg)
defer cleanupModel(m)
id := srand.String(8)
defer os.RemoveAll(id)
token := []byte("token")
basicCC := func() protocol.ClusterConfig {
return protocol.ClusterConfig{
Folders: []protocol.Folder{{
ID: id,
Label: id,
}}}
}
// Earlier tests might cause the connection to get closed, thus ClusterConfig
// would panic.
clusterConfig := func(deviceID protocol.DeviceID, cm protocol.ClusterConfig) {
m.AddConnection(&fakeConnection{id: deviceID, model: m}, protocol.Hello{})
m.ClusterConfig(deviceID, cm)
}
clusterConfig(device1, basicCC())
if _, ok := m.cfg.Folder(id); ok {
t.Fatal("unexpected added")
}
cc := basicCC()
cc.Folders[0].Devices = []protocol.Device{{ID: device1}}
clusterConfig(device1, cc)
if _, ok := m.cfg.Folder(id); ok {
t.Fatal("unexpected added")
}
cc = basicCC()
cc.Folders[0].Devices = []protocol.Device{{ID: myID}}
clusterConfig(device1, cc)
if _, ok := m.cfg.Folder(id); ok {
t.Fatal("unexpected added")
}
// New folder, encrypted -> add as enc
cc = createClusterConfig(device1, id)
cc.Folders[0].Devices[1].EncryptionPasswordToken = token
clusterConfig(device1, cc)
if cfg, ok := m.cfg.Folder(id); !ok {
t.Fatal("unexpected unadded")
} else {
if !cfg.SharedWith(device1) {
t.Fatal("unexpected unshared")
}
if cfg.Type != config.FolderTypeReceiveEncrypted {
t.Fatal("Folder not added as receiveEncrypted")
}
}
// New device, unencrypted on encrypted folder -> reject
clusterConfig(device2, createClusterConfig(device2, id))
if cfg, _ := m.cfg.Folder(id); cfg.SharedWith(device2) {
t.Fatal("unexpected shared")
}
// New device, encrypted on encrypted folder -> share
cc = createClusterConfig(device2, id)
cc.Folders[0].Devices[1].EncryptionPasswordToken = token
clusterConfig(device2, cc)
if cfg, _ := m.cfg.Folder(id); !cfg.SharedWith(device2) {
t.Fatal("unexpected unshared")
}
// New folder, no encrypted -> add "normal"
id = srand.String(8)
defer os.RemoveAll(id)
clusterConfig(device1, createClusterConfig(device1, id))
if cfg, ok := m.cfg.Folder(id); !ok {
t.Fatal("unexpected unadded")
} else {
if !cfg.SharedWith(device1) {
t.Fatal("unexpected unshared")
}
if cfg.Type != config.FolderTypeSendReceive {
t.Fatal("Folder not added as send-receive")
}
}
// New device, encrypted on unencrypted folder -> reject
cc = createClusterConfig(device2, id)
cc.Folders[0].Devices[1].EncryptionPasswordToken = token
clusterConfig(device2, cc)
if cfg, _ := m.cfg.Folder(id); cfg.SharedWith(device2) {
t.Fatal("unexpected shared")
}
// New device, unencrypted on unencrypted folder -> share
clusterConfig(device2, createClusterConfig(device2, id))
if cfg, _ := m.cfg.Folder(id); !cfg.SharedWith(device2) {
t.Fatal("unexpected unshared")
}
}
func changeIgnores(t *testing.T, m *model, expected []string) {
arrEqual := func(a, b []string) bool {
if len(a) != len(b) {
@@ -3220,14 +3255,14 @@ func TestRequestLimit(t *testing.T) {
file := "tmpfile"
befReq := time.Now()
first, err := m.Request(device1, "default", file, 2000, 0, nil, 0, false)
first, err := m.Request(device1, "default", file, 0, 2000, 0, nil, 0, false)
if err != nil {
t.Fatalf("First request failed: %v", err)
}
reqDur := time.Since(befReq)
returned := make(chan struct{})
go func() {
second, err := m.Request(device1, "default", file, 2000, 0, nil, 0, false)
second, err := m.Request(device1, "default", file, 0, 2000, 0, nil, 0, false)
if err != nil {
t.Errorf("Second request failed: %v", err)
}
@@ -3829,7 +3864,10 @@ func TestClusterConfigOnFolderAdd(t *testing.T) {
fcfg := testFolderConfigTmp()
fcfg.ID = "second"
fcfg.Label = "second"
fcfg.Devices = []config.FolderDeviceConfiguration{{device2, protocol.EmptyDeviceID}}
fcfg.Devices = []config.FolderDeviceConfiguration{{
DeviceID: device2,
IntroducedBy: protocol.EmptyDeviceID,
}}
if w, err := cfg.SetFolder(fcfg); err != nil {
t.Fatal(err)
} else {
@@ -3841,7 +3879,10 @@ func TestClusterConfigOnFolderAdd(t *testing.T) {
func TestClusterConfigOnFolderShare(t *testing.T) {
testConfigChangeTriggersClusterConfigs(t, true, true, nil, func(cfg config.Wrapper) {
fcfg := cfg.FolderList()[0]
fcfg.Devices = []config.FolderDeviceConfiguration{{device2, protocol.EmptyDeviceID}}
fcfg.Devices = []config.FolderDeviceConfiguration{{
DeviceID: device2,
IntroducedBy: protocol.EmptyDeviceID,
}}
if w, err := cfg.SetFolder(fcfg); err != nil {
t.Fatal(err)
} else {
@@ -4161,6 +4202,145 @@ func TestNeedMetaAfterIndexReset(t *testing.T) {
}
}
func TestCcCheckEncryption(t *testing.T) {
w, fcfg := tmpDefaultWrapper()
m := setupModel(w)
m.Stop()
defer cleanupModel(m)
pw := "foo"
token := protocol.PasswordToken(fcfg.ID, pw)
m.folderEncryptionPasswordTokens[fcfg.ID] = token
testCases := []struct {
tokenRemote, tokenLocal []byte
isEncryptedRemote, isEncryptedLocal bool
expectedErr error
}{
{
tokenRemote: token,
tokenLocal: token,
expectedErr: errEncryptionInvConfigRemote,
},
{
isEncryptedRemote: true,
isEncryptedLocal: true,
expectedErr: errEncryptionInvConfigLocal,
},
{
tokenRemote: token,
tokenLocal: nil,
isEncryptedRemote: false,
isEncryptedLocal: false,
expectedErr: errEncryptionNotEncryptedLocal,
},
{
tokenRemote: token,
tokenLocal: nil,
isEncryptedRemote: true,
isEncryptedLocal: false,
expectedErr: nil,
},
{
tokenRemote: token,
tokenLocal: nil,
isEncryptedRemote: false,
isEncryptedLocal: true,
expectedErr: nil,
},
{
tokenRemote: nil,
tokenLocal: token,
isEncryptedRemote: true,
isEncryptedLocal: false,
expectedErr: nil,
},
{
tokenRemote: nil,
tokenLocal: token,
isEncryptedRemote: false,
isEncryptedLocal: true,
expectedErr: nil,
},
{
tokenRemote: nil,
tokenLocal: token,
isEncryptedRemote: false,
isEncryptedLocal: false,
expectedErr: errEncryptionNotEncryptedLocal,
},
{
tokenRemote: nil,
tokenLocal: nil,
isEncryptedRemote: true,
isEncryptedLocal: false,
expectedErr: errEncryptionNotEncryptedRemote,
},
{
tokenRemote: nil,
tokenLocal: nil,
isEncryptedRemote: false,
isEncryptedLocal: true,
expectedErr: errEncryptionNotEncryptedRemote,
},
{
tokenRemote: nil,
tokenLocal: nil,
isEncryptedRemote: false,
isEncryptedLocal: false,
expectedErr: nil,
},
}
for i, tc := range testCases {
tfcfg := fcfg.Copy()
if tc.isEncryptedLocal {
tfcfg.Type = config.FolderTypeReceiveEncrypted
m.folderEncryptionPasswordTokens[fcfg.ID] = token
}
dcfg := config.FolderDeviceConfiguration{DeviceID: device1}
if tc.isEncryptedRemote {
dcfg.EncryptionPassword = pw
}
deviceInfos := &indexSenderStartInfo{
remote: protocol.Device{ID: device1, EncryptionPasswordToken: tc.tokenRemote},
local: protocol.Device{ID: myID, EncryptionPasswordToken: tc.tokenLocal},
}
err := m.ccCheckEncryption(tfcfg, dcfg, deviceInfos, false)
if err != tc.expectedErr {
t.Errorf("Testcase %v: Expected error %v, got %v", i, tc.expectedErr, err)
}
if tc.expectedErr == nil {
err := m.ccCheckEncryption(tfcfg, dcfg, deviceInfos, true)
if tc.isEncryptedRemote || tc.isEncryptedLocal {
if err != nil {
t.Errorf("Testcase %v: Expected no error, got %v", i, err)
}
} else {
if err != errEncryptionNotEncryptedUntrusted {
t.Errorf("Testcase %v: Expected error %v, got %v", i, errEncryptionNotEncryptedUntrusted, err)
}
}
}
if err != nil || (!tc.isEncryptedRemote && !tc.isEncryptedLocal) {
continue
}
if tc.isEncryptedLocal {
m.folderEncryptionPasswordTokens[fcfg.ID] = []byte("notAMatch")
} else {
dcfg.EncryptionPassword = "notAMatch"
}
err = m.ccCheckEncryption(tfcfg, dcfg, deviceInfos, false)
if err != errEncryptionPassword {
t.Errorf("Testcase %v: Expected error %v, got %v", i, errEncryptionPassword, err)
}
}
}
func equalStringsInAnyOrder(a, b []string) bool {
if len(a) != len(b) {
return false
+3 -3
View File
@@ -101,7 +101,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
<-done
// Request a file by traversing the symlink
res, err := m.Request(device1, "default", "symlink/requests_test.go", 10, 0, nil, 0, false)
res, err := m.Request(device1, "default", "symlink/requests_test.go", 0, 10, 0, nil, 0, false)
if err == nil || res != nil {
t.Error("Managed to traverse symlink")
}
@@ -499,7 +499,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
t.Fatalf("unexpected weak hash: %d != 103547413", f.Blocks[0].WeakHash)
}
res, err := m.Request(device1, "default", "foo", int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
res, err := m.Request(device1, "default", "foo", 0, int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
if err != nil {
t.Fatal(err)
}
@@ -513,7 +513,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
must(t, writeFile(tfs, "foo", payload, 0777))
_, err = m.Request(device1, "default", "foo", int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
_, err = m.Request(device1, "default", "foo", 0, int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
if err == nil {
t.Fatalf("expected failure")
}
+47 -2
View File
@@ -7,6 +7,7 @@
package model
import (
"encoding/binary"
"time"
"github.com/pkg/errors"
@@ -131,7 +132,7 @@ func (s *sharedPullerState) tempFile() (*lockedWriterAt, error) {
return s.writer, nil
}
if err := inWritableDir(s.tempFileInWritableDir, s.fs, s.tempName, s.ignorePerms); err != nil {
if err := s.addWriterLocked(); err != nil {
s.failLocked(err)
return nil, err
}
@@ -139,6 +140,10 @@ func (s *sharedPullerState) tempFile() (*lockedWriterAt, error) {
return s.writer, nil
}
func (s *sharedPullerState) addWriterLocked() error {
return inWritableDir(s.tempFileInWritableDir, s.fs, s.tempName, s.ignorePerms)
}
// tempFileInWritableDir should only be called from tempFile.
func (s *sharedPullerState) tempFileInWritableDir(_ string) error {
// The permissions to use for the temporary file should be those of the
@@ -184,9 +189,14 @@ func (s *sharedPullerState) tempFileInWritableDir(_ string) error {
// Don't truncate symlink files, as that will mean that the path will
// contain a bunch of nulls.
if s.sparse && !s.file.IsSymlink() {
size := s.file.Size
// Trailer added to encrypted files
if len(s.file.Encrypted) > 0 {
size += int64(s.file.ProtoSize() + 4)
}
// Truncate sets the size of the file. This creates a sparse file or a
// space reservation, depending on the underlying filesystem.
if err := fd.Truncate(s.file.Size); err != nil {
if err := fd.Truncate(size); err != nil {
// The truncate call failed. That can happen in some cases when
// space reservation isn't possible or over some network
// filesystems... This generally doesn't matter.
@@ -305,6 +315,13 @@ func (s *sharedPullerState) finalClose() (bool, error) {
return false, nil
}
if len(s.file.Encrypted) > 0 {
if err := s.finalizeEncrypted(); err != nil && s.err == nil {
// This is our error as we weren't errored before.
s.err = err
}
}
if s.writer != nil {
if err := s.writer.SyncClose(s.fsync); err != nil && s.err == nil {
// This is our error as we weren't errored before.
@@ -324,6 +341,34 @@ func (s *sharedPullerState) finalClose() (bool, error) {
return true, s.err
}
// finalizeEncrypted adds a trailer to the encrypted file containing the
// serialized FileInfo and the length of that FileInfo. When initializing a
// folder from encrypted data we can extract this FileInfo from the end of
// the file and regain the original metadata.
func (s *sharedPullerState) finalizeEncrypted() error {
size := s.file.ProtoSize()
bs := make([]byte, 4+size)
n, err := s.file.MarshalTo(bs)
if err != nil {
return err
}
binary.BigEndian.PutUint32(bs[n:], uint32(n))
bs = bs[:n+4]
if s.writer == nil {
if err := s.addWriterLocked(); err != nil {
return err
}
}
if _, err := s.writer.WriteAt(bs, s.file.Size); err != nil {
return err
}
s.file.Size += int64(len(bs))
return nil
}
// Progress returns the momentarily progress for the puller
func (s *sharedPullerState) Progress() *pullerProgress {
s.mut.RLock()
+1
View File
@@ -49,6 +49,7 @@ func init() {
defaultCfg = defaultCfgWrapper.RawCopy()
defaultAutoAcceptCfg = config.Configuration{
Version: config.CurrentVersion,
Devices: []config.DeviceConfiguration{
{
DeviceID: myID, // self