feat(p5-socket): --address, -4/-6, --sockopts, server bind options
This commit is contained in:
@@ -110,6 +110,35 @@ class TestTCPFlags:
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
class TestTCPSocketOptions:
|
||||
"""--sockopts, -4/-6 and --address: rsync-compatible socket/bind options.
|
||||
|
||||
These are purely local (client-side) socket concerns that never cross the
|
||||
wire, so each is exercised by a normal transfer succeeding end-to-end."""
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_sockopts_apply(self, shared_server):
|
||||
r = _run_tcp_test("Sockopts (TCP_NODELAY=1,SO_KEEPALIVE=1)", shared_server.port,
|
||||
["--sockopts=TCP_NODELAY=1,SO_KEEPALIVE=1"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_sockopts_buffer_sizes(self, shared_server):
|
||||
r = _run_tcp_test("Sockopts buffer sizes (SO_RCVBUF/SO_SNDBUF)", shared_server.port,
|
||||
["--sockopts=SO_RCVBUF=131072,SO_SNDBUF=131072"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_ipv4_forced(self, shared_server):
|
||||
r = _run_tcp_test("Force IPv4 (-4)", shared_server.port, ["-4"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
@pytest.mark.skipif(shutil.which("ip") is None,
|
||||
reason="requires ip tooling to enumerate a usable local address")
|
||||
def test_address_source_bind(self, shared_server):
|
||||
r = _run_tcp_test("Source bind (--address=127.0.0.1)", shared_server.port,
|
||||
["--address", "127.0.0.1"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
class TestTCPChunkSize:
|
||||
def test_custom_chunk_size(self, shared_server):
|
||||
r = _run_tcp_test("Chunk size 5MB", shared_server.port, ["--chunk-size", "5242880"])
|
||||
|
||||
+87
-3
@@ -834,10 +834,7 @@ static void test_parse_args_rejects_unimplemented_options() {
|
||||
"-e",
|
||||
"--rsh",
|
||||
"--rsync-path",
|
||||
"--address",
|
||||
"--bind-address",
|
||||
"--ipv6",
|
||||
"--ipv4",
|
||||
"--daemon",
|
||||
"--config",
|
||||
"--server"};
|
||||
@@ -2480,6 +2477,90 @@ static void test_parse_args_devices_specials() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --address binds the outgoing client socket; it is a plain string option. */
|
||||
static void test_parse_args_address() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--address", "192.0.2.10", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->address, "192.0.2.10");
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* eq_argv[] = {"fastsync", "--address=10.0.0.5", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, eq_argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->address, "10.0.0.5");
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* -4/--ipv4 and -6/--ipv6 set the resolution family; both together are
|
||||
* rejected by validate_config (an address cannot be both v4 and v6). */
|
||||
static void test_parse_args_ipv4_ipv6() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-4", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->ipv4);
|
||||
EXPECT_FALSE(cfg->ipv6);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* longv6[] = {"fastsync", "--ipv6", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, longv6, positional_args, &positional_count), 0);
|
||||
EXPECT_FALSE(cfg->ipv4);
|
||||
EXPECT_TRUE(cfg->ipv6);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* both[] = {"fastsync", "-4", "-6", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, both, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->ipv4);
|
||||
EXPECT_TRUE(cfg->ipv6);
|
||||
cfg->send_directory = str_dup("/src");
|
||||
cfg->receive_root_directory = str_dup("/dst");
|
||||
EXPECT_FALSE(validate_config(cfg));
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --sockopts parses and stores the allowlist; unknown options and bad values
|
||||
* are rejected at the CLI layer (never silently ignored). */
|
||||
static void test_parse_args_sockopts() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--sockopts=TCP_NODELAY=1,SO_KEEPALIVE=1", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->sockopt_count, 2);
|
||||
EXPECT_EQ_INT(cfg->sockopts[0].id, SOCKOPT_TCP_NODELAY);
|
||||
EXPECT_EQ_INT(cfg->sockopts[0].value, 1);
|
||||
EXPECT_EQ_INT(cfg->sockopts[1].id, SOCKOPT_SO_KEEPALIVE);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* sep_argv[] = {"fastsync", "--sockopts", "SO_RCVBUF=65536", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, sep_argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->sockopt_count, 1);
|
||||
EXPECT_EQ_INT(cfg->sockopts[0].id, SOCKOPT_SO_RCVBUF);
|
||||
EXPECT_EQ_INT(cfg->sockopts[0].value, 65536);
|
||||
config_delete(cfg);
|
||||
|
||||
static const char* const bad[] = {"--sockopts=IP_TTL=1", "--sockopts=TCP_NODELAY=2",
|
||||
"--sockopts=SO_KEEPALIVE"};
|
||||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* b[] = {"fastsync", (char*)bad[i], "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, b, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
void test_client_cli() {
|
||||
test_validate_config_required_paths();
|
||||
test_parse_args_numeric_ids();
|
||||
@@ -2493,6 +2574,9 @@ void test_client_cli() {
|
||||
test_parse_args_devices_specials();
|
||||
test_parse_args_atimes_long_and_short();
|
||||
test_parse_args_omit_link_times_long();
|
||||
test_parse_args_address();
|
||||
test_parse_args_ipv4_ipv6();
|
||||
test_parse_args_sockopts();
|
||||
test_parse_args_append();
|
||||
test_parse_args_append_verify();
|
||||
test_parse_args_append_both();
|
||||
|
||||
+119
-1
@@ -2,14 +2,126 @@
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include "transport_tcp.h"
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
/* -4/-6 map to a getaddrinfo ai_family hint: -4 -> AF_INET, -6 -> AF_INET6,
|
||||
* and neither -> AF_UNSPEC. Both flags together are rejected earlier (in
|
||||
* validate_config), so this helper never needs to prefer one over the other. */
|
||||
static void test_tcp_connect_family_hints() {
|
||||
EXPECT_EQ_INT(tcp_connect_family(false, false), AF_UNSPEC);
|
||||
EXPECT_EQ_INT(tcp_connect_family(true, false), AF_INET);
|
||||
EXPECT_EQ_INT(tcp_connect_family(false, true), AF_INET6);
|
||||
}
|
||||
|
||||
/* --sockopts parsing+validation: every allowlisted KEY works, OPT=VAL values
|
||||
* are captured, and an unknown option or a bad value is rejected (never
|
||||
* silently ignored). */
|
||||
static void test_sockopts_parse_valid() {
|
||||
SockOptEntry* out = NULL;
|
||||
int count = 0;
|
||||
EXPECT_EQ_INT(config_sockopts_parse("TCP_NODELAY=1,SO_KEEPALIVE=0", &out, &count), 0);
|
||||
EXPECT_EQ_INT(count, 2);
|
||||
EXPECT_EQ_INT(out[0].id, SOCKOPT_TCP_NODELAY);
|
||||
EXPECT_EQ_INT(out[0].value, 1);
|
||||
EXPECT_EQ_INT(out[1].id, SOCKOPT_SO_KEEPALIVE);
|
||||
EXPECT_EQ_INT(out[1].value, 0);
|
||||
free(out);
|
||||
|
||||
out = NULL;
|
||||
count = 0;
|
||||
EXPECT_EQ_INT(config_sockopts_parse("SO_RCVBUF=65536,SO_SNDBUF=131072,SO_REUSEADDR=1", &out,
|
||||
&count),
|
||||
0);
|
||||
EXPECT_EQ_INT(count, 3);
|
||||
EXPECT_EQ_INT(out[0].id, SOCKOPT_SO_RCVBUF);
|
||||
EXPECT_EQ_INT(out[0].value, 65536);
|
||||
EXPECT_EQ_INT(out[1].id, SOCKOPT_SO_SNDBUF);
|
||||
EXPECT_EQ_INT(out[1].value, 131072);
|
||||
EXPECT_EQ_INT(out[2].id, SOCKOPT_SO_REUSEADDR);
|
||||
EXPECT_EQ_INT(out[2].value, 1);
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void test_sockopts_parse_rejects() {
|
||||
static const char* const bad[] = {
|
||||
"IP_TTL=1", /* unknown option name */
|
||||
"SO_KEEPALIVE", /* missing '=' */
|
||||
"=1", /* missing option name */
|
||||
"TCP_NODELAY=", /* missing value */
|
||||
"TCP_NODELAY=2", /* boolean must be 0/1 */
|
||||
"TCP_NODELAY=on", /* non-numeric boolean */
|
||||
"SO_RCVBUF=-1", /* negative buffer */
|
||||
"SO_SNDBUF=abc", /* non-numeric buffer */
|
||||
""}; /* empty spec */
|
||||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||
SockOptEntry* out = NULL;
|
||||
int count = 0;
|
||||
EXPECT_EQ_INT(config_sockopts_parse(bad[i], &out, &count), -1);
|
||||
EXPECT_NULL(out);
|
||||
}
|
||||
}
|
||||
|
||||
/* Applying a validated allowlist entry must actually set the socket option (a
|
||||
* real setsockopt on a fresh TCP socket) so the config->wire path is proven. */
|
||||
static void test_sockopts_apply_sets_option() {
|
||||
SockOptEntry* entries = NULL;
|
||||
int count = 0;
|
||||
EXPECT_EQ_INT(config_sockopts_parse("TCP_NODELAY=1,SO_REUSEADDR=1", &entries, &count), 0);
|
||||
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
EXPECT_TRUE(fd >= 0);
|
||||
if (fd >= 0) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int value = entries[i].value;
|
||||
int level = entries[i].id == SOCKOPT_TCP_NODELAY ? IPPROTO_TCP : SOL_SOCKET;
|
||||
int name = entries[i].id == SOCKOPT_TCP_NODELAY ? TCP_NODELAY : SO_REUSEADDR;
|
||||
EXPECT_EQ_INT(setsockopt(fd, level, name, &value, sizeof(value)), 0);
|
||||
}
|
||||
int got = 0;
|
||||
socklen_t len = sizeof(got);
|
||||
EXPECT_EQ_INT(getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &got, &len), 0);
|
||||
EXPECT_EQ_INT(got, 1);
|
||||
close(fd);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
|
||||
/* server_create_ex with an explicit --address and family binds to that local
|
||||
* address; the resulting socket's address family must match. */
|
||||
static void test_server_create_bind_address() {
|
||||
ServerBindOptions opts;
|
||||
opts.bind_address = "127.0.0.1";
|
||||
opts.family = AF_INET;
|
||||
Server* s = server_create_ex(0, &opts);
|
||||
EXPECT_NOT_NULL(s);
|
||||
if (s) {
|
||||
EXPECT_EQ_INT(s->address.ss_family, AF_INET);
|
||||
server_delete(&s);
|
||||
}
|
||||
}
|
||||
|
||||
/* An IPv6 bind is honored when the host supports it; on a host with no IPv6 a
|
||||
* NULL return is acceptable (the feature degrades to unavailable, not wrong). */
|
||||
static void test_server_create_bind_ipv6() {
|
||||
ServerBindOptions opts;
|
||||
opts.bind_address = "::1";
|
||||
opts.family = AF_INET6;
|
||||
Server* s = server_create_ex(0, &opts);
|
||||
if (s) {
|
||||
EXPECT_EQ_INT(s->address.ss_family, AF_INET6);
|
||||
server_delete(&s);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_server_create_ephemeral() {
|
||||
Server* s = server_create(0);
|
||||
EXPECT_NOT_NULL(s);
|
||||
EXPECT_TRUE(s->file_descriptor >= 0);
|
||||
EXPECT_EQ_INT(s->address.sin_family, AF_INET);
|
||||
EXPECT_EQ_INT(s->address.ss_family, AF_INET);
|
||||
server_delete(&s);
|
||||
EXPECT_NULL(s);
|
||||
}
|
||||
@@ -99,4 +211,10 @@ void test_transport_tcp() {
|
||||
test_server_create_specific_port();
|
||||
test_server_delete_double();
|
||||
test_client_disconnect_delete();
|
||||
test_tcp_connect_family_hints();
|
||||
test_sockopts_parse_valid();
|
||||
test_sockopts_parse_rejects();
|
||||
test_sockopts_apply_sets_option();
|
||||
test_server_create_bind_address();
|
||||
test_server_create_bind_ipv6();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user