fix(a7-3/s1): require TLS or local transport for daemon auth
Daemon modules that declare 'auth users' no longer accept credentials over a remote plaintext connection: server_module_gate refuses at the config gate, before any SCRAM challenge is sent, unless the connection is verified TLS with a client certificate matching --client-cn, or a local/SSH transport (loopback TCP peer or the --stdio pipe). --allow-unauthenticated does not relax this. The TLS client-CN comparison now uses credentials_secure_equal (S2). Clients sending --password-file to a non-loopback daemon must use --tls; validate_config rejects the plaintext case before any network I/O. Adds utils_sockaddr_is_loopback / utils_fd_peer_is_local / utils_host_is_loopback helpers with unit tests, a client validation unit test, and integration tests for the client-side plaintext rejection and the wrong-CN gate refusal.
This commit is contained in:
@@ -528,6 +528,16 @@ restart-gated enumeration channel remains (persisting a dummy key is out of
|
|||||||
scope); and the store iteration count is observable pre-auth by design, since
|
scope); and the store iteration count is observable pre-auth by design, since
|
||||||
the miss path must match a hit.
|
the miss path must match a hit.
|
||||||
|
|
||||||
|
An `auth users` module only accepts credentials over an encrypted, verified TLS
|
||||||
|
connection whose client certificate matches the server's `--client-cn`, or over
|
||||||
|
a local/SSH transport (a loopback TCP peer or the `--stdio` pipe). A remote
|
||||||
|
plaintext peer is refused before any challenge is sent, and
|
||||||
|
`--allow-unauthenticated` does **not** relax this: that flag only relaxes the
|
||||||
|
standalone plaintext gate. Clients sending daemon credentials with
|
||||||
|
`--password-file` to a non-loopback daemon must therefore use `--tls`; the
|
||||||
|
client rejects a non-local plaintext credential destination before any network
|
||||||
|
I/O.
|
||||||
|
|
||||||
TLS provides encrypted TCP transport. Supplying `--ca` enables certificate
|
TLS provides encrypted TCP transport. Supplying `--ca` enables certificate
|
||||||
verification; without it, traffic is encrypted but peer identity is not
|
verification; without it, traffic is encrypted but peer identity is not
|
||||||
verified. Use certificate verification for deployments where authentication
|
verified. Use certificate verification for deployments where authentication
|
||||||
|
|||||||
+2
-2
@@ -639,9 +639,9 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
|||||||
- **Module selection & confinement:** the client requests a module with an rsync-style `host::module[/path]` destination. The module name crosses the wire as a trailing string on the config frame (bumping `PROTOCOL_VERSION` 2.14.0 → 2.15.0; the bump is required because the config-frame layout changed and the strict same-version handshake is what prevents a peer from desynchronizing on the new trailing field). The daemon looks the module up in ITS OWN config and uses the module's `path` as the authorized root through the exact same `configure_authorization` confinement the standalone server applies to `--destination-root` (`file_open_secure_parent`, `has_path_traversal`, `path_is_within`); the client never supplies the root, every client-chosen-ownership/super-user request is refused unless the module declares `client owner = yes` (the daemon's per-module opt-in, see below), and the operator `--no-super` veto forces super-user activities off for every daemon connection. The client's `/path` part is relative inside the module and is rejected if absolute or if it contains `..`. Unknown modules are refused before any data moves (the run fails cleanly at the config handshake). An absolute destination and a module request against a non-daemon server are also refused.
|
- **Module selection & confinement:** the client requests a module with an rsync-style `host::module[/path]` destination. The module name crosses the wire as a trailing string on the config frame (bumping `PROTOCOL_VERSION` 2.14.0 → 2.15.0; the bump is required because the config-frame layout changed and the strict same-version handshake is what prevents a peer from desynchronizing on the new trailing field). The daemon looks the module up in ITS OWN config and uses the module's `path` as the authorized root through the exact same `configure_authorization` confinement the standalone server applies to `--destination-root` (`file_open_secure_parent`, `has_path_traversal`, `path_is_within`); the client never supplies the root, every client-chosen-ownership/super-user request is refused unless the module declares `client owner = yes` (the daemon's per-module opt-in, see below), and the operator `--no-super` veto forces super-user activities off for every daemon connection. The client's `/path` part is relative inside the module and is rejected if absolute or if it contains `..`. Unknown modules are refused before any data moves (the run fails cleanly at the config handshake). An absolute destination and a module request against a non-daemon server are also refused.
|
||||||
- **`client owner` (client-chosen-ownership opt-in):** by default a daemon module refuses every request that would let the client pick an owner or ask for super-user activities — `--numeric-ids`, `--chown`, `--usermap`/`--groupmap`, `--fake-super`, `--copy-as`, and an explicit `--super` — at the config handshake (before `STATUS_OK`), because a daemon has no per-module opt-in for client-chosen ownership and any anonymous client could otherwise force arbitrary owner ids inside the module root. `client owner = yes` opts a single module in, allowing those requests within that module's root (the standalone listener and the SSH `--stdio` server always honor them for their single operator-authorized root). Without the opt-in the daemon also forces super-user **device** activity off for that connection — char/block device-node creation (`--devices`) and `--write-devices` — even under the default `AUTO` mode, so a non-opted module can never be made to `mknod` or write a raw device; those entries are skipped (not refused) so an ordinary `-a` push still succeeds without device nodes. The opt-in does **not** lift the privilege requirement: `--copy-as` still needs a root receiver, and the operator `--no-super` veto still forces super-user activities off for every connection. The daemon logs a prominent startup warning for each `client owner = yes` module so the operator's deliberate choice is visible.
|
- **`client owner` (client-chosen-ownership opt-in):** by default a daemon module refuses every request that would let the client pick an owner or ask for super-user activities — `--numeric-ids`, `--chown`, `--usermap`/`--groupmap`, `--fake-super`, `--copy-as`, and an explicit `--super` — at the config handshake (before `STATUS_OK`), because a daemon has no per-module opt-in for client-chosen ownership and any anonymous client could otherwise force arbitrary owner ids inside the module root. `client owner = yes` opts a single module in, allowing those requests within that module's root (the standalone listener and the SSH `--stdio` server always honor them for their single operator-authorized root). Without the opt-in the daemon also forces super-user **device** activity off for that connection — char/block device-node creation (`--devices`) and `--write-devices` — even under the default `AUTO` mode, so a non-opted module can never be made to `mknod` or write a raw device; those entries are skipped (not refused) so an ordinary `-a` push still succeeds without device nodes. The opt-in does **not** lift the privilege requirement: `--copy-as` still needs a root receiver, and the operator `--no-super` veto still forces super-user activities off for every connection. The daemon logs a prominent startup warning for each `client owner = yes` module so the operator's deliberate choice is visible.
|
||||||
- **`read only` safe default:** every network transfer FastSync currently supports is a push that writes under the module root, so a `read only` module refuses the connection (clear server log "module is read only"; the client exits non-zero, nothing is transferred). A future pull/list operation can be opened up when it exists; the knob is already stored.
|
- **`read only` safe default:** every network transfer FastSync currently supports is a push that writes under the module root, so a `read only` module refuses the connection (clear server log "module is read only"; the client exits non-zero, nothing is transferred). A future pull/list operation can be opened up when it exists; the knob is already stored.
|
||||||
- **`auth users` (A7 SCRAM-SHA-256 authentication):** a module that declares `auth users` requires the client to present credentials. The config frame carries ONLY the username; the daemon answers an auth-required module with `STATUS_AUTH_CHALLENGE` (PBKDF2 iteration count, 16-byte salt, 32-byte server nonce), the client answers with `STATUS_AUTH_RESPONSE` (fresh 32-byte client nonce + a 32-byte ClientProof), and the daemon accepts only when the proof verifies **and** the username is **on the module's `auth users` list** and has a store entry, replying `STATUS_AUTH_OK` with a 32-byte ServerSignature the client verifies before proceeding. Verification is constant-time over fixed 32-byte keys (the compare runs even for a miss), username membership uses a constant-time full-length scan, and an unknown/off-list user still receives a challenge and runs the same math against a dummy verifier: a deterministic per-username salt (`HMAC-SHA256(store dummy key, username)`), the store-wide uniform iteration count and dummy keys. Re-probing the same unknown username therefore yields an identical salt and iteration count while a different username yields a different salt, so there is no user-enumeration or timing oracle. The daemon logs the username but **never the password, proof or keys**. A module WITHOUT `auth users` stays open (legitimate rsync configuration); credentials sent to such a module are ignored. Read-only is orthogonal: even a correctly authenticated push to a `read only` module is still refused (all FastSync network transfers write). Fail-closed policy: a daemon whose config declares `auth users` on any module refuses to start unless a credential store was given (`--password-file` and/or `--early-input`); a missing or empty store is never silently treated as "open". A failed handshake (missing credentials, unknown/off-list user, wrong proof or malformed data) yields a single generic `STATUS_AUTH_FAILED` and the daemon closes before any data moves. Two residuals are accepted: the dummy salt is stable within one daemon lifetime but changes across restarts, leaving a restart-gated enumeration channel (persisting the dummy key is out of scope); and the store iteration count is observable pre-auth by design, since the miss path must match a hit.
|
- **`auth users` (A7 SCRAM-SHA-256 authentication):** a module that declares `auth users` requires the client to present credentials. The config frame carries ONLY the username; the daemon answers an auth-required module with `STATUS_AUTH_CHALLENGE` (PBKDF2 iteration count, 16-byte salt, 32-byte server nonce), the client answers with `STATUS_AUTH_RESPONSE` (fresh 32-byte client nonce + a 32-byte ClientProof), and the daemon accepts only when the proof verifies **and** the username is **on the module's `auth users` list** and has a store entry, replying `STATUS_AUTH_OK` with a 32-byte ServerSignature the client verifies before proceeding. Verification is constant-time over fixed 32-byte keys (the compare runs even for a miss), username membership uses a constant-time full-length scan, and an unknown/off-list user still receives a challenge and runs the same math against a dummy verifier: a deterministic per-username salt (`HMAC-SHA256(store dummy key, username)`), the store-wide uniform iteration count and dummy keys. Re-probing the same unknown username therefore yields an identical salt and iteration count while a different username yields a different salt, so there is no user-enumeration or timing oracle. The daemon logs the username but **never the password, proof or keys**. A module WITHOUT `auth users` stays open (legitimate rsync configuration); credentials sent to such a module are ignored. Read-only is orthogonal: even a correctly authenticated push to a `read only` module is still refused (all FastSync network transfers write). Fail-closed policy: a daemon whose config declares `auth users` on any module refuses to start unless a credential store was given (`--password-file` and/or `--early-input`); a missing or empty store is never silently treated as "open". A failed handshake (missing credentials, unknown/off-list user, wrong proof or malformed data) yields a single generic `STATUS_AUTH_FAILED` and the daemon closes before any data moves. Two residuals are accepted: the dummy salt is stable within one daemon lifetime but changes across restarts, leaving a restart-gated enumeration channel (persisting the dummy key is out of scope); and the store iteration count is observable pre-auth by design, since the miss path must match a hit. **Transport policy (hardening A7-3/S1):** an auth-required module accepts credentials only over an encrypted, verified TLS connection whose client certificate matches `--client-cn`, or over a local/SSH transport (a loopback TCP peer, or the `--stdio` pipe); a remote plaintext peer is refused at the config gate before any challenge is sent, and `--allow-unauthenticated` does **not** relax this.
|
||||||
- **Credential store format:** server `--password-file`/`--early-input` files are line-based `user:$fastsync$1$pbkdf2-sha256$<iters>$<salt_b64>$<stored_key_b64>$<server_key_b64>`, one per line (standard base64; 16-byte salt, 32-byte keys; `iters` in `[100000, 10000000]`, default 600000). Every entry in the resulting store must agree on `iters` (a store whose entries disagree, or where a layered `--early-input` disagrees with `--password-file`, is rejected). Generate lines with `fastsync-server --hash-credentials FILE [--iterations N]`; the emitted lines are secret material, so redirect them to an owner-only (mode 0600) file (the tool warns on stderr if stdout is a group/other-accessible regular file). Blank lines and lines starting with `#`/`;` are comments; the parser is strict (a malformed line fails the whole load, so a typo can never let a different set of users in). **The legacy `user:SHA256HEX` form is hard-rejected** with an actionable "legacy" error; there is no auto-upgrade, so a replayable bearer digest can never be loaded by a 2.19.0 daemon. The client `--password-file` holds `user:password` on its first meaningful line (the literal password, used only for the handshake then burned); keep both files readable only by their owner (mode 0600). Per-username wire length is bounded (256 chars) and every decoded salt/key length is validated.
|
- **Credential store format:** server `--password-file`/`--early-input` files are line-based `user:$fastsync$1$pbkdf2-sha256$<iters>$<salt_b64>$<stored_key_b64>$<server_key_b64>`, one per line (standard base64; 16-byte salt, 32-byte keys; `iters` in `[100000, 10000000]`, default 600000). Every entry in the resulting store must agree on `iters` (a store whose entries disagree, or where a layered `--early-input` disagrees with `--password-file`, is rejected). Generate lines with `fastsync-server --hash-credentials FILE [--iterations N]`; the emitted lines are secret material, so redirect them to an owner-only (mode 0600) file (the tool warns on stderr if stdout is a group/other-accessible regular file). Blank lines and lines starting with `#`/`;` are comments; the parser is strict (a malformed line fails the whole load, so a typo can never let a different set of users in). **The legacy `user:SHA256HEX` form is hard-rejected** with an actionable "legacy" error; there is no auto-upgrade, so a replayable bearer digest can never be loaded by a 2.19.0 daemon. The client `--password-file` holds `user:password` on its first meaningful line (the literal password, used only for the handshake then burned); keep both files readable only by their owner (mode 0600). Per-username wire length is bounded (256 chars) and every decoded salt/key length is validated.
|
||||||
- **Plaintext caveat:** over a plaintext (non-TLS) daemon a sniffer can read the challenge and response and mount an **offline dictionary attack** against a weak password, so use `--tls` for any real deployment. Unlike the old challenge-less exchange there is **no replay**: the proof is bound to the fresh per-connection server nonce, so a captured `STATUS_AUTH_RESPONSE` cannot be reused on another connection (an integration test proxies the daemon and proves this). The daemon logs a warning when an auth-required module is reached over plaintext. TLS client-CN (`--client-cn`) is an independent transport identity check and composes with password auth: both may be required on the same connection.
|
- **Plaintext caveat:** an auth-required module is refused, **before any challenge is sent**, unless the connection is encrypted and verified TLS whose client certificate matches the server's `--client-cn`, or a local/SSH transport (a loopback TCP peer, or the `--stdio` pipe). A remote plaintext peer never receives a challenge, and `--allow-unauthenticated` does **not** relax this policy (that flag only relaxes the standalone plaintext gate). On the loopback/SSH transports that remain permitted, a local sniffer could still read the challenge and response and mount an **offline dictionary attack** against a weak password, so use `--tls` for any real deployment. Clients sending daemon credentials with `--password-file` to a non-loopback daemon must use `--tls`; the client rejects such a destination before any network I/O. Unlike the old challenge-less exchange there is **no replay**: the proof is bound to the fresh per-connection server nonce, so a captured `STATUS_AUTH_RESPONSE` cannot be reused on another connection (an integration test proxies the daemon and proves this). TLS client-CN (`--client-cn`) is an independent transport identity check and composes with password auth: both may be required on the same connection.
|
||||||
- **Wire/protocol:** the config-frame auth block is now `[int present][str_redacted username]` (the old digest field is gone), and the frame stream gains the challenge/response (`STATUS_AUTH_CHALLENGE` → `STATUS_AUTH_RESPONSE` → `STATUS_AUTH_OK`/`STATUS_AUTH_FAILED`) between the config frame and the `STATUS_OK` ack. Both are wire-layout changes, so `PROTOCOL_VERSION` is bumped **2.18.0 → 2.19.0** (see the A7 note in `src/shared/config.h`); the strict same-version handshake keeps a 2.19 client and a 2.18 server from desynchronizing.
|
- **Wire/protocol:** the config-frame auth block is now `[int present][str_redacted username]` (the old digest field is gone), and the frame stream gains the challenge/response (`STATUS_AUTH_CHALLENGE` → `STATUS_AUTH_RESPONSE` → `STATUS_AUTH_OK`/`STATUS_AUTH_FAILED`) between the config frame and the `STATUS_OK` ack. Both are wire-layout changes, so `PROTOCOL_VERSION` is bumped **2.18.0 → 2.19.0** (see the A7 note in `src/shared/config.h`); the strict same-version handshake keeps a 2.19 client and a 2.18 server from desynchronizing.
|
||||||
- **Client side:** `host::module/path` selects the TCP transport and connects to `--server-port`; `host:path` stays the SSH transport; plain paths stay local TCP. The daemon username comes from `--password-file` (first `user:password` line), and `--password-file` without a `host::module/path` destination is a client error (fail fast). A `user@host::module` form is rejected with a pointer to `--password-file`. The client's plaintext password is wiped from memory (`config_burn_auth`) at transfer teardown.
|
- **Client side:** `host::module/path` selects the TCP transport and connects to `--server-port`; `host:path` stays the SSH transport; plain paths stay local TCP. The daemon username comes from `--password-file` (first `user:password` line), and `--password-file` without a `host::module/path` destination is a client error (fail fast). A `user@host::module` form is rejected with a pointer to `--password-file`. The client's plaintext password is wiped from memory (`config_burn_auth`) at transfer teardown.
|
||||||
- **MOTD (Wave C):** a daemon configured with a global `motd file` sends that file's content as the first server→client string frame after the config-frame STATUS_OK ack (rsync sends the MOTD as the first thing from the server at the start of a daemon connection). Only the daemon listener path (`host::module`) gets a MOTD; the `--stdio` SSH path never sends or reads one. The server reads the file bounded to 4096 bytes and treats an absent/unreadable file as "no MOTD" (an empty frame, never an error). The exchange is server→client only and does **not** bump `PROTOCOL_VERSION`: every 2.15.0 daemon client reads the frame after the ack, so sender and receiver stay in lockstep (see the Wave C note in `src/shared/config.h`). `--no-motd` is the client-side suppression switch: the client still reads (consumes) the frame to keep the stream in sync but does not display it. The MOTD is printed to stdout with control bytes (ESC included) escaped octal-style while newlines/tabs are preserved, so a hostile server cannot inject terminal escape sequences.
|
- **MOTD (Wave C):** a daemon configured with a global `motd file` sends that file's content as the first server→client string frame after the config-frame STATUS_OK ack (rsync sends the MOTD as the first thing from the server at the start of a daemon connection). Only the daemon listener path (`host::module`) gets a MOTD; the `--stdio` SSH path never sends or reads one. The server reads the file bounded to 4096 bytes and treats an absent/unreadable file as "no MOTD" (an empty frame, never an error). The exchange is server→client only and does **not** bump `PROTOCOL_VERSION`: every 2.15.0 daemon client reads the frame after the ack, so sender and receiver stay in lockstep (see the Wave C note in `src/shared/config.h`). `--no-motd` is the client-side suppression switch: the client still reads (consumes) the frame to keep the stream in sync but does not display it. The MOTD is printed to stdout with control bytes (ESC included) escaped octal-style while newlines/tabs are preserved, so a hostile server cannot inject terminal escape sequences.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "delay_updates.h"
|
#include "delay_updates.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "usage.h"
|
#include "usage.h"
|
||||||
|
#include "utils.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
@@ -136,6 +137,15 @@ bool validate_config(const Config* config) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Daemon credentials (A7, protocol 2.19.0): a --password-file would send the
|
||||||
|
username in the clear and derive a SCRAM proof a network sniffer could
|
||||||
|
attack offline, so it is only allowed over TLS (which itself mandates a
|
||||||
|
verified --cert/--key/--ca set above) or to a loopback destination. A
|
||||||
|
remote plaintext daemon is refused here, before any network I/O. */
|
||||||
|
if (config->password_file && !config->use_tls && !utils_host_is_loopback(config->server_host)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "sending daemon credentials to a non-local server requires --tls");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (config->delay_updates && config->inplace) {
|
if (config->delay_updates && config->inplace) {
|
||||||
log_message(LOG_LEVEL_ERROR, "--delay-updates does not work with --inplace");
|
log_message(LOG_LEVEL_ERROR, "--delay-updates does not work with --inplace");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+17
-5
@@ -173,7 +173,7 @@ static bool tls_client_identity_allowed(SSL* ssl) {
|
|||||||
size_t required_length = strlen(required_client_cn);
|
size_t required_length = strlen(required_client_cn);
|
||||||
bool allowed = length >= 0 && (size_t)length == required_length &&
|
bool allowed = length >= 0 && (size_t)length == required_length &&
|
||||||
required_length < sizeof(common_name) &&
|
required_length < sizeof(common_name) &&
|
||||||
memcmp(common_name, required_client_cn, required_length) == 0;
|
credentials_secure_equal(common_name, required_client_cn, required_length);
|
||||||
X509_free(certificate);
|
X509_free(certificate);
|
||||||
return allowed;
|
return allowed;
|
||||||
}
|
}
|
||||||
@@ -382,11 +382,23 @@ static const char* server_module_gate(const Config* config, void* context) {
|
|||||||
return "requested daemon module requires authentication and no credential "
|
return "requested daemon module requires authentication and no credential "
|
||||||
"store is configured";
|
"store is configured";
|
||||||
}
|
}
|
||||||
if (gate_ctx && !gate_ctx->ssl) {
|
/* Transport policy (A7-3/S1): an auth-required module only accepts
|
||||||
log_message(LOG_LEVEL_WARNING,
|
* credentials over an encrypted, verified TLS connection whose client
|
||||||
"daemon module '%s' is authenticating over a plaintext connection (no --tls); "
|
* certificate matches --client-cn, or over a local/SSH transport (a
|
||||||
"the credential exchange is not encrypted",
|
* loopback TCP peer, or the --stdio pipe). A remote plaintext peer is
|
||||||
|
* refused HERE, before the challenge is sent, so an unverified client never
|
||||||
|
* receives a nonce. --allow-unauthenticated is intentionally NOT consulted:
|
||||||
|
* that flag relaxes the standalone plaintext gate, never this one. */
|
||||||
|
bool tls_ok = gate_ctx && gate_ctx->ssl && SSL_get_verify_result(gate_ctx->ssl) == X509_V_OK &&
|
||||||
|
tls_client_identity_allowed(gate_ctx->ssl);
|
||||||
|
bool local_ok = gate_ctx && gate_ctx->fd >= 0 && utils_fd_peer_is_local(gate_ctx->fd);
|
||||||
|
if (!tls_ok && !local_ok) {
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"daemon module '%s' requires authentication over an encrypted, verified TLS "
|
||||||
|
"connection (or a local/SSH transport); refusing",
|
||||||
config->module);
|
config->module);
|
||||||
|
return "daemon module requires authentication over an encrypted, verified TLS "
|
||||||
|
"connection";
|
||||||
}
|
}
|
||||||
if (!gate_ctx || gate_ctx->fd < 0) {
|
if (!gate_ctx || gate_ctx->fd < 0) {
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available",
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available",
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
#include <arpa/inet.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
@@ -543,3 +546,67 @@ bool append_tail_length(unsigned long long old_size, unsigned long long check_si
|
|||||||
*tail_out = check_size - old_size;
|
*tail_out = check_size - old_size;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* True when a bound/peer socket address is on the loopback interface: any
|
||||||
|
127.0.0.0/8 IPv4 address, IPv6 ::1, or an IPv4-mapped ::ffff:127.x.x.x. This
|
||||||
|
is the transport-local test the daemon auth gate uses to decide whether a
|
||||||
|
plaintext connection is a trustworthy local/SSH channel. */
|
||||||
|
bool utils_sockaddr_is_loopback(const struct sockaddr* addr) {
|
||||||
|
if (!addr)
|
||||||
|
return false;
|
||||||
|
if (addr->sa_family == AF_INET) {
|
||||||
|
const struct sockaddr_in* v4 = (const struct sockaddr_in*)addr;
|
||||||
|
uint32_t host = ntohl(v4->sin_addr.s_addr);
|
||||||
|
return (host & 0xff000000u) == 0x7f000000u;
|
||||||
|
}
|
||||||
|
if (addr->sa_family == AF_INET6) {
|
||||||
|
const struct sockaddr_in6* v6 = (const struct sockaddr_in6*)addr;
|
||||||
|
if (IN6_IS_ADDR_LOOPBACK(&v6->sin6_addr))
|
||||||
|
return true;
|
||||||
|
/* An IPv4-mapped ::ffff:127.x.x.x is loopback too. */
|
||||||
|
if (IN6_IS_ADDR_V4MAPPED(&v6->sin6_addr) && v6->sin6_addr.s6_addr[12] == 127)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* True when the fd's peer is a local channel: a loopback TCP peer, or a
|
||||||
|
non-socket descriptor (the --stdio SSH transport is a pipe, so a failed
|
||||||
|
getpeername with ENOTSOCK counts as local). Any other socket peer is not
|
||||||
|
local. */
|
||||||
|
bool utils_fd_peer_is_local(int fd) {
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
struct sockaddr_storage peer;
|
||||||
|
socklen_t length = sizeof(peer);
|
||||||
|
if (getpeername(fd, (struct sockaddr*)&peer, &length) != 0)
|
||||||
|
return errno == ENOTSOCK;
|
||||||
|
return utils_sockaddr_is_loopback((const struct sockaddr*)&peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* True when a client-supplied host string names a loopback destination:
|
||||||
|
"localhost", any 127.0.0.0/8 literal, "::1", or "[::1]". */
|
||||||
|
bool utils_host_is_loopback(const char* host) {
|
||||||
|
if (!host || host[0] == '\0')
|
||||||
|
return false;
|
||||||
|
if (strcmp(host, "localhost") == 0)
|
||||||
|
return true;
|
||||||
|
struct in_addr v4;
|
||||||
|
if (inet_pton(AF_INET, host, &v4) == 1)
|
||||||
|
return (ntohl(v4.s_addr) & 0xff000000u) == 0x7f000000u;
|
||||||
|
struct in6_addr addr6;
|
||||||
|
if (host[0] == '[') {
|
||||||
|
size_t len = strlen(host);
|
||||||
|
if (len < 3 || host[len - 1] != ']')
|
||||||
|
return false;
|
||||||
|
/* inet_pton needs the bare address, not the bracketed form. */
|
||||||
|
char bare[INET6_ADDRSTRLEN];
|
||||||
|
if (len - 2 >= sizeof(bare))
|
||||||
|
return false;
|
||||||
|
memcpy(bare, host + 1, len - 2);
|
||||||
|
bare[len - 2] = '\0';
|
||||||
|
return inet_pton(AF_INET6, bare, &addr6) == 1 && IN6_IS_ADDR_LOOPBACK(&addr6);
|
||||||
|
}
|
||||||
|
return inet_pton(AF_INET6, host, &addr6) == 1 && IN6_IS_ADDR_LOOPBACK(&addr6);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
|
||||||
char* str_dup(const char* string);
|
char* str_dup(const char* string);
|
||||||
char* output_escape(const char* string, bool eight_bit_output);
|
char* output_escape(const char* string, bool eight_bit_output);
|
||||||
@@ -66,5 +67,10 @@ bool format_human_bytes(unsigned long long bytes, char* buffer, size_t buffer_si
|
|||||||
bool append_resume_eligible(unsigned long long old_size, unsigned long long check_size);
|
bool append_resume_eligible(unsigned long long old_size, unsigned long long check_size);
|
||||||
bool append_tail_length(unsigned long long old_size, unsigned long long check_size,
|
bool append_tail_length(unsigned long long old_size, unsigned long long check_size,
|
||||||
unsigned long long* tail_out);
|
unsigned long long* tail_out);
|
||||||
|
/* Loopback / local-transport classification for the daemon auth gate and the
|
||||||
|
client credential rule. See utils.c for the exact accepted forms. */
|
||||||
|
bool utils_sockaddr_is_loopback(const struct sockaddr* addr);
|
||||||
|
bool utils_fd_peer_is_local(int fd);
|
||||||
|
bool utils_host_is_loopback(const char* host);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -766,6 +766,26 @@ class TestDaemonAuthentication:
|
|||||||
finally:
|
finally:
|
||||||
os.unlink(cred_path)
|
os.unlink(cred_path)
|
||||||
|
|
||||||
|
@pytest.mark.ci
|
||||||
|
def test_remote_plaintext_credentials_rejected_client_side(self):
|
||||||
|
"""A7-3/S1: sending daemon credentials to a clearly non-local daemon
|
||||||
|
WITHOUT --tls is refused by the client itself, before any network I/O
|
||||||
|
(192.0.2.0/24 is TEST-NET-1 and never reachable, so a network attempt
|
||||||
|
would time out instead of failing fast)."""
|
||||||
|
cred_path = os.path.join(TEST_DATA_DIR, "client_remote.pw")
|
||||||
|
_write_client_password_file(cred_path, "alice", ALICE_PASS)
|
||||||
|
try:
|
||||||
|
cmd = CLIENT_CMD + ["--source-dir", SOURCE_DIR,
|
||||||
|
"--dest-dir", "192.0.2.1::files",
|
||||||
|
"--save-to-disk", "--password-file", cred_path,
|
||||||
|
"--server-port", "873"]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = (result.stderr or "") + (result.stdout or "")
|
||||||
|
assert "--tls" in combined, combined
|
||||||
|
finally:
|
||||||
|
os.unlink(cred_path)
|
||||||
|
|
||||||
def test_client_empty_password_file_rejected(self):
|
def test_client_empty_password_file_rejected(self):
|
||||||
"""Client-side: an empty --password-file is rejected (no credentials)."""
|
"""Client-side: an empty --password-file is rejected (no credentials)."""
|
||||||
cred_path = os.path.join(TEST_DATA_DIR, "client_empty.pw")
|
cred_path = os.path.join(TEST_DATA_DIR, "client_empty.pw")
|
||||||
@@ -1001,23 +1021,30 @@ class TestDaemonMotd:
|
|||||||
d.stop()
|
d.stop()
|
||||||
|
|
||||||
|
|
||||||
def _generate_tls_certs(cert_dir):
|
def _generate_tls_certs(cert_dir, extra_san_ips=None):
|
||||||
"""Generate a self-signed CA, server cert (with 127.0.0.1 SAN) and a client
|
"""Generate a self-signed CA, server cert (with 127.0.0.1 SAN plus any
|
||||||
cert signed by that CA, for the TLS+auth composition test."""
|
extra_san_ips) and two client certs signed by that CA: one with the
|
||||||
|
expected CN (fastsync-client) and one with a WRONG CN, for the TLS+auth
|
||||||
|
composition and wrong-identity tests."""
|
||||||
os.makedirs(cert_dir, exist_ok=True)
|
os.makedirs(cert_dir, exist_ok=True)
|
||||||
ca_key, ca_cert = os.path.join(cert_dir, "ca.key"), os.path.join(cert_dir, "ca.pem")
|
ca_key, ca_cert = os.path.join(cert_dir, "ca.key"), os.path.join(cert_dir, "ca.pem")
|
||||||
server_key = os.path.join(cert_dir, "server.key")
|
server_key = os.path.join(cert_dir, "server.key")
|
||||||
server_cert = os.path.join(cert_dir, "server.pem")
|
server_cert = os.path.join(cert_dir, "server.pem")
|
||||||
client_key = os.path.join(cert_dir, "client.key")
|
client_key = os.path.join(cert_dir, "client.key")
|
||||||
client_cert = os.path.join(cert_dir, "client.pem")
|
client_cert = os.path.join(cert_dir, "client.pem")
|
||||||
|
wrong_client_key = os.path.join(cert_dir, "wrong_client.key")
|
||||||
|
wrong_client_cert = os.path.join(cert_dir, "wrong_client.pem")
|
||||||
subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
||||||
"-keyout", ca_key, "-out", ca_cert, "-days", "1",
|
"-keyout", ca_key, "-out", ca_cert, "-days", "1",
|
||||||
"-subj", "/CN=FastSync Test CA"], check=True, capture_output=True)
|
"-subj", "/CN=FastSync Test CA"], check=True, capture_output=True)
|
||||||
san = os.path.join(cert_dir, "san.conf")
|
san = os.path.join(cert_dir, "san.conf")
|
||||||
|
san_ips = ["IP.1 = 127.0.0.1"]
|
||||||
|
for index, ip in enumerate(extra_san_ips or [], start=2):
|
||||||
|
san_ips.append("IP.%d = %s" % (index, ip))
|
||||||
with open(san, "w") as f:
|
with open(san, "w") as f:
|
||||||
f.write("[req]\ndistinguished_name = dn\nreq_extensions = v3_req\n\n"
|
f.write("[req]\ndistinguished_name = dn\nreq_extensions = v3_req\n\n"
|
||||||
"[dn]\nCN = localhost\n\n[v3_req]\nsubjectAltName = @an\n\n"
|
"[dn]\nCN = localhost\n\n[v3_req]\nsubjectAltName = @an\n\n"
|
||||||
"[an]\nDNS.1 = localhost\nIP.1 = 127.0.0.1\n")
|
"[an]\nDNS.1 = localhost\n" + "\n".join(san_ips) + "\n")
|
||||||
subprocess.run(["openssl", "req", "-newkey", "rsa:2048", "-nodes",
|
subprocess.run(["openssl", "req", "-newkey", "rsa:2048", "-nodes",
|
||||||
"-keyout", server_key, "-out", os.path.join(cert_dir, "server.csr"),
|
"-keyout", server_key, "-out", os.path.join(cert_dir, "server.csr"),
|
||||||
"-subj", "/CN=localhost", "-config", san], check=True, capture_output=True)
|
"-subj", "/CN=localhost", "-config", san], check=True, capture_output=True)
|
||||||
@@ -1031,12 +1058,20 @@ def _generate_tls_certs(cert_dir):
|
|||||||
subprocess.run(["openssl", "x509", "-req", "-in", os.path.join(cert_dir, "client.csr"),
|
subprocess.run(["openssl", "x509", "-req", "-in", os.path.join(cert_dir, "client.csr"),
|
||||||
"-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial",
|
"-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial",
|
||||||
"-out", client_cert, "-days", "1"], check=True, capture_output=True)
|
"-out", client_cert, "-days", "1"], check=True, capture_output=True)
|
||||||
|
subprocess.run(["openssl", "req", "-newkey", "rsa:2048", "-nodes",
|
||||||
|
"-keyout", wrong_client_key, "-out", os.path.join(cert_dir, "wrong_client.csr"),
|
||||||
|
"-subj", "/CN=wrong-client"], check=True, capture_output=True)
|
||||||
|
subprocess.run(["openssl", "x509", "-req", "-in", os.path.join(cert_dir, "wrong_client.csr"),
|
||||||
|
"-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial",
|
||||||
|
"-out", wrong_client_cert, "-days", "1"], check=True, capture_output=True)
|
||||||
return {
|
return {
|
||||||
"ca": ca_cert,
|
"ca": ca_cert,
|
||||||
"server_cert": server_cert,
|
"server_cert": server_cert,
|
||||||
"server_key": server_key,
|
"server_key": server_key,
|
||||||
"client_cert": client_cert,
|
"client_cert": client_cert,
|
||||||
"client_key": client_key,
|
"client_key": client_key,
|
||||||
|
"wrong_client_cert": wrong_client_cert,
|
||||||
|
"wrong_client_key": wrong_client_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1077,3 +1112,48 @@ class TestDaemonTLSAuth:
|
|||||||
d.stop()
|
d.stop()
|
||||||
os.unlink(client_creds)
|
os.unlink(client_creds)
|
||||||
shutil.rmtree(cert_dir, ignore_errors=True)
|
shutil.rmtree(cert_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
@pytest.mark.ci
|
||||||
|
def test_wrong_client_cn_refused_before_auth_challenge(self):
|
||||||
|
"""A7-3/S1: over a NON-local TLS connection an auth-required module is
|
||||||
|
refused at the config gate when the CA-valid client certificate does not
|
||||||
|
match --client-cn -- before any SCRAM challenge is sent and before any
|
||||||
|
file data moves. The daemon is started WITH --allow-unauthenticated to
|
||||||
|
prove that flag does not relax the auth-module transport policy."""
|
||||||
|
try:
|
||||||
|
remote_ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
except OSError:
|
||||||
|
pytest.skip("hostname does not resolve")
|
||||||
|
if remote_ip.startswith("127."):
|
||||||
|
pytest.skip("host resolves to loopback; no non-loopback interface")
|
||||||
|
cert_dir = os.path.join(TEST_DATA_DIR, "daemon_tls_certs_wrong")
|
||||||
|
certs = _generate_tls_certs(cert_dir, extra_san_ips=[remote_ip])
|
||||||
|
client_creds = os.path.join(TEST_DATA_DIR, "daemon_tls_wrong_client.pw")
|
||||||
|
_write_client_password_file(client_creds, "alice", ALICE_PASS)
|
||||||
|
d = DaemonManager()
|
||||||
|
port = _find_free_port()
|
||||||
|
log_path = os.path.join(TEST_DATA_DIR, "fastsyncd.log")
|
||||||
|
try:
|
||||||
|
d.start(CONF_FILE, port_override=port, extra_args=[
|
||||||
|
"--tls", "--cert", certs["server_cert"], "--key", certs["server_key"],
|
||||||
|
"--ca", certs["ca"], "--client-cn", "fastsync-client",
|
||||||
|
"--password-file", CRED_FILE])
|
||||||
|
before_files = _tree_file_count(AUTH_MODULE)
|
||||||
|
log_before = os.path.getsize(log_path) if os.path.exists(log_path) else 0
|
||||||
|
tls_flags = ["--tls",
|
||||||
|
"--cert", certs["wrong_client_cert"], "--key",
|
||||||
|
certs["wrong_client_key"], "--ca", certs["ca"]]
|
||||||
|
result, _ = run_client(SOURCE_DIR, "%s::locked" % remote_ip, port=port,
|
||||||
|
flags=tls_flags, extra_args=["--password-file", client_creds])
|
||||||
|
assert result.returncode != 0, "a wrong client CN must be refused"
|
||||||
|
assert _tree_file_count(AUTH_MODULE) == before_files, \
|
||||||
|
"a refused connection wrote file data"
|
||||||
|
with open(log_path, "rb") as f:
|
||||||
|
f.seek(log_before)
|
||||||
|
tail = f.read().decode("utf-8", "replace")
|
||||||
|
assert "requires authentication over an encrypted, verified TLS connection" in tail, \
|
||||||
|
tail[-400:]
|
||||||
|
finally:
|
||||||
|
d.stop()
|
||||||
|
os.unlink(client_creds)
|
||||||
|
shutil.rmtree(cert_dir, ignore_errors=True)
|
||||||
|
|||||||
@@ -69,6 +69,42 @@ static void test_validate_config_tls_requirements() {
|
|||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A7-3/S1: --password-file sends daemon credentials, so it is only allowed
|
||||||
|
over TLS (which itself mandates a verified --cert/--key/--ca) or to a
|
||||||
|
loopback destination. A remote plaintext daemon is refused up front. */
|
||||||
|
static void test_validate_config_credentials_require_tls_or_loopback() {
|
||||||
|
/* Default host is 127.0.0.1 (loopback), so plaintext credentials are fine. */
|
||||||
|
Config* cfg = valid_client_config();
|
||||||
|
cfg->password_file = str_dup("creds.pw");
|
||||||
|
EXPECT_TRUE(validate_config(cfg));
|
||||||
|
|
||||||
|
/* localhost is loopback too. */
|
||||||
|
free(cfg->server_host);
|
||||||
|
cfg->server_host = str_dup("localhost");
|
||||||
|
EXPECT_TRUE(validate_config(cfg));
|
||||||
|
|
||||||
|
/* A clearly remote host over plaintext is refused before any network I/O. */
|
||||||
|
free(cfg->server_host);
|
||||||
|
cfg->server_host = str_dup("192.0.2.1");
|
||||||
|
EXPECT_FALSE(validate_config(cfg));
|
||||||
|
|
||||||
|
/* TLS makes the remote destination acceptable (cert/key/ca are required). */
|
||||||
|
cfg->use_tls = true;
|
||||||
|
EXPECT_FALSE(validate_config(cfg));
|
||||||
|
cfg->tls_cert = str_dup("cert.pem");
|
||||||
|
cfg->tls_key = str_dup("key.pem");
|
||||||
|
cfg->tls_ca = str_dup("ca.pem");
|
||||||
|
EXPECT_TRUE(validate_config(cfg));
|
||||||
|
|
||||||
|
/* No credentials: the remote plaintext rule does not apply. */
|
||||||
|
cfg->use_tls = false;
|
||||||
|
char* creds = cfg->password_file;
|
||||||
|
cfg->password_file = NULL;
|
||||||
|
EXPECT_TRUE(validate_config(cfg));
|
||||||
|
cfg->password_file = creds;
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_validate_config_delta_sendfile_constraints() {
|
static void test_validate_config_delta_sendfile_constraints() {
|
||||||
Config* cfg = valid_client_config();
|
Config* cfg = valid_client_config();
|
||||||
cfg->use_delta = true;
|
cfg->use_delta = true;
|
||||||
@@ -3117,6 +3153,7 @@ void test_client_cli() {
|
|||||||
test_validate_config_append_verify_rejects_whole_file();
|
test_validate_config_append_verify_rejects_whole_file();
|
||||||
test_validate_config_incompatible_options();
|
test_validate_config_incompatible_options();
|
||||||
test_validate_config_tls_requirements();
|
test_validate_config_tls_requirements();
|
||||||
|
test_validate_config_credentials_require_tls_or_loopback();
|
||||||
test_validate_config_delta_sendfile_constraints();
|
test_validate_config_delta_sendfile_constraints();
|
||||||
test_cli_help();
|
test_cli_help();
|
||||||
test_cli_archive_flags();
|
test_cli_archive_flags();
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
|
#include <arpa/inet.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <threads.h>
|
#include <threads.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -269,12 +272,88 @@ static int escape_thread(void* arg) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A7-3/S1 transport classification: the daemon auth gate and the client
|
||||||
|
credential rule both key off these helpers, so cover the exact accepted
|
||||||
|
forms plus the negative cases. */
|
||||||
|
static void test_loopback_helpers() {
|
||||||
|
/* Host strings. */
|
||||||
|
EXPECT_TRUE(utils_host_is_loopback("localhost"));
|
||||||
|
EXPECT_TRUE(utils_host_is_loopback("127.0.0.1"));
|
||||||
|
EXPECT_TRUE(utils_host_is_loopback("127.255.255.254"));
|
||||||
|
EXPECT_TRUE(utils_host_is_loopback("127.0.0.0"));
|
||||||
|
EXPECT_TRUE(utils_host_is_loopback("::1"));
|
||||||
|
EXPECT_TRUE(utils_host_is_loopback("[::1]"));
|
||||||
|
EXPECT_FALSE(utils_host_is_loopback("128.0.0.1"));
|
||||||
|
EXPECT_FALSE(utils_host_is_loopback("10.0.0.1"));
|
||||||
|
EXPECT_FALSE(utils_host_is_loopback("0.0.0.0"));
|
||||||
|
EXPECT_FALSE(utils_host_is_loopback("example.com"));
|
||||||
|
EXPECT_FALSE(utils_host_is_loopback(""));
|
||||||
|
EXPECT_FALSE(utils_host_is_loopback(NULL));
|
||||||
|
|
||||||
|
/* Raw sockaddr classification. */
|
||||||
|
struct sockaddr_in v4;
|
||||||
|
memset(&v4, 0, sizeof(v4));
|
||||||
|
v4.sin_family = AF_INET;
|
||||||
|
EXPECT_TRUE(inet_pton(AF_INET, "127.0.0.1", &v4.sin_addr) == 1);
|
||||||
|
EXPECT_TRUE(utils_sockaddr_is_loopback((const struct sockaddr*)&v4));
|
||||||
|
EXPECT_TRUE(inet_pton(AF_INET, "127.5.5.5", &v4.sin_addr) == 1);
|
||||||
|
EXPECT_TRUE(utils_sockaddr_is_loopback((const struct sockaddr*)&v4));
|
||||||
|
EXPECT_TRUE(inet_pton(AF_INET, "128.0.0.1", &v4.sin_addr) == 1);
|
||||||
|
EXPECT_FALSE(utils_sockaddr_is_loopback((const struct sockaddr*)&v4));
|
||||||
|
|
||||||
|
struct sockaddr_in6 v6;
|
||||||
|
memset(&v6, 0, sizeof(v6));
|
||||||
|
v6.sin6_family = AF_INET6;
|
||||||
|
EXPECT_TRUE(inet_pton(AF_INET6, "::1", &v6.sin6_addr) == 1);
|
||||||
|
EXPECT_TRUE(utils_sockaddr_is_loopback((const struct sockaddr*)&v6));
|
||||||
|
EXPECT_TRUE(inet_pton(AF_INET6, "::ffff:127.0.0.1", &v6.sin6_addr) == 1);
|
||||||
|
EXPECT_TRUE(utils_sockaddr_is_loopback((const struct sockaddr*)&v6));
|
||||||
|
EXPECT_TRUE(inet_pton(AF_INET6, "::ffff:127.255.255.254", &v6.sin6_addr) == 1);
|
||||||
|
EXPECT_TRUE(utils_sockaddr_is_loopback((const struct sockaddr*)&v6));
|
||||||
|
EXPECT_TRUE(inet_pton(AF_INET6, "::ffff:10.0.0.1", &v6.sin6_addr) == 1);
|
||||||
|
EXPECT_FALSE(utils_sockaddr_is_loopback((const struct sockaddr*)&v6));
|
||||||
|
|
||||||
|
EXPECT_FALSE(utils_sockaddr_is_loopback(NULL));
|
||||||
|
|
||||||
|
/* A pipe has no socket peer: getpeername fails with ENOTSOCK, which is the
|
||||||
|
--stdio/SSH case and must count as local. */
|
||||||
|
int pipe_fds[2];
|
||||||
|
EXPECT_EQ_INT(pipe(pipe_fds), 0);
|
||||||
|
EXPECT_TRUE(utils_fd_peer_is_local(pipe_fds[0]));
|
||||||
|
close(pipe_fds[0]);
|
||||||
|
close(pipe_fds[1]);
|
||||||
|
EXPECT_FALSE(utils_fd_peer_is_local(-1));
|
||||||
|
|
||||||
|
/* A real loopback TCP peer is local. */
|
||||||
|
int listener = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
EXPECT_TRUE(listener >= 0);
|
||||||
|
struct sockaddr_in bind_addr;
|
||||||
|
memset(&bind_addr, 0, sizeof(bind_addr));
|
||||||
|
bind_addr.sin_family = AF_INET;
|
||||||
|
bind_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
bind_addr.sin_port = 0;
|
||||||
|
EXPECT_EQ_INT(bind(listener, (const struct sockaddr*)&bind_addr, sizeof(bind_addr)), 0);
|
||||||
|
EXPECT_EQ_INT(listen(listener, 1), 0);
|
||||||
|
socklen_t addr_len = sizeof(bind_addr);
|
||||||
|
EXPECT_EQ_INT(getsockname(listener, (struct sockaddr*)&bind_addr, &addr_len), 0);
|
||||||
|
int dialer = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
EXPECT_TRUE(dialer >= 0);
|
||||||
|
EXPECT_EQ_INT(connect(dialer, (const struct sockaddr*)&bind_addr, sizeof(bind_addr)), 0);
|
||||||
|
int accepted = accept(listener, NULL, NULL);
|
||||||
|
EXPECT_TRUE(accepted >= 0);
|
||||||
|
EXPECT_TRUE(utils_fd_peer_is_local(accepted));
|
||||||
|
close(accepted);
|
||||||
|
close(dialer);
|
||||||
|
close(listener);
|
||||||
|
}
|
||||||
|
|
||||||
void test_shared_utils() {
|
void test_shared_utils() {
|
||||||
test_walker_removes_extras_keeps_manifest_and_protected();
|
test_walker_removes_extras_keeps_manifest_and_protected();
|
||||||
test_walker_max_delete_exceeded_deletes_nothing();
|
test_walker_max_delete_exceeded_deletes_nothing();
|
||||||
test_walker_max_delete_exact_bound_deletes();
|
test_walker_max_delete_exact_bound_deletes();
|
||||||
test_walker_unlimited_deletes_all();
|
test_walker_unlimited_deletes_all();
|
||||||
test_walker_hard_bound_all_or_nothing();
|
test_walker_hard_bound_all_or_nothing();
|
||||||
|
test_loopback_helpers();
|
||||||
|
|
||||||
/* --append / --append-verify tail-resume math: a resume is eligible only for
|
/* --append / --append-verify tail-resume math: a resume is eligible only for
|
||||||
a shorter existing destination, and the tail length is then the difference. */
|
a shorter existing destination, and the tail length is then the difference. */
|
||||||
|
|||||||
Reference in New Issue
Block a user