Syncthing — Interfaces#
Interface catalog#
protocol.Connection#
- Package:
github.com/syncthing/syncthing/lib/protocol - File:
lib/protocol/protocol.go:110 - Methods:
Index(ctx context.Context, idx *Index) error IndexUpdate(ctx context.Context, idxUp *IndexUpdate) error Request(ctx context.Context, req *Request) ([]byte, error) ClusterConfig(config *ClusterConfig, passwords map[string]string) DownloadProgress(ctx context.Context, dp *DownloadProgress) Start() Close(err error) DeviceID() DeviceID Statistics() Statistics Closed() <-chan struct{} ConnectionInfo // embedded - Purpose: Represents one active BEP (Block Exchange Protocol) session with a remote peer. Abstracts the transport (QUIC, TCP, relay) from protocol message sending. The embedding of
ConnectionInfoattaches metadata (type, transport, address, crypto) to the messaging surface. - Implementations:
rawConnection(unexported, the only production implementation). Tests use fakes. - Design quality: Well-segregated for its domain — every method maps to a BEP message type or lifecycle event. The
Closed() <-chan struct{}method returns a receive-only channel rather than a callback, which integrates naturally withselect. Intentionally broad because BEP has a fixed number of message types.
protocol.Model#
- Package:
github.com/syncthing/syncthing/lib/protocol - File:
lib/protocol/protocol.go:78 - Methods:
Index(conn Connection, idx *Index) error IndexUpdate(conn Connection, idxUp *IndexUpdate) error Request(conn Connection, req *Request) (RequestResponse, error) ClusterConfig(conn Connection, config *ClusterConfig) error Closed(conn Connection, err error) DownloadProgress(conn Connection, p *DownloadProgress) error - Purpose: The callback/handler interface that receives incoming BEP messages from a peer. Defined in
lib/protocolbut consumed by the protocol dispatcher and satisfied bymodel.Model. This is the classic consumer-defined interface: the protocol layer owns the interface, and the model layer owns the implementation. - Implementations:
model.model(the concrete sync engine), test fakes. - Design quality: Clean separation of concerns — protocol decodes messages, model handles business logic. Each method receives the originating
Connection, enabling multi-peer routing without extra state. Six methods mirror exactly the BEP message types that require a handler, no more.
model.Model#
- Package:
github.com/syncthing/syncthing/lib/model - File:
lib/model/model.go:76 - Methods: ~30 methods including:
suture.Service // embedded connections.Model // embedded (→ protocol.Model + AddConnection + OnHello + DeviceStatistics) // Folder operations ResetFolder, DelayScan, ScanFolder, ScanFolders, ScanFolderSubdirs State, FolderErrors, WatchError, Override, Revert, BringToFront LoadIgnores, CurrentIgnores, SetIgnores GetFolderVersions, RestoreFolderVersions // Index queries (iterator-based) LocalFiles, LocalFilesSequenced, AllGlobalFiles LocalSize, GlobalSize, NeedSize, ReceiveOnlySize NeedFolderFiles, RemoteNeedFolderFiles, LocalChangedFolderFiles // File lookup CurrentFolderFile, CurrentGlobalFile, Availability // Peer/connection info Completion, ConnectionStats, ConnectedTo DeviceStatistics, FolderStatistics // Pending device/folder handshake PendingDevices, PendingFolders, DismissPendingDevice, DismissPendingFolder // Misc GlobalDirectoryTree, RequestGlobal, UsageReportingStats - Purpose: The central abstraction of the entire application. Everything the REST API, discovery, connections, and UI need from the sync engine is expressed through this interface. Doubles as both an active service (via
suture.Service) and a queryable façade. - Implementations:
model(unexported struct inlib/model). All consumers (API, connections, usage reporter) hold the interface value, not the concrete type. - Design quality: Deliberately broad — this is the “God interface” of the application, aggregating operations across folder management, file queries, peer connections, and statistics. Its size (~30 methods) reflects the real complexity of the domain rather than poor design; narrower facets are used where possible (e.g.,
connections.Modelis a 4-method subset). The use ofiter.Seq[T]for file iteration (Go 1.23+ rangefunc) is modern and avoids materializing large slices.
config.Wrapper#
- Package:
github.com/syncthing/syncthing/lib/config - File:
lib/config/wrapper.go:91 - Methods: ~25 methods including:
suture.Service // embedded ConfigPath() string MyID() protocol.DeviceID RawCopy() Configuration RequiresRestart() bool Save() error Modify(ModifyFunction) (Waiter, error) RemoveFolder(id string) (Waiter, error) RemoveDevice(id protocol.DeviceID) (Waiter, error) // Accessors GUI() GUIConfiguration LDAP() LDAPConfiguration Options() OptionsConfiguration DefaultIgnores() Ignores Folder, Folders, FolderList, FolderPasswords, DefaultFolder Device, Devices, DeviceList, DefaultDevice IgnoredDevices, IgnoredDevice, IgnoredFolder Subscribe(c Committer) Configuration Unsubscribe(c Committer) - Purpose: The live configuration façade. All components hold
config.Wrapperand subscribe to it viaCommittercallbacks for change notification.Modify()serializes mutations through a queue, then notifies all subscribers, enabling config changes to propagate at runtime without restart. - Implementations:
wrapper(unexported). - Design quality: The
Modify(ModifyFunction) (Waiter, error)API is notably elegant — callers pass a function that receives a mutable copy of config, keeping mutation intent explicit and atomic. TheWaiterreturn value lets callers optionally block until the change has been committed and all subscribers notified.
config.Committer#
- Package:
github.com/syncthing/syncthing/lib/config - File:
lib/config/wrapper.go:60 - Methods:
CommitConfiguration(from, to Configuration) (handled bool) String() string - Purpose: The observer/subscriber interface for configuration changes. Any component that needs to react to config changes implements this. The
handled boolreturn lets a subscriber signal whether a change requires a full restart (true = handled gracefully, false = restart needed). - Implementations:
model.model,connections.service,api.service,discover.Manager, and several others — essentially every major component. - Design quality: Minimal (2 methods) and well-focused. The
String()method is included for logging purposes so the config subsystem can identify which subscriber did not handle a change.
fs.Filesystem#
- Package:
github.com/syncthing/syncthing/lib/fs - File:
lib/fs/filesystem.go:31 - Methods: ~22 methods including:
Chmod, Lchown, Chtimes Create, Open, OpenFile CreateSymlink, ReadSymlink, SymlinksSupported DirNames, Mkdir, MkdirAll Remove, RemoveAll, Rename Lstat, Stat Walk Watch(path, ignore Matcher, ctx, ignorePerms) (<-chan Event, <-chan error, error) Hide, Unhide, Glob, Roots, Usage Type() FilesystemType URI() string Options() []Option SameFile(fi1, fi2 FileInfo) bool PlatformData, GetXattr, SetXattr - Purpose: Full abstraction of filesystem operations. Enables per-folder filesystem type selection (local, virtual) and layered decoration (mtime correction, case normalization, metrics, debug logging). Watch returns dual channels — one for file events, one for fatal errors — compatible with
select-driven event loops. - Implementations:
basicFilesystem(os-backed), wrapped bymetricsFS,walkFilesystem,logFilesystem,mtimeFilesystem,caseFilesystem,errorFilesystem(for bad configuration).NewFilesystem()factory composes these layers. - Design quality: Broad but justified — this mirrors the Unix filesystem API. The decorator chain via
wrappingFilesystem.underlying()and the genericunwrapFilesystem[T]()function (using Go generics) provide a type-safe way to peel back layers when needed. Xattr methods and platform data are recent additions for cross-platform metadata sync.
fs.File#
- Package:
github.com/syncthing/syncthing/lib/fs - File:
lib/fs/filesystem.go:75 - Methods:
io.Closer, io.Reader, io.ReaderAt, io.Seeker, io.Writer, io.WriterAt // embedded Name() string Truncate(size int64) error Stat() (FileInfo, error) Sync() error - Purpose: Abstract file handle returned by
Filesystem.Open()/Create()/OpenFile(). Mirrorsos.Filebut removes OS-specific methods. - Implementations: Thin wrappers over
os.Filein the basic filesystem. - Design quality: Good use of stdlib interface composition. Embedding six standard
iointerfaces gives this interface a well-understood semantic contract while adding only the 4 most essential extras (Name,Truncate,Stat,Sync).
events.Logger#
- Package:
github.com/syncthing/syncthing/lib/events - File:
lib/events/events.go:236 - Methods:
suture.Service // embedded Log(t EventType, data interface{}) Subscribe(mask EventType) Subscription - Purpose: The in-process event bus. Components emit typed events; the REST API subscribes and long-polls them. The
mask EventTypebitmask on subscriptions allows filtering at subscription time rather than inside consumers, keeping the fan-out efficient. - Implementations:
logger(unexported). ANopLoggerexists for tests. - Design quality: Intentionally minimal public surface (2 methods beyond the service lifecycle). Complexity lives inside
logger’s serialized event loop. TheEventTypebitmask is an effective optimization — high-volume events likeFolderScanProgresscan be masked off by subscribers that don’t care.
events.Subscription#
- Package:
github.com/syncthing/syncthing/lib/events - File:
lib/events/events.go:262 - Methods:
C() <-chan Event Poll(timeout time.Duration) (Event, error) Mask() EventType Unsubscribe() - Purpose: A filtered subscription to the event bus.
C()exposes the underlying channel for use inselectstatements;Poll()offers a blocking call for single-event consumers (e.g., the REST API’s long-poll handler). - Implementations:
subscription(unexported),BufferedSubscription(buffered wrapper with separate interface). - Design quality: Dual access paths (channel vs. blocking poll) serve different calling patterns without forcing consumers to wrap channels themselves.
discover.Finder / discover.FinderService#
- Package:
github.com/syncthing/syncthing/lib/discover - File:
lib/discover/discover.go:18 - Methods (
Finder):Lookup(ctx context.Context, deviceID protocol.DeviceID) (address []string, err error) Error() error String() string Cache() map[protocol.DeviceID]CacheEntry - Methods (
FinderService): embedsFinder+suture.Service - Purpose:
Finderabstracts a single discovery backend (local UDP, global HTTPS, relay).FinderServiceextends it with a supervised background goroutine, used for backends that must maintain persistent state (e.g., caching, keep-alive pings). TheManageraggregates multipleFinderServicevalues and returns merged address lists. - Implementations:
localDiscovery(UDP beacon),globalDiscovery(HTTPS to relay server),relayFinder. - Design quality: Clean two-tier design.
Finderis the minimal contract;FinderServicecomposes it with lifecycle management. TheError()method enables the Manager to surface per-backend health to the REST API without coupling the API to the concrete backend types.
connections.Service#
- Package:
github.com/syncthing/syncthing/lib/connections - File:
lib/connections/service.go:129 - Methods:
suture.Service // embedded discover.AddressLister // embedded (ExternalAddresses, AllAddresses) ListenerStatus() map[string]ListenerStatusEntry ConnectionStatus() map[string]ConnectionStatusEntry NATType() string - Purpose: The transport manager’s public face. Embeds
discover.AddressListerso that the discovery system can query what addresses the connection service is actually listening on — breaking the circular dependency at the interface level. - Implementations:
service(unexported). - Design quality: Narrow and purposeful. The embedding of
AddressListeris the key architectural trick; it resolves the bootstrap dependency cycle between discovery and connections by defining what connections must provide to discovery.
Interface patterns#
Size distribution#
Syncthing’s interfaces span a wide range:
- Tiny (1-2 methods):
config.Waiter(1),config.Verifier(1),discover.AddressLister(2),config.Committer(2),events.Logger(2+suture) - Medium (4-10 methods):
protocol.Model(6),discover.Finder(4),events.Subscription(4),protocol.ConnectionInfo(8) - Large (10-25 methods):
protocol.Connection(11+embedded),fs.Filesystem(~22),config.Wrapper(~25),fs.File(4+6 embedded) - Very large (30+):
model.Model(~30 methods)
The large interfaces tend to be the architectural boundaries between major subsystems; narrow interfaces appear at internal or adapter seams.
Embedding#
Interface composition via embedding is pervasive and deliberate:
connections.Modelembedsprotocol.Model(widening the callback surface for connection events)model.Modelembedssuture.Service+connections.Model(service lifecycle + peer callbacks + model operations)connections.Serviceembedssuture.Service+discover.AddressListerdiscover.FinderServiceembedsFinder+suture.Servicefs.Fileembeds 6 stdlibiointerfacesprotocol.ConnectionembedsConnectionInfo
This layered embedding reflects deliberate ISP: consumers that only need lifecycle management take suture.Service; those that need peer callbacks take protocol.Model; those that need the full sync engine take model.Model.
Implicit satisfaction (consumer-defined interfaces)#
Nearly all interfaces are defined by the consumer package, not the provider — the canonical Go pattern:
protocol.Modelis defined inlib/protocoland satisfied bylib/modelconnections.Modelis defined inlib/connectionsand satisfied bylib/modelconfig.Committeris defined inlib/configand satisfied by every subscribing servicefs.Matcheris defined inlib/fsand satisfied bylib/ignore
The only exception to the pattern is model.Model itself, which is defined by the provider (lib/model) and consumed by lib/api and others — but this is practical given its size.
Stdlib interfaces used#
io.Closer,io.Reader,io.Writer,io.ReaderAt,io.WriterAt,io.Seeker— all infs.Filesuture.Service(the project’s “standard” service lifecycle interface, used as pervasively as stdlibio.Reader)context.Context— all network and filesystem operations pass contextnet.Addr— connection metadata
Key abstractions#
model.Model— The central hub of the entire system. Every other component either queries it, feeds events into it, or receives callbacks from it. Its breadth reflects the complexity of the sync domain; there is no smaller interface that would be sufficient.protocol.Connection— Defines the full BEP session contract. The separation ofConnection(sender) fromprotocol.Model(receiver) creates a clean bidirectional design: one interface for outgoing messages, one for incoming callbacks.fs.Filesystem— The OS abstraction that enables decorator-based layering (mtime, case, metrics, logging) and makes the entire sync engine testable without touching the real filesystem. The genericunwrapFilesystem[T]()function is a subtle but important design detail.config.Wrapper+config.Committer— Together these form a publish/subscribe configuration bus.Wrapperowns the data and serializes mutations;Committeris the observer contract all services implement. The runtime reconfiguration story depends entirely on this pair.events.Logger+events.Subscription— The internal event bus. These two interfaces decouple all status/progress reporting from the REST API and from each other. Any component can emit events without knowing who is watching; the API subscribes only to what it needs via bitmask.
Interface-driven extensibility#
Syncthing uses interfaces as first-class extension points in three areas:
Filesystem backends: fs.Filesystem is registered via filesystemFactories (a map of FilesystemType → factory function). New filesystem types (e.g., S3, virtual, encrypted) can be registered without changing any consumer. The decorator chain (mtime, case, metrics, walk, log) is composed at NewFilesystem() time using the wrappingFilesystem unwrap protocol.
Discovery backends: discover.FinderService is the extension point. The Manager accepts any number of FinderService implementations. Adding a new discovery mechanism (DNS-SD, mDNS, DHT) means implementing a 4-method Finder and wrapping it with a suture service — no changes to the Manager.
Transport dialers and listeners: connections.genericDialer and connections.genericListener (both unexported) define the transport plugin contracts. Implementations exist for QUIC, TCP, and relay. Adding a new transport means implementing these two interfaces and registering the dialerFactory / listenerFactory.
File versioning: lib/versioner.Versioner (not detailed above) abstracts the versioning strategy per folder. Multiple implementations exist (trash-can, staggered, simple, external), selectable per-folder in config — all through a common interface.