Commit Graph
508 Commits
Author SHA1 Message Date
TapTap 18d1b84246 refactor(protocol): guard session release, clarify Data.owner contract
Add a NULL guard to protocol_release_memory_for_session so it no-ops like
the sibling session setters.  Correct the Data.owner doc comment, which
implied a non-zero protocol_charge always has an owner; document that
owner may be NULL for uncharged/ownerless Data, that any such charge
falls back to the bound session, and that a charged Data must not outlive
its owning session.  Note the lifetime contract on the release API too.

Extend tests/test_protocol.c to cover destroying a charged Data with no
session bound (the other half of the original bug) and to assert that
data_create/data_create_reserve start with owner == NULL and
protocol_charge == 0.
2026-09-13 10:42:45 +02:00
TapTap c78a21de57 docs(shared): clarify authorized_root accessor contracts
Document on utils_get_authorized_root_path() that the returned pointer is
borrowed and invalidated by the next authorized-root setter, that the fd
and path are not read atomically (non-reentrant), and that the fd remains
caller-owned.  Add a matching single-threaded/set-before-threads note at
the accessor definitions in utils.c.

In server.c, drop the redundant utils_set_authorized_root(-1, NULL) after
a failed utils_set_authorized_root(): the setter already fail-closes the
state on allocation failure.  The following close(root_fd) is unchanged.
2026-09-13 10:38:21 +02:00
TapTap 87f6cb0243 refactor(config): single X-macro table for serialized fields
Every Config field that crosses the wire was declared in up to six places
(struct member, config_set_defaults, send_*, receive_*, and the two CLI
option tables) and could drift silently.  Add CONFIG_WIRE_FIELDS in
config.h: one ordered per-segment table where each serialized field is
declared once with its C type, default and wire codec (KIND).

config.h now expands the table to declare the struct members;
config_set_defaults() expands it to assign the defaults; and
config_send_wire_block()/config_receive() expand the per-segment lists to
emit/consume the frame.  The per-segment function names, call order and
segment boundaries are preserved exactly.

Fields with genuinely custom logic keep dedicated helpers but are still
declared once in the table: the protocol-version handshake (HEADER), daemon
SCRAM auth (STR_REDACTED_AUTH), the daemon module name (STR_MODULE), the
repeated count+array blocks (BLOCK_SKIP_SUFFIXES/BLOCK_BASIS/BLOCK_IDMAP),
--copy-as presence/ids (COPY_AS_*), and the derived --delta / use_xattrs
bits (DERIVED_DELTA, BOOL_XATTR_DERIVE).  The version field remains a
special header (validated before any other field is parsed) and is sent by
config_send_wire_block() explicitly.

No public field is renamed and PROTOCOL_VERSION stays "2.20.0".  Because
the struct declaration order is no longer the wire order, the wire order is
now enforced solely by the table and by a byte-exact golden test
(follow-up commit).  Add config_send_wire_block() so that test can hash the
frame body without the STATUS_OK handshake.
2026-09-13 10:28:26 +02:00
TapTap 4c17122b00 feat(daemon): enforce per-module/per-host caps and shared auth lockout
Wire the shared registry into the accept loop (parent claims a slot before
fork, blocks SIGCHLD across fork+pid publication, and reclaims the dead
child's slot from the SIGCHLD handler so per-module/per-source counts are
released even on SIGKILL). The connection child records the selected module
and normalized peer IP once the config frame names them: an over-cap module
or source is refused at the config gate with an audit log, and a source
that exceeded the auth-failure threshold is refused before a SCRAM
challenge (the counter is shared across children and cleared on success).
The existing global cap and host ACLs are untouched.
2026-09-13 10:24:05 +02:00
TapTap 0abaa62193 feat(daemon): parse per-host cap and auth lockout config keys
Add global keys `max connections per host` (default 0 = unlimited),
`auth lockout threshold` (default 10, 0 disables) and
`auth lockout duration` (default 300 s, 0 disables). Module
`max connections` now accepts 0 as unlimited. Bound the number of
[module] sections (DAEMON_CONF_MAX_MODULES) so the shared registry's
per-module counter array stays fixed-size; absent keys keep their
defaults so old configs still load.
2026-09-13 10:24:01 +02:00
TapTap 5334397b81 feat(daemon): add shared cross-process connection registry
The daemon forks one child per accepted connection, so per-module and
per-source accounting must live in state shared across the children. Add a
fixed-size registry carved from an anonymous shared mapping
(mmap(MAP_SHARED|MAP_ANONYMOUS)) created before the accept loop: a slot
lifecycle (FREE/CLAIMED/REGISTERED) with parent claim/reclaim and a
lock-free, open-addressed per-source table for the per-host occupancy and
the shared auth-failure counter. C11 atomics only; no pthread locks across
fork.

Unit tests cover slot exhaustion, the module/host caps, pid reclaim and
fork-shared visibility.
2026-09-13 10:23:58 +02:00
TapTap 3260a39ab4 refactor(shared): single owner for authorized_root state 2026-09-13 10:06:04 +02:00
TapTap 5d3c43305e fix(protocol): release Data charge to its owning session
Data charged against a ProtocolSession kept only the charge amount, so
data_destroy released it from whatever session was thread-locally bound
at destroy time. Destroying a received Data on another thread, after the
session was unbound, or while a different session was bound leaked the
originating session's budget and underflowed the other's.

Add Data.owner, set it whenever protocol_receive_data_limited charges a
session, and have data_destroy release against that owner directly via
the newly-exported protocol_release_memory_for_session. Uncharged Data
(owner NULL) keeps the previous bound-session fallback.

Add a unit test proving a Data acquired on session A is released to A
even when unrelated session B is bound at destroy time.
2026-09-13 10:05:38 +02:00
TapTap 0155902d95 docs: update stale PipelineContextReceiver reference 2026-09-13 07:30:28 +02:00
TapTap 3499baf80b build: explicit CMake targets; move receiver pipeline out of shared 2026-09-13 07:20:28 +02:00
TapTap c2df0347ef fix(client,protocol): EINTR-safe sends, armed abort, keepalive drain grace, TLS WANT_WRITE 2026-09-13 06:59:02 +02:00
TapTap 1fa2fbd266 feat(client): --port alias, --threads=N, graceful abort, keepalive 2026-09-13 06:22:28 +02:00
TapTap dcc78c14c5 docs,fuzz: fix ownership/alloc comments; fuzz chunk metadata path 2026-09-13 05:48:27 +02:00
TapTap 5a829adb85 Merge branch 'fix/w5-scanner' into fix/w5-integration 2026-09-13 05:28:50 +02:00
TapTap 42c72030fb Merge branch 'fix/w5-config' into fix/w5-integration 2026-09-13 05:28:50 +02:00
TapTap 99f8045105 refactor(scanner,send): embed scanner options; unify stats and config ownership 2026-09-13 05:28:32 +02:00
TapTap eea66a7848 refactor(config,metadata): shared invariants; length-bounded metadata parser 2026-09-13 05:24:23 +02:00
TapTap c944787e03 refactor: remove dead file_store subsystem and unused wrappers 2026-09-13 05:19:18 +02:00
TapTap 3cf2e2c91f docs(version): align 2.20.0 artifacts; fix protocol-bump rationale 2026-09-13 05:01:15 +02:00
TapTap 301cb0dbaf fix(utils,file-list): bound keep/files-from indexes to O(M) memory 2026-09-13 04:52:10 +02:00
TapTap a4f4110397 Merge branch 'fix/w4-queue' into fix/w4-integration 2026-09-13 04:19:12 +02:00
TapTap 24fe8c5583 Merge branch 'fix/w4-compress' into fix/w4-integration 2026-09-13 04:19:12 +02:00
TapTap d274bdff4e Merge branch 'fix/w4-hash' into fix/w4-integration 2026-09-13 04:19:12 +02:00
TapTap 6c636a19e6 perf(compression,tcp): reuse zstd contexts; enable TCP_NODELAY 2026-09-13 04:18:55 +02:00
TapTap 1a83e284c4 perf(utils,file-list): index delete keep-set and --files-from lookups 2026-09-13 04:16:28 +02:00
TapTap 317d5d081a perf(protocol): pack metadata into one frame (PROTOCOL 2.20.0) 2026-09-13 04:08:36 +02:00
TapTap 69fe7f3c9f perf(send,scanner): byte-bound sender queues; drop redundant stat 2026-09-13 04:06:30 +02:00
TapTap ffa1d24625 fix(receiver): harden idle-progress definition, single error frame, sendfile timeout 2026-09-13 03:46:20 +02:00
TapTap b16349b81e fix(protocol): honor --timeout for protocol I/O; bound idle/session time 2026-09-13 03:29:01 +02:00
TapTap fc560246c1 fix(daemon): close ACL fail-opens (v4-mapped peers, invalid patterns) and cap auth delay 2026-09-13 02:51:07 +02:00
TapTap dff6609976 feat(daemon): host ACL, configurable max connections, peer audit, auth-failure delay 2026-09-13 02:36:15 +02:00
TapTap ba1c7a369f fix(server,log): non-socket shutdown fallback, drop redundant delay cleanup, unlock logging I/O 2026-09-13 02:13:22 +02:00
TapTap 312ed05170 Merge branch 'fix/w2-log' into fix/w2-integration 2026-09-13 01:49:44 +02:00
TapTap fecbe2c90c fix(server): child-safe signals, single fd owner, handler cleanup epilogue 2026-09-13 01:49:27 +02:00
TapTap c8f5d80fcb fix(log): serialize message emission; clear log_fp before close; use logger 2026-09-13 01:44:59 +02:00
TapTap ea2f76cd7a fix(receiver): charge per-entry DirTimeList cost; cap client --skip-compress 2026-09-13 01:18:54 +02:00
TapTap 76eeba1773 Merge branch 'fix/w1d-hardening' into fix/w1-integration 2026-09-13 00:59:35 +02:00
TapTap b72ab298ab Merge branch 'fix/w1c-wire' into fix/w1-integration 2026-09-13 00:59:35 +02:00
TapTap a90e234eb3 harden: overflow guards, auth-user validation, TLS1.3 policy, build hardening 2026-09-13 00:59:21 +02:00
TapTap f8252cf3e7 fix(protocol): bound pre-auth config string memory 2026-09-13 00:57:32 +02:00
TapTap 4557924972 fix(receiver): cap DirTimeList growth and fix placeholder Data leaks 2026-09-13 00:57:32 +02:00
TapTap 082ac2645d Merge feat/ref-dirtime: dir-time capture gate dedup 2026-09-12 20:43:42 +02:00
TapTap 1fd462cca8 refactor(config): replace SUPER_MODE_* macros with SuperMode enum
Type Config.super_mode as SuperMode (a proper C enum) instead of a bare
int.  The wire boundary still carries the mode as an int: send casts the
enum explicitly and receive reads a temporary int, validates the
AUTO..OFF range, then casts.  Emitted bytes and accepted values are
unchanged.  ModuleGateContext.super_mode_override keeps its -1 sentinel
as int with an explicit cast at the apply site.

Behavior preserved.
2026-09-12 20:43:24 +02:00
TapTap 4ac37c4d8a refactor(dir-times): extract dir_times_should_capture predicate
Deduplicate the repeated directory-time capture gate
(`config->use_metadata && !config->omit_dir_times`) used by the
sender-side (multiprocessing.c) and receiver-side (receiver.c) sinks
into a single predicate declared next to the DirTimeList machinery in
file_receive.h and defined in file_receive.c.

Behavior preserved: identical short-circuit condition and semantics,
no signature or protocol changes.
2026-09-12 20:42:47 +02:00
TapTap 1b90ee2449 fix(review): close loopback TLS auth bypass; align docs and wrong-CN test
- server gate: the --allow-unauthenticated loopback allowance now requires
  an actual plaintext connection (!gate_ctx->ssl), so a loopback TLS client
  whose cert fails the --client-cn check is refused before any SCRAM
  challenge instead of falling through the plaintext opt-in.  Keep the
  invalid-fd guard as belt-and-braces (unreachable after the policy check).
- test: rewrote test_wrong_client_cn_refused_before_auth_challenge to run
  deterministically over 127.0.0.1 with --tls + --allow-unauthenticated and
  a CA-valid wrong-CN client cert, asserting the gate refusal log and an
  unchanged module tree (no skip).
- docs: --client-cn is mandatory with --tls; dummykey sidecar is secret
  material; document all transient-fallback reasons; qualify
  --allow-unauthenticated in README and --help so it cannot read as
  permitting remote plaintext auth.
- credentials.h: drop stale restrictive-umask claim (fchmod forces exact
  0600; only create/write/fsync/link/fchmod failure degrades to ephemeral).
2026-09-12 19:50:52 +02:00
TapTap 89b967f29a Merge feat/dummy-key: persist anti-enumeration dummy key across restarts
# Conflicts:
#	RSYNC_COMPAT.md
2026-09-12 19:30:35 +02:00
TapTap ac3c4c7c72 fix(a7-auth): harden dummy-key temp creation
- Make the atomic-publish temp name unpredictable by appending 16 random
  hex chars to the pid, so a leftover/planted temp cannot be targeted.
- On EEXIST, unlink the stale temp and retry the O_EXCL create once
  (bounded), so a crash leftover or reused pid cannot silently defeat
  sidecar persistence.
- fchmod the temp fd to 0600 after creation (umask can clear owner bits)
  and treat failure as a create failure, so the published sidecar is
  always exactly 0600.
- Clarify comments: the sidecar requires exact 0600 while the store and
  password files only reject group/other bits.
- Add a unit test that a restrictive umask still yields an exact 0600
  sidecar; clean random-suffixed temps in tests.
2026-09-12 19:29:23 +02:00
TapTap d53614d06b fix(a7-3/s1): fail closed on non-loopback peers; require plaintext opt-in before challenge
utils_fd_peer_is_local now returns true only when getpeername SUCCEEDS and the
peer address classifies as loopback. A non-socket descriptor (pipe/socketpair)
or any getpeername error is NOT local, so the daemon auth gate fails closed
instead of treating an untestable --stdio pipe as trusted (daemon auth modules
are --daemon-only and the stdio path never loads a daemon config).

server_module_gate now requires --allow-unauthenticated for the loopback
plaintext auth path: a plaintext loopback connection without the operator
opt-in is refused at the config gate BEFORE server_auth_handshake, so no SCRAM
challenge is sent. Remote peers still require verified TLS regardless of the
flag; the handler keeps its defense-in-depth checks.

Docs state the exact policy (verified TLS with matching --client-cn, or
operator-opted-in loopback plaintext), drop the SSH/stdio auth-transport claim
(they are daemon-only), and add the loopback trust-boundary relay caveat and
the CN-only (no SAN) residual. Adds a unit-test negative for pipe/socketpair
and an integration test where a relay observes no challenge when the flag is
absent.
2026-09-12 19:18:29 +02:00
TapTap f0381a6b8e fix(a7-auth): publish dummy-key sidecar atomically and harden reads
Address review findings on the persistent dummy-key sidecar:

- Publish atomically: write a private same-directory temp file
  (<store>.dummykey.tmp.<pid>, 0600), fsync, then link(2) into place;
  fsync the containing directory and drop the temp name. A concurrent
  starter can no longer observe a zero/partial sidecar and fail closed.
  On EEXIST adopt the winner's sidecar; otherwise warn and use a
  transient ephemeral key.
- Harden the read path (initial and EEXIST-adopt) with
  O_RDONLY|O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC: reject planted symlinks
  (ELOOP fails closed) and never block on a planted FIFO.
- Require the exact owner-only mode (st_mode & 07777) == 0600 and make
  the rejection message truthful.
- Report a clear "short write" instead of a stale strerror(errno) when
  write() returns 0.
- Document the artifact and its creation-failure caveat (FIFO store
  path, read-only filesystem, missing directory) in README.md and
  RSYNC_COMPAT.md.
- Tests: known-key sidecar adoption (dummy salt KAT + reload), symlink
  rejection, and the exact-0600 rule (0400 now rejected).
2026-09-12 19:16:11 +02:00
TapTap a7a1930e88 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.
2026-09-12 19:02:05 +02:00