< copy of https://github.com/sqlite/sqlite/blob/sqlite3-rsync/tool/sqlite3-rsync.c >
Explain the protocol over SSH part of this
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:
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
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
}
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.
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
andctx.pOut
areFILE*
streams connected to the SSH process's stdin and stdout.
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.
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.
-
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);
-
Waiting for Replica Messages:
- Listens for messages from the replica.
- Possible messages include
REPLICA_HASH
,REPLICA_READY
,REPLICA_ERROR
, andREPLICA_END
.
-
Processing Replica Hashes:
- On receiving
REPLICA_HASH
, it collects the hashes sent by the replica.
readBytes(p, 20, buf); // Read the 20-byte hash
- On receiving
-
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
- After receiving
-
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);
- Sends
-
Initialization:
- Waits for
ORIGIN_BEGIN
from the origin. - Opens the replica database.
- Waits for
-
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
-
Indicating Readiness:
- After sending all hashes, sends
REPLICA_READY
to indicate it's ready to receive pages.
writeByte(p, REPLICA_READY); fflush(p->pOut);
- After sending all hashes, sends
-
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
- On receiving
-
Committing the Transaction:
- On receiving
ORIGIN_TXN
, commits the transaction. - Sends
REPLICA_END
if needed.
- On receiving
-
Error Handling:
- If an error occurs, sends
REPLICA_ERROR
with an error message.
- If an error occurs, sends
-
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);
-
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.
- Both sides send their respective end messages (
ORIGIN_END
orREPLICA_END
) to signal the completion of the synchronization process. - The communication channels are closed, and resources are cleaned up.
Scenario: Synchronizing local.db
with remote.db
on remotehost
.
-
User Command:
sqlite3-rsync local.db user@remotehost:/path/to/remote.db
-
Program Execution:
- Constructs SSH command and starts communication with the remote replica.
- Local program runs
originSide()
. - Remote program (over SSH) runs
replicaSide()
.
-
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
).
- Origin sends database info (
-
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.
-
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.
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.