Skip to content

Instantly share code, notes, and snippets.

@simonw
Last active October 14, 2024 10:01
Show Gist options
  • Save simonw/ffbf90e0602df04c2f6b387de42acba4 to your computer and use it in GitHub Desktop.
Save simonw/ffbf90e0602df04c2f6b387de42acba4 to your computer and use it in GitHub Desktop.

Prompt

< copy of https://github.com/sqlite/sqlite/blob/sqlite3-rsync/tool/sqlite3-rsync.c >

Explain the protocol over SSH part of this

Response

The sqlite3-rsync utility is designed to synchronize SQLite databases efficiently, especially when dealing with large databases where only a small portion of the data has changed. One of its key features is the ability to synchronize a local database with a remote one over SSH, minimizing bandwidth usage by only transferring the necessary data.

Here's a detailed explanation of how the protocol over SSH works in this program:

1. Understanding the Use Case

When you run sqlite3-rsync with one of the databases specified as a remote location (using the USER@HOST:PATH syntax), the program needs to communicate with a counterpart on the remote machine. This communication is established over SSH.

Example Command:

sqlite3-rsync local.db user@remotehost:/path/to/remote.db

2. Argument Parsing and Determining Roles

The program starts by parsing command-line arguments to determine:

  • Which database is the origin (source) and which is the replica (destination).
  • Whether the origin or replica is local or remote.
if (ctx.zOrigin == 0) {
  fprintf(stderr, "missing ORIGIN database filename\n");
  return 1;
}
if (ctx.zReplica == 0) {
  fprintf(stderr, "missing REPLICA database filename\n");
  return 1;
}

It checks for the presence of a colon : to identify remote databases:

if ((zDiv = strchr(ctx.zOrigin, ':')) != 0) {
  // Remote ORIGIN and local REPLICA
} else if ((zDiv = strchr(ctx.zReplica, ':')) != 0) {
  // Local ORIGIN and remote REPLICA
}

3. Constructing the SSH Command

If the origin or replica is remote, the program constructs an SSH command to run the appropriate counterpart on the remote machine.

Remote Origin Example:

sqlite3_str *pStr = sqlite3_str_new(0);
append_escaped_arg(pStr, zSsh, 1);           // SSH command
sqlite3_str_appendf(pStr, " -e none");       // SSH options
*(zDiv++) = 0;                               // Split USER@HOST and PATH
append_escaped_arg(pStr, ctx.zOrigin, 0);    // USER@HOST
append_escaped_arg(pStr, zExe, 1);           // Remote executable (sqlite3-rsync)
append_escaped_arg(pStr, "--origin", 0);     // Indicate origin side
append_escaped_arg(pStr, zDiv, 1);           // Remote database path
append_escaped_arg(pStr, file_tail(ctx.zReplica), 1); // Replica database filename
zCmd = sqlite3_str_finish(pStr);

Resulting SSH Command:

ssh -e none user@remotehost sqlite3-rsync --origin /path/to/remote.db local.db

Note:

  • append_escaped_arg() is used to safely construct the command, preventing shell injection.
  • file_tail() extracts the filename from a path.

4. Establishing Communication Over SSH

The program uses a custom popen2() function to execute the SSH command and establish bidirectional communication:

if (popen2(zCmd, &ctx.pIn, &ctx.pOut, &childPid, 0)) {
  fprintf(stderr, "Could not start auxiliary process: %s\n", zCmd);
  return 1;
}
  • popen2() starts the SSH process and provides file handles for reading and writing.
  • ctx.pIn and ctx.pOut are FILE* streams connected to the SSH process's stdin and stdout.

5. Role-Specific Functions

Depending on whether the local machine is the origin or the replica, the program calls the corresponding function:

  • Local Origin:

    originSide(&ctx);
  • Local Replica:

    replicaSide(&ctx);

These functions handle the protocol logic for their respective roles.

6. Protocol Messages

The communication protocol is defined using message codes:

#define ORIGIN_BEGIN    0x41 // 'A'
#define ORIGIN_END      0x42 // 'B'
#define ORIGIN_ERROR    0x43 // 'C'
#define ORIGIN_PAGE     0x44 // 'D'
#define ORIGIN_TXN      0x45 // 'E'
#define ORIGIN_MSG      0x46 // 'F'

#define REPLICA_BEGIN   0x61 // 'a'
#define REPLICA_ERROR   0x62 // 'b'
#define REPLICA_END     0x63 // 'c'
#define REPLICA_HASH    0x64 // 'd'
#define REPLICA_READY   0x65 // 'e'
#define REPLICA_MSG     0x66 // 'f'

Messages are sent as single bytes, followed by any necessary data.

7. Communication Flow

A. Origin Side (originSide() Function)

  1. Initialization:

    • Opens the origin database.
    • Sends ORIGIN_BEGIN with protocol version, page size, and total page count.
    writeByte(p, ORIGIN_BEGIN);
    writeByte(p, PROTOCOL_VERSION);
    writePow2(p, szPg); // Page size as power of two
    writeUint32(p, nPage); // Total number of pages
    fflush(p->pOut);
  2. Waiting for Replica Messages:

    • Listens for messages from the replica.
    • Possible messages include REPLICA_HASH, REPLICA_READY, REPLICA_ERROR, and REPLICA_END.
  3. Processing Replica Hashes:

    • On receiving REPLICA_HASH, it collects the hashes sent by the replica.
    readBytes(p, 20, buf); // Read the 20-byte hash
  4. Sending Missing Pages:

    • After receiving REPLICA_READY, it compares the replica's hashes with its own pages.
    • Sends ORIGIN_PAGE messages for pages that differ or are missing.
    writeByte(p, ORIGIN_PAGE);
    writeUint32(p, pgno); // Page number
    writeBytes(p, szPg, pContent); // Page content
  5. Finalizing the Transaction:

    • Sends ORIGIN_TXN to indicate the transaction should be committed.
    • Sends ORIGIN_END to signal the end of communication.
    writeByte(p, ORIGIN_TXN);
    writeUint32(p, nPage);
    writeByte(p, ORIGIN_END);

B. Replica Side (replicaSide() Function)

  1. Initialization:

    • Waits for ORIGIN_BEGIN from the origin.
    • Opens the replica database.
  2. Sending Page Hashes:

    • Computes hashes of its pages up to the minimum of its page count and the origin's page count.
    • Sends REPLICA_HASH messages with the page hashes.
    writeByte(p, REPLICA_HASH);
    writeBytes(p, 20, a); // Hash of the page
  3. Indicating Readiness:

    • After sending all hashes, sends REPLICA_READY to indicate it's ready to receive pages.
    writeByte(p, REPLICA_READY);
    fflush(p->pOut);
  4. Receiving Pages:

    • On receiving ORIGIN_PAGE, updates its database with the new page content.
    readUint32(p, &pgno); // Page number
    readBytes(p, szOPage, buf); // Page content
    // Insert or update the page in the database
  5. Committing the Transaction:

    • On receiving ORIGIN_TXN, commits the transaction.
    • Sends REPLICA_END if needed.
  6. Error Handling:

    • If an error occurs, sends REPLICA_ERROR with an error message.

8. Data Transmission

  • Data Formats:

    • Integers are transmitted in big-endian format.
    • Page sizes are sent as powers of two.
    • Hashes are 20-byte values (e.g., SHA-1 or custom hash).
  • Reading and Writing Functions:

    int readUint32(SQLiteRsync *p, unsigned int *pU);
    void writeUint32(SQLiteRsync *p, unsigned int x);
    int readByte(SQLiteRsync *p);
    void writeByte(SQLiteRsync *p, int c);
    void readBytes(SQLiteRsync *p, int nByte, void *pData);
    void writeBytes(SQLiteRsync *p, int nByte, const void *pData);

9. Ensuring Data Integrity and Efficiency

  • Page Hash Comparison:

    • Only pages that differ between the origin and replica are transmitted.
    • This minimizes bandwidth usage.
  • Transaction Management:

    • Transactions ensure that the replica database remains consistent.
    • Changes are committed only after all necessary pages are received.

10. Closing the Communication

  • Both sides send their respective end messages (ORIGIN_END or REPLICA_END) to signal the completion of the synchronization process.
  • The communication channels are closed, and resources are cleaned up.

11. Example Workflow

Scenario: Synchronizing local.db with remote.db on remotehost.

  1. User Command:

    sqlite3-rsync local.db user@remotehost:/path/to/remote.db
  2. Program Execution:

    • Constructs SSH command and starts communication with the remote replica.
    • Local program runs originSide().
    • Remote program (over SSH) runs replicaSide().
  3. Synchronization Steps:

    • Origin sends database info (ORIGIN_BEGIN).
    • Replica sends page hashes (REPLICA_HASH).
    • Origin sends missing or updated pages (ORIGIN_PAGE).
    • Origin signals transaction commit (ORIGIN_TXN).
    • Communication ends (ORIGIN_END, REPLICA_END).

12. Security and Robustness

  • Argument Escaping:

    • The program carefully escapes all arguments in SSH commands to prevent shell injection attacks.
    • Uses append_escaped_arg() to handle special characters.
  • Error Handling:

    • Errors are communicated between the origin and replica using error messages.
    • Both sides can gracefully handle errors and terminate the synchronization if necessary.

13. Utilities and Helper Functions

  • Hash Functions:

    • Custom hash functions are used to compute page hashes.
    • Ensures compatibility and consistency between different systems.
  • Time Measurement:

    • The program measures the time taken for synchronization to provide performance metrics.
    sqlite3_int64 currentTime(void);
  • Logging:

    • Verbosity levels can be adjusted to provide more detailed output.
    • Logs can help in debugging and monitoring the synchronization process.

Conclusion

The protocol over SSH in sqlite3-rsync allows for efficient and secure synchronization of SQLite databases between a local and a remote machine. By carefully constructing SSH commands, establishing communication channels, and using a defined set of protocol messages, the program minimizes bandwidth usage and ensures data integrity during the synchronization process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment