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 ConnectionInfo attaches 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 with select. 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/protocol but consumed by the protocol dispatcher and satisfied by model.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 in lib/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.Model is a 4-method subset). The use of iter.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.Wrapper and subscribe to it via Committer callbacks 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. The Waiter return 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 bool return 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 by metricsFS, 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 generic unwrapFilesystem[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(). Mirrors os.File but removes OS-specific methods.
  • Implementations: Thin wrappers over os.File in the basic filesystem.
  • Design quality: Good use of stdlib interface composition. Embedding six standard io interfaces 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 EventType bitmask on subscriptions allows filtering at subscription time rather than inside consumers, keeping the fan-out efficient.
  • Implementations: logger (unexported). A NopLogger exists for tests.
  • Design quality: Intentionally minimal public surface (2 methods beyond the service lifecycle). Complexity lives inside logger’s serialized event loop. The EventType bitmask is an effective optimization — high-volume events like FolderScanProgress can 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 in select statements; 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): embeds Finder + suture.Service
  • Purpose: Finder abstracts a single discovery backend (local UDP, global HTTPS, relay). FinderService extends it with a supervised background goroutine, used for backends that must maintain persistent state (e.g., caching, keep-alive pings). The Manager aggregates multiple FinderService values and returns merged address lists.
  • Implementations: localDiscovery (UDP beacon), globalDiscovery (HTTPS to relay server), relayFinder.
  • Design quality: Clean two-tier design. Finder is the minimal contract; FinderService composes it with lifecycle management. The Error() 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.AddressLister so 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 AddressLister is 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.Model embeds protocol.Model (widening the callback surface for connection events)
  • model.Model embeds suture.Service + connections.Model (service lifecycle + peer callbacks + model operations)
  • connections.Service embeds suture.Service + discover.AddressLister
  • discover.FinderService embeds Finder + suture.Service
  • fs.File embeds 6 stdlib io interfaces
  • protocol.Connection embeds ConnectionInfo

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.Model is defined in lib/protocol and satisfied by lib/model
  • connections.Model is defined in lib/connections and satisfied by lib/model
  • config.Committer is defined in lib/config and satisfied by every subscribing service
  • fs.Matcher is defined in lib/fs and satisfied by lib/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 in fs.File
  • suture.Service (the project’s “standard” service lifecycle interface, used as pervasively as stdlib io.Reader)
  • context.Context — all network and filesystem operations pass context
  • net.Addr — connection metadata

Key abstractions#

  1. 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.

  2. protocol.Connection — Defines the full BEP session contract. The separation of Connection (sender) from protocol.Model (receiver) creates a clean bidirectional design: one interface for outgoing messages, one for incoming callbacks.

  3. 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 generic unwrapFilesystem[T]() function is a subtle but important design detail.

  4. config.Wrapper + config.Committer — Together these form a publish/subscribe configuration bus. Wrapper owns the data and serializes mutations; Committer is the observer contract all services implement. The runtime reconfiguration story depends entirely on this pair.

  5. 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.