feat: add SSH transport, rsync-style CLI, and --stdio server mode

- Replace send()/recv() with read()/write() + io_set_fds() for fd abstraction
- Add client_connect_ssh() via socketpair + fork/exec; reap child in client_disconnect()
- Add server --stdio flag for SSH invocation
- Replace handle_arg() with rsync-style positional args (fastsync <src> <dest>)
- Add SSH vs TCP auto-detection via is_remote_dest()
- Add TransportType and ssh_destination to Config, fix uninitialized fields in
  config_receive() (pre-existing multithreaded crash)
- Sender threads now wait for server STATUS_OK before disconnecting
- Add --help, error handling for sendfile+SSH combos
- Add pre-flight checks and Posix Args test case to test.py
- Add test_config_ssh_dest() unit test
This commit is contained in:
2026-07-06 18:42:33 +02:00
parent e4ab2a414c
commit fbd7ccf4dd
8 changed files with 347 additions and 40 deletions
+5
View File
@@ -22,6 +22,8 @@ Config *config_create(char *version, char *send_directory,
config->compression_level = compression_level;
config->num_connections = num_connections;
config->use_sendfile = use_sendfile;
config->transport = TRANSPORT_TCP;
config->ssh_destination = NULL;
return config;
}
@@ -29,6 +31,7 @@ void config_delete(Config *config) {
free(config->version);
free(config->send_directory);
free(config->receive_root_directory);
free(config->ssh_destination);
free(config);
}
@@ -63,6 +66,8 @@ Config *config_receive(int file_descriptor) {
config->compression_level = receive_int(file_descriptor);
config->num_connections = receive_int(file_descriptor);
config->use_sendfile = receive_int(file_descriptor);
config->transport = TRANSPORT_TCP;
config->ssh_destination = NULL;
send_status(file_descriptor, STATUS_OK);
return config;
}
+7
View File
@@ -3,6 +3,11 @@
#include <stdbool.h>
typedef enum {
TRANSPORT_TCP,
TRANSPORT_SSH
} TransportType;
typedef struct Config {
char *version;
char *send_directory;
@@ -16,6 +21,8 @@ typedef struct Config {
bool use_metadata;
int compression_level;
int num_connections;
TransportType transport;
char *ssh_destination;
} Config;
Config *config_create(char *version, char *send_directory,
+138 -8
View File
@@ -1,12 +1,134 @@
#include "socket.h"
#include "log.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
static __thread int io_read_fd = -1;
static __thread int io_write_fd = -1;
void io_set_fds(int read_fd, int write_fd) {
io_read_fd = read_fd;
io_write_fd = write_fd;
}
static int io_fd(int dir_fd, int file_descriptor) {
return (dir_fd != -1) ? dir_fd : file_descriptor;
}
typedef struct {
char user[256];
char host[256];
char remote_path[4096];
} RemoteDest;
static int parse_remote_dest(const char *dest, RemoteDest *r) {
const char *colon = strchr(dest, ':');
if (!colon) return -1;
size_t remote_path_len = strlen(colon + 1);
if (remote_path_len >= sizeof(r->remote_path)) return -1;
memcpy(r->remote_path, colon + 1, remote_path_len + 1);
const char *at = memchr(dest, '@', colon - dest);
if (at) {
size_t user_len = at - dest;
if (user_len >= sizeof(r->user)) return -1;
memcpy(r->user, dest, user_len);
r->user[user_len] = '\0';
size_t host_len = colon - at - 1;
if (host_len >= sizeof(r->host)) return -1;
memcpy(r->host, at + 1, host_len);
r->host[host_len] = '\0';
} else {
r->user[0] = '\0';
size_t host_len = colon - dest;
if (host_len >= sizeof(r->host)) return -1;
memcpy(r->host, dest, host_len);
r->host[host_len] = '\0';
}
return 0;
}
Client *client_connect_ssh(char *destination) {
RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) {
fprintf(stderr, "Invalid remote destination: %s\n", destination);
exit(EXIT_FAILURE);
}
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
perror("socketpair failed");
exit(EXIT_FAILURE);
}
int exec_pipe[2];
if (pipe(exec_pipe) < 0) {
perror("pipe failed");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid == 0) {
close(sv[0]);
close(exec_pipe[0]);
fcntl(exec_pipe[1], F_SETFD, FD_CLOEXEC);
if (sv[1] != STDIN_FILENO)
dup2(sv[1], STDIN_FILENO);
if (sv[1] != STDOUT_FILENO)
dup2(sv[1], STDOUT_FILENO);
if (sv[1] > 1) close(sv[1]);
char ssh_user[512];
if (r.user[0] != '\0')
snprintf(ssh_user, sizeof(ssh_user), "%s@%s", r.user, r.host);
else
snprintf(ssh_user, sizeof(ssh_user), "%s", r.host);
execlp("ssh", "ssh", "-o", "Compression=no", "-o",
"ControlMaster=no", ssh_user, "fastsync-server", "--stdio",
(char *)NULL);
perror("exec of ssh failed");
(void)write(exec_pipe[1], "x", 1);
_exit(1);
}
close(sv[1]);
close(exec_pipe[1]);
char exec_status;
ssize_t n = read(exec_pipe[0], &exec_status, 1);
close(exec_pipe[0]);
if (n > 0) {
close(sv[0]);
waitpid(pid, NULL, 0);
fprintf(stderr, "Error: could not launch 'fastsync-server --stdio' on remote\n");
exit(EXIT_FAILURE);
}
Client *client = malloc(sizeof(Client));
client->file_descriptor = sv[0];
client->address.sin_family = AF_UNIX;
client->address_length = 0;
client->ssh_child_pid = pid;
return client;
}
Server *server_create(int port) {
Server *server = (Server *)malloc(sizeof(Server));
if (server == NULL) {
@@ -82,6 +204,7 @@ Client *client_create() {
client->file_descriptor = file_descriptor;
client->address.sin_family = AF_INET;
client->address_length = sizeof(client->address);
client->ssh_child_pid = -1;
return client;
}
@@ -100,7 +223,14 @@ void client_connect(Client *client, char *host, int port) {
}
}
void client_disconnect(Client *client) { close(client->file_descriptor); }
void client_disconnect(Client *client) {
close(client->file_descriptor);
if (client->ssh_child_pid > 0) {
int status;
waitpid(client->ssh_child_pid, &status, 0);
client->ssh_child_pid = -1;
}
}
void client_delete(Client *client) {
if (client == NULL)
@@ -110,12 +240,11 @@ void client_delete(Client *client) {
void send_n_data(int file_descriptor, void *data, size_t data_size) {
log_message(LOG_LEVEL_DEBUG, " Sending n Data: %zu", data_size);
int fd = io_fd(io_write_fd, file_descriptor);
ssize_t total_bytes_send = 0;
while (total_bytes_send < data_size) {
printf("Trying: %zu\n", data_size - total_bytes_send);
ssize_t bytes_send = send(file_descriptor, (char *)data + total_bytes_send,
data_size - total_bytes_send, 0);
printf("Bytes send: %zd\n", bytes_send);
ssize_t bytes_send = write(fd, (char *)data + total_bytes_send,
data_size - total_bytes_send);
if (bytes_send <= 0) {
perror("Could not send data!");
exit(EXIT_FAILURE);
@@ -127,11 +256,12 @@ void send_n_data(int file_descriptor, void *data, size_t data_size) {
void receive_n_data(int file_descriptor, void *data, size_t data_size) {
log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size);
int fd = io_fd(io_read_fd, file_descriptor);
size_t total_bytes_received = 0;
while (total_bytes_received < data_size) {
long long bytes_received =
recv(file_descriptor, data + total_bytes_received,
data_size - total_bytes_received, 0);
ssize_t bytes_received =
read(fd, (char *)data + total_bytes_received,
data_size - total_bytes_received);
if (bytes_received == -1 || bytes_received == 0) {
perror("Could not receive bytes!");
exit(EXIT_FAILURE);
+3
View File
@@ -21,13 +21,16 @@ typedef struct Client {
struct sockaddr_in address;
unsigned int address_length;
int file_descriptor;
pid_t ssh_child_pid;
} Client;
Client *client_create();
void client_disconnect(Client *client);
void client_delete(Client *client);
void client_connect(Client *client, char *host, int port);
Client *client_connect_ssh(char *destination);
void io_set_fds(int read_fd, int write_fd);
void send_n_data(int file_descriptor, void *data, size_t data_size);
void receive_n_data(int file_descriptor, void *data, size_t data_size);
void send_str(int file_descriptor, char *data);