Passive Peer

    +

    Description — Couchbase Lite’s Peer-to-Peer Synchronization enables edge devices to synchronize securely without consuming centralized cloud-server resources
    Abstract — How to set up a Listener to accept a Replicator connection and sync using peer-to-peer
    Related Content — API Reference | Passive Peer | Active Peer

    Enterprise Edition only
    This an Enterprise Edition feature. Purchase the Enterprise License, which includes official Couchbase Support, to use it in production (see the license and support Licensing).
    iOS Restrictions
    iOS 14 Applications

    When your application attempts to access the user’s local network, iOS will prompt them to allow (or deny) access. You can customize the message presented to the user by editing the description for the NSLocalNetworkUsageDescription key in the Info.plist.

    Code Snippets
    The code examples are indicative only. They demonstrate basic concepts and approaches to using a feature. Use them as inspiration and adapt these examples to best practice when developing applications for your platform.

    Introduction

    This content provides code and configuration examples covering the implementation of Peer-to-Peer Sync over websockets. Specifically it covers the implementation of a Passive Peer.

    This passive peer (also referred to as the server, or listener) will accept a connection from an Active Peer (also referred to as the client or replicator) and participate in the replication of database changes to synchronize both databases.

    Subsequent sections provide additional details, and examples for the main configuration options.

    Secure Storage
    The use of TLS, its associated keys and certificates requires using secure storage to minimize the chances of a security breach. The implementation of this storage differs from platform to platform — see Using secure storage.

    Configuration Summary

    You should configure and initialize a listener for each Couchbase Lite database instance you want to sync. There is no limit on the number of listeners you may configure — Example 1 shows a simple initialization and configuration process.

    Example 1. Listener configuration and initialization
    fileprivate var _thisListener:URLEndpointListener?
    fileprivate var thisDB:Database?
    
      let listenerConfig =
        URLEndpointListenerConfiguration(database: thisDB!) (1)
    
      /* optionally */ let wsPort: UInt16 = 55991
      /* optionally */ let wssPort: UInt16 = 55990
      listenerConfig.port =  wssPort (2)
    
      listenerConfig.networkInterface = "10.1.1.10"  (3)
    
      listenerConfig.enableDeltaSync = true (4)
    
      listenerConfig.disableTLS  = false (5)
    
      // Set the credentials the server presents the client
      // Use an anonymous self-signed cert
      listenerConfig.tlsIdentity = nil (6)
    
      // Configure how the client is to be authenticated
      // Here, use Basic Authentication
      listenerConfig.authenticator =
        ListenerPasswordAuthenticator(authenticator: {
          (validUser, validPassword) -> Bool in
            if (self._allowlistedUsers.contains {
                  $0 == validPassword && $1 == validUser
                }) {
                return true
                }
              return false
            }) (7)
    
    
      // Initialize the listener
      _thisListener = URLEndpointListener(config: listenerConfig) (8)
      guard let thisListener = _thisListener else {
        throw ListenerError.NotInitialized
        // ... take appropriate actions
      }
      // Start the listener
      try thisListener.start() (9)

    Notes on example:

    1 Identify the local database to be used — see: Initialize the Listener Configuration
    2 Optionally, choose a port to use. By default the system will automatically assign a port — to over-ride this, see: [set-network-and-port]
    3 Optionally, choose a network-interface to use. By default the system will listen on all network interfaces — to over-ride this see: [set-network-and-port]
    4 Optionally, choose to sync only changes, the default is not to enable delta-sync — see: Delta Sync.
    5 Set server security. TLS is always enabled out-of-the-box, so you can usually omit this line. But you can, optionally, disable TLS (not advisable in production) — see: TLS Security
    6 Set the credentials this server will present to the client for authentication. Here we show the default TLS authentication, which is by anonymous self-signed certificate. The server must always authenticate itself to the client.
    7 Set client security — define the credentials the server expects the client to present for authentication. Here we show how basic authentication is configured to authenticate the client-supplied credentials from the http authentication header against valid credentials — see Authenticating the Client for more options.

    Note that client authentication is optional.

    8 Initialize the listener using the configuration settings.
    9 Start Listener

    API References

    You can find Swift API References here.

    Device Discovery

    This phase is optional: If the listener is initialized on a well known URL endpoint (for example, a static IP Address or well known DNS address) then you can configure active peers to connect to those.

    Prior to initiating the listener you may execute a peer discovery phase.

    For the passive peer, this involves advertising the service using, for example Bonjour (see: https://developer.apple.com/bonjour/) and waiting for an invite from the active peer.

    The connection is established once the passive peer has authenticated and accepted an active peer’s invitation.

    Initialize the Listener Configuration

    Initialize the listener configuration with the local database. All other configuration values take their default setting.

    Each Listener instance serves one Couchbase Lite database. Couchbase sets no hard limit on the number of listeners you can initialize.

    Example 2. Specify Local Database

    In this example thisDB has previously been declared as an object of type Database.

    let listenerConfig =
      URLEndpointListenerConfiguration(database: thisDB!) (1)

    Notes on example:

    1 Set the local database using the URLEndpointListenerConfiguration's constructor init(database:).

    The database must be opened before the listener is started.

    Set Port and Network Interface

    Port number

    The Listener will automatically select an available port if you do not specify one.

    Example 3. Specify a Port to Use
    /* optionally */ let wsPort: UInt16 = 55991
    /* optionally */ let wssPort: UInt16 = 55990
    listenerConfig.port =  wssPort (1)

    Notes on example:

    1 To use a canonical port — one known to other applications — specify it explicitly using the port method shown here. Ensure that any port you do specify is not blocked by firewall rules.

    Network Interface

    The Listener will listen on all network interfaces by default.

    Example 4. Specify a Network Interface to Use
    listenerConfig.networkInterface = "10.1.1.10"  (1)
    Notes on Example 4
    1 To specify an interface — one known to other applications — identify it explicitly, using the networkInterface method shown here. This must be either an IP Address or network interface name such as en0.
    Where necessary, you can identify the available interfaces at runtime, using appropriate platform tools — see Example 5.
    Example 5. Identify available network interfaces
    import SystemConfiguration
    // . . .
    
      #if os(macOS)
      for interface in SCNetworkInterfaceCopyAll() as! [SCNetworkInterface] {
          // do something with this `interface`
      }
      #endif
    // . . .

    Delta Sync

    Delta Sync allows clients to sync only those parts of a document that have changed. This can result in significant savings in bandwidth consumption as well as throughput improvements. Both valuable benefits, especially when network bandwidth is constrained.

    Example 6. Enable delta sync
    listenerConfig.enableDeltaSync = true (1)

    Notes on example:

    1 Delta sync replication is not enabled by default. Use URLEndpointListenerConfiguration's enableDeltaSync method to activate or deactivate it.

    TLS Security

    Enable or Disable TLS

    Define whether the connection is to use TLS or clear text.

    TLS based encryption is enabled by default and this setting ought to be used in any production environment. However, it can be disabled, for example for development and-or test environments.

    When TLS is enabled, Couchbase Lite provides several options on how the Listener may be configured with an appropriate TLS Identity — see Configure TLS Identity for Listener.

    You can use URLEndpointListenerConfiguration's disableTLS method to disable TLS communication if necessary

    The disableTLS setting must be 'false' when Client Cert Authentication is required.

    Basic Authentication can be used with, or without, TLS.

    disableTLS works in conjunction with TLSIdentity, to enable developers to define the key and certificate to be used.

    • If disableTLS is true — TLS communication is disabled and TLS identity is ignored. Active peers will use the ws:// URL scheme used to connect to the listener.

    • If disableTLS is false or not specified — TLS communication is enabled.

      Active peers will use the wss:// URL scheme to connect to the listener.

    Configure TLS Identity for Listener

    Define the credentials the server will present to the client for authentication. Note that the server must always authenticate itself with the client — see Active Peer - authenticating the listener for how the client deals with this.

    Use URLEndpointListenerConfiguration's tlsIdentity method to configure the TLS Identity used in TLS communication.

    If TLSIdentity is not set, then the listener uses an auto-generated anonymous self-signed identity (unless disableTLS = true). Whilst the client cannot use this to authenticate the server, it will use it to encrypt communication, giving a more secure option than non-TLS communication.

    The auto-generated anonymous self-signed identity is saved in Keychain for future use to obviate the need to re-generate it.

    Typically, you will configure the listener’s TLS Identity once during initial launch and re-use it (from Keychain on any subsequent starts.
    Example 7. Set Listener’s TLS identity
    • Import

    • Create Self-Signed Cert

    • Use Anonymous Self-Signed Certificate

    Import an identity from a secure key and certificate data source.

    listenerConfig.disableTLS  = false (1)
    
    guard let pathToCert =
      Bundle.main.path(forResource: "cert", ofType: "p12")
    else { /* process error */ return }
    
    guard let localCertificate =
      try? NSData(contentsOfFile: pathToCert) as Data
    else { /* process error */ return } (2)
    
    let thisIdentity =
      try TLSIdentity.importIdentity(withData: localCertificate,
                                    password: "123",
                                    label: thisSecId) (3)
    
    // Set the credentials the server presents the client
    listenerConfig.tlsIdentity = thisIdentity    (4)

    Notes on example:

    1 Ensure TLS is used
    2 Get key and certificate data
    3 Use the retrieved data to create and store the TLS identity
    4 Set this identity as the one presented in response to the client’s prompt

    Create a TLSIdentity for the server using convenience API. The system generates a self-signed certificate.

    listenerConfig.disableTLS  = false (1)
    
    let attrs = [certAttrCommonName: "Couchbase Inc"] (2)
    
    let thisIdentity =
      try TLSIdentity.createIdentity(forServer: true, /* isServer */
            attributes: attrs,
            expiration: Date().addingTimeInterval(86400),
            label: "Server-Cert-Label") (3)
    
    // Set the credentials the server presents the client
    listenerConfig.tlsIdentity = thisIdentity    (4)

    Notes on example:

    1 Ensure TLS is used.
    2 Map the required certificate attributes, in this case the common name.
    3 Create the required TLS identity using the attributes. Add to Keychain as 'couchbase-docs-cert'.
    4 Configure the server to present the defined identity credentials when prompted.

    This examples uses an “anonymous” self signed certificate. Generated certificates are held in Keychain.

    listenerConfig.disableTLS  = false (1)
    
    // Set the credentials the server presents the client
    // Use an anonymous self-signed cert
    listenerConfig.tlsIdentity = nil (2)

    Notes on example:

    1 These are are the default settings. Authentication using an anonymous self-signed certificate is assumed.

    Authenticating the Client

    Define how the server (listener) will authenticate the client as one it is prepared to interact with.

    Whilst client authentication is optional, Couchbase lite provides the necessary tools to implement it. Use the URLEndpointListenerConfiguration class’s authenticator method to specify how the client-supplied credentials are to be authenticated.

    Valid options are:

    • No authentication — If you do not define an Authenticator then all clients are accepted.

    • Basic Authentication — uses the ListenerPasswordAuthenticator to authenticate the client using the client-supplied username and password (from the http authentication header).

    • ListenerCertificateAuthenticator — which authenticates the client using a client supplied chain of one or more certificates. You should initialize the authenticator using one of the following constructors:

      • A list of one or more root certificates — the client supplied certificate must end at a certificate in this list if it is to be authenticated

      • A block of code that assumes total responsibility for authentication — it must return a boolean response (true for an authenticated client, or false for a failed authentication).

    Use Basic Authentication

    Define how to authenticate client-supplied username and password credentials. To use client-supplied certificates instead — see: Using Client Certificate Authentication

    Example 8. Password authentication
    // Configure how the client is to be authenticated
    // Here, use Basic Authentication
    listenerConfig.authenticator =
      ListenerPasswordAuthenticator(authenticator: {
        (validUser, validPassword) -> Bool in
          if (self._allowlistedUsers.contains {
                $0 == validPassword && $1 == validUser
              }) {
              return true
              }
            return false
          }) (1)

    Notes on example:

    1 Where 'username'/'password' are the client-supplied values (from the http-authentication header) and validUser/validPassword are the values acceptable to the server.

    Using Client Certificate Authentication

    Define how the server will authenticate client-supplied certificates.

    There are two ways to authenticate a client:

    • A chain of one or more certificates that ends at a certificate in the list of certificates supplied to the constructor for ListenerCertificateAuthenticator.

    • Application logic: This method assumes complete responsibility for verifying and authenticating the client.

      If the parameter supplied to the constructor for ListenerCertificateAuthenticator is of type ListenerCertificateAuthenticatorDelegate, all other forms of authentication are bypassed.

      The client response to the certificate request is passed to the method supplied as the constructor parameter. The logic should take the form of function or block (such as, a closure expression) where the platform allows.

    Example 9. Set Certificate Authorization
    • Root CA

    • Application Logic

    Configure the server (listener) to authenticate the client against a list of one or more certificates provided by the server to the the ListenerCertificateAuthenticator.

    // Authenticate using Cert Authority
    
    // cert is a pre-populated object of type:SecCertificate representing a certificate
    let rootCertData = SecCertificateCopyData(cert) as Data (1)
    let rootCert = SecCertificateCreateWithData(kCFAllocatorDefault, rootCertData as CFData)! //
    
    listenerConfig.authenticator = ListenerCertificateAuthenticator.init (rootCerts: [rootCert]) (2) (3)

    Notes on example:

    1 Get the identity data to authenticate against. This can be, for example, from a resource file provided with the app, or an identity previously saved in Keychain.
    2 Configure the authenticator to authenticate the client supplied certificate(s) using these root certs. A valid client will provide one or more certificates that match a certificate in this list.
    3 Add the authenticator to the listener configuration.

    Configure the server (listener) to authenticate the client using user-supplied logic.

    // Authenticate self-signed cert using application logic
    
    // cert is a user-supplied object of type:SecCertificate representing a certificate
    let rootCertData = SecCertificateCopyData(cert) as Data (1)
    let rootCert = SecCertificateCreateWithData(kCFAllocatorDefault, rootCertData as CFData)!
    
    listenerConfig.authenticator = ListenerCertificateAuthenticator.init { (2)
      (certs) -> Bool in
        var certs:SecCertificate
        var certCommonName:CFString?
        let status=SecCertificateCopyCommonName(certs[0], &certCommonName)
        if (self._allowedCommonNames.contains(["name": certCommonName! as String])) {
            return true
        }
        return false
    } (3)

    Notes on example:

    1 Get the identity data to authenticate against. This can be, for example, from a resource file provided with the app, or an identity previously saved in Keychain.
    2 Configure the Authenticator to pass the root certificates to a user supplied code block. This code assumes complete responsibility for authenticating the client supplied certificate(s). It must return a boolean value; with true denoting the client supplied certificate authentic.
    3 Add the authenticator to the listener configuration.

    Delete Entry

    You can remove unwanted TLS identities from Keychain using the convenience API.

    Example 10. Deleting TLS Identities
    try TLSIdentity.deleteIdentity(withLabel: serverCertLabel);

    The Impact of TLS Settings

    The table in this section shows the expected system behavior (in regards to security) depending on the TLS configuration settings deployed.

    Table 1. Expected system behavior
    disableTLS tlsIdentity (corresponding to server) Expected system behavior

    true

    Ignored

    TLS is disabled; all communication is plain text.

    false

    set to nil

    • The system will auto generate an anonymous self signed cert.

    • Active peers (clients) should be configured to accept self-signed certificates.

    • Communication is encrypted

    false

    Set to server identity generated from a self- or CA-signed certificate

    • On first use — Bring your own certificate and private key; for example, using the TLSIdentity class’s CreateIdentity() method to add it to the Keychain.

    • Each time — Use the server identity from the certificate stored in the Keychain; for example, using the TLSIdentity class’s identity(withLabel:) method with the alias you want to retrieve..

    • System will use the configured identity.

    • Active peers will validate the server certificate corresponding to the TLSIdentity (as long as they are configured to not skip validation — see TLS Security).

    Start Listener

    Once you have completed the Listener’s configuration settings you can initialize the Listener instance and start it running — see: Example 11

    Example 11. Initialize and start listener
    // Initialize the listener
    _thisListener = URLEndpointListener(config: listenerConfig) (1)
    guard let thisListener = _thisListener else {
      throw ListenerError.NotInitialized
      // ... take appropriate actions
    }
    // Start the listener
    try thisListener.start() (2)

    Monitor Listener

    Use the Listener’s status property/method to get counts of total and active connections — see: Example 12.

    You should note that these counts can be extremely volatile. So, the actual number of active connections may have changed, by the time the ConnectionStatus class returns a result.

    Example 12. Get connection counts
    let totalConnections = thisListener.status.connectionCount
    let activeConnections = thisListener.status.activeConnectionCount

    Notes on example:

    Stop Listener

    It is best practice to check tha status of the listener’s connections and stop only when you have confirmed that there are no active connections — see Example 12.

    Example 13. Stop listener using stop method
    thisListener.stop()
    Closing the database will also close the listener.