Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc71a7df5 | ||
|
|
ffa1d24625 | ||
|
|
b16349b81e | ||
|
|
4bc84fe954 | ||
|
|
fc560246c1 | ||
|
|
dff6609976 | ||
|
|
1acb66628d | ||
|
|
ba1c7a369f | ||
|
|
d28489d83c | ||
|
|
312ed05170 | ||
|
|
fecbe2c90c | ||
|
|
c8f5d80fcb | ||
|
|
8147ff7b50 | ||
|
|
b7fbb56289 | ||
|
|
08063b6d73 | ||
|
|
ea2f76cd7a | ||
|
|
76eeba1773 | ||
|
|
b72ab298ab | ||
|
|
446a714ef8 | ||
|
|
a90e234eb3 | ||
|
|
f8252cf3e7 | ||
|
|
4557924972 | ||
|
|
59ce174d22 | ||
|
|
2a8941ee5c | ||
|
|
37037a6ee7 | ||
|
|
061e9ad43f | ||
|
|
f928879755 | ||
|
|
84b7cb0de3 | ||
|
|
921472b8b3 | ||
|
|
082ac2645d | ||
|
|
eefbd1e849 | ||
|
|
1fd462cca8 | ||
|
|
4ac37c4d8a | ||
|
|
08af945bd6 |
No files matched your search
@@ -12,7 +12,7 @@ jobs:
|
|||||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v10
|
container: gitea.tap-tap.win/taptap/fastsync-ci:v10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
|
||||||
- name: clang-format check
|
- name: clang-format check
|
||||||
run: find src/ tests/ -name '*.c' -o -name '*.h' | xargs clang-format --dry-run --Werror
|
run: find src/ tests/ -name '*.c' -o -name '*.h' | xargs clang-format --dry-run --Werror
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
needs: lint
|
needs: lint
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
|
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
sanitizer: [address, undefined]
|
sanitizer: [address, undefined]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
run: cmake -B build-${{ matrix.sanitizer }} -S . -DSANITIZER=${{ matrix.sanitizer }}
|
run: cmake -B build-${{ matrix.sanitizer }} -S . -DSANITIZER=${{ matrix.sanitizer }}
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
|
||||||
- name: Configure (clang + fuzz)
|
- name: Configure (clang + fuzz)
|
||||||
run: CC=clang CXX=clang++ cmake -B build-fuzz -S . -DENABLE_FUZZ=ON
|
run: CC=clang CXX=clang++ cmake -B build-fuzz -S . -DENABLE_FUZZ=ON
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
run: cmake -B build -S . -DENABLE_COVERAGE=ON
|
run: cmake -B build -S . -DENABLE_COVERAGE=ON
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
|
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
|
||||||
|
|||||||
+41
-1
@@ -38,11 +38,24 @@ if(ENABLE_COVERAGE)
|
|||||||
add_link_options(--coverage)
|
add_link_options(--coverage)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- Build hardening option ---
|
||||||
|
# Production hardening is applied to the shipping server/client binaries only,
|
||||||
|
# and only when no sanitizer or coverage instrumentation is active: sanitizers
|
||||||
|
# carry their own instrumentation, and _FORTIFY_SOURCE requires an optimising
|
||||||
|
# build (never the -O0 used for coverage).
|
||||||
|
option(ENABLE_HARDENING "Enable compiler/linker hardening for production targets" ON)
|
||||||
|
set(HARDENING_ACTIVE OFF)
|
||||||
|
if(ENABLE_HARDENING AND SANITIZER STREQUAL "none" AND NOT ENABLE_COVERAGE)
|
||||||
|
set(HARDENING_ACTIVE ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
xxhash
|
xxhash
|
||||||
GIT_REPOSITORY https://github.com/Cyan4973/xxHash
|
GIT_REPOSITORY https://github.com/Cyan4973/xxHash
|
||||||
GIT_TAG v0.8.3
|
# v0.8.3 is a lightweight tag pointing at this exact commit (no ^{} peel
|
||||||
|
# entry); pin the commit SHA instead of the mutable tag.
|
||||||
|
GIT_TAG e626a72bc2321cd320e953a0ccf1584cad60f363 # v0.8.3
|
||||||
SOURCE_SUBDIR cmake_unofficial
|
SOURCE_SUBDIR cmake_unofficial
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(xxhash)
|
FetchContent_MakeAvailable(xxhash)
|
||||||
@@ -73,6 +86,33 @@ add_executable(client ${CLIENT_SRCS} ${SHARED_SRCS} ${FILE_STORE_SRCS} ${SERVER_
|
|||||||
target_include_directories(client PRIVATE src/shared src/server src/client)
|
target_include_directories(client PRIVATE src/shared src/server src/client)
|
||||||
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||||
|
|
||||||
|
# --- Production hardening ---
|
||||||
|
# Each compile flag is probed so a compiler/architecture that lacks it still
|
||||||
|
# configures cleanly. _FORTIFY_SOURCE is guarded separately because it only
|
||||||
|
# works in an optimising build. xxHash is a static archive built by
|
||||||
|
# FetchContent, so it must be position-independent for the -pie link.
|
||||||
|
if(HARDENING_ACTIVE)
|
||||||
|
set_target_properties(xxhash PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||||
|
include(CheckCCompilerFlag)
|
||||||
|
foreach(flag -fstack-protector-strong -fstack-clash-protection -fPIE)
|
||||||
|
string(MAKE_C_IDENTIFIER "HARDEN_${flag}" _harden_var)
|
||||||
|
check_c_compiler_flag("${flag}" ${_harden_var})
|
||||||
|
endforeach()
|
||||||
|
check_c_compiler_flag("-D_FORTIFY_SOURCE=2" HARDEN_FORTIFY_SOURCE)
|
||||||
|
foreach(target server client)
|
||||||
|
foreach(flag -fstack-protector-strong -fstack-clash-protection -fPIE)
|
||||||
|
string(MAKE_C_IDENTIFIER "HARDEN_${flag}" _harden_var)
|
||||||
|
if(${_harden_var})
|
||||||
|
target_compile_options(${target} PRIVATE ${flag})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
if(HARDEN_FORTIFY_SOURCE)
|
||||||
|
target_compile_options(${target} PRIVATE -D_FORTIFY_SOURCE=2)
|
||||||
|
endif()
|
||||||
|
target_link_options(${target} PRIVATE -pie -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack)
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|
||||||
# --- Testing ---
|
# --- Testing ---
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ partial, alternate, and planned behavior.
|
|||||||
| `--existing` | Skip files not already present at the destination; update existing files normally. |
|
| `--existing` | Skip files not already present at the destination; update existing files normally. |
|
||||||
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
|
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
|
||||||
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) |
|
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) |
|
||||||
| `--timeout <sec>` | I/O timeout in seconds (default: 30) |
|
| `--timeout <sec>` | Positive I/O timeout in seconds, applied to both the socket (`SO_RCVTIMEO`/`SO_SNDTIMEO`, built-in default 30 s) and the per-message protocol poll deadline (built-in default 60 s). Omit the option to keep both built-ins; `0` is rejected. The server side keeps the built-in 60 s protocol window (the value is not sent on the wire). |
|
||||||
| `--contimeout <sec>` | Connection timeout in seconds (default: 10) |
|
| `--contimeout <sec>` | Connection timeout in seconds (default: 10) |
|
||||||
| `--backup` | Backup existing destination files before overwriting |
|
| `--backup` | Backup existing destination files before overwriting |
|
||||||
| `--backup-dir <dir>` | Target directory for backups (requires `--backup`) |
|
| `--backup-dir <dir>` | Target directory for backups (requires `--backup`) |
|
||||||
@@ -151,6 +151,19 @@ partial, alternate, and planned behavior.
|
|||||||
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
|
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
|
||||||
| `--client-cn <name>` | TLS client certificate common name; mandatory with `--tls` (a TLS connection always verifies the client CN) |
|
| `--client-cn <name>` | TLS client certificate common name; mandatory with `--tls` (a TLS connection always verifies the client CN) |
|
||||||
|
|
||||||
|
**Per-message vs. connection timeouts.** `--timeout` bounds each individual protocol
|
||||||
|
send/receive (the `poll()` deadline), so a peer that stops mid-frame is dropped. It
|
||||||
|
does not, by itself, stop a peer that keeps sending well-formed frames forever. The
|
||||||
|
receiver therefore also enforces two wall-clock (`CLOCK_MONOTONIC`) bounds on a
|
||||||
|
connection: a **1 hour** idle limit and a **24 hour** overall session cap. Only
|
||||||
|
frames that move real work (not `STATUS_KEEPALIVE`/`STATUS_ABORT` and not an
|
||||||
|
empty `STATUS_CHECK_BATCH`/`STATUS_DIR_TIMES`) refresh the idle timestamp, so a
|
||||||
|
peer cannot hold a connection slot by emitting cheap empty frames; a peer that
|
||||||
|
fabricates minimal non-empty frames can still occupy a slot until the 24 hour
|
||||||
|
cap, since no bound can require actual payload without risking a legitimate
|
||||||
|
long operation. Both are deliberately generous so a legitimate long-running
|
||||||
|
transfer is never aborted.
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
| Argument | Description |
|
| Argument | Description |
|
||||||
@@ -384,7 +397,7 @@ features without changing the meaning of ordinary compatibility options.
|
|||||||
| `--bwlimit <KB/s>` | Apply token-bucket bandwidth limiting. |
|
| `--bwlimit <KB/s>` | Apply token-bucket bandwidth limiting. |
|
||||||
| `--progress` | Show transfer progress and throughput. |
|
| `--progress` | Show transfer progress and throughput. |
|
||||||
| `--stats` | Print transfer statistics. |
|
| `--stats` | Print transfer statistics. |
|
||||||
| `--timeout <seconds>` | Set I/O timeout. |
|
| `--timeout <seconds>` | Set the socket **and** per-message protocol I/O timeout (positive seconds). Omit to keep the built-in 30 s socket / 60 s protocol defaults. |
|
||||||
| `--contimeout <seconds>` | Set connection timeout. |
|
| `--contimeout <seconds>` | Set connection timeout. |
|
||||||
|
|
||||||
Short-option conflicts with rsync have been resolved for the CLI namespace
|
Short-option conflicts with rsync have been resolved for the CLI namespace
|
||||||
@@ -487,6 +500,33 @@ defaults to the current directory. |
|
|||||||
| `-v`, `--verbose` | Enable debug logging. |
|
| `-v`, `--verbose` | Enable debug logging. |
|
||||||
| `--help` | Print server usage. |
|
| `--help` | Print server usage. |
|
||||||
|
|
||||||
|
### Daemon configuration
|
||||||
|
|
||||||
|
`fastsync-server --daemon --config FILE` reads a line-based module config (an
|
||||||
|
implicit global section, then `[module]` sections). Besides `port`, `motd file`,
|
||||||
|
and `address`, the global section accepts:
|
||||||
|
|
||||||
|
- `max connections = N` — cap on concurrent connections, default 100. The
|
||||||
|
listener enforces it; `0`, negative, and non-numeric values are parse errors.
|
||||||
|
- `auth failure delay = MS` — milliseconds to sleep after a failed
|
||||||
|
authentication, default 500. `0` disables it and the value is capped at 60000,
|
||||||
|
so online password guessing is rate-limited per connection. Successful auths
|
||||||
|
are never delayed.
|
||||||
|
- `hosts allow` / `hosts deny` — comma- and/or whitespace-separated host access
|
||||||
|
patterns.
|
||||||
|
|
||||||
|
A `[module]` may also set `max connections` (parsed and validated but not
|
||||||
|
enforced per module — the global cap applies to the whole listener) and its own
|
||||||
|
`hosts allow`/`hosts deny`.
|
||||||
|
|
||||||
|
Host patterns are `*` (match all), IPv4/IPv6 literals, or IPv4/IPv6 CIDR
|
||||||
|
(`10.0.0.0/8`, `2001:db8::/32`). Hostnames are not resolved, so hostname globs
|
||||||
|
are rejected at parse time rather than silently never matching. A matching
|
||||||
|
`hosts deny` rejects; if any `hosts allow` entries exist, a peer matching none of
|
||||||
|
them is rejected; deny takes precedence over allow. The global list is checked
|
||||||
|
before the module list, before authentication, and the connecting peer address
|
||||||
|
(IPv4 or IPv6) appears in the connection and authentication audit log lines.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|||||||
+4
-2
@@ -627,7 +627,7 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
|||||||
|------|-------------------|-----------------|-------|
|
|------|-------------------|-----------------|-------|
|
||||||
| `--daemon` | Run as rsync daemon | ✅ Implemented | Wave A: a real persistent listener. `fastsync-server --daemon --config FILE` (plus `--no-detach` to stay foreground; without it the listener detaches to the background after binding) reads a FastSync-native module config file and serves each connection confined to the requested module's `path` root (never a client-chosen root; every client-chosen-ownership/super-user request (`--numeric-ids`/`--chown`/`--usermap`/`--groupmap`/`--fake-super`/`--copy-as`/explicit `--super`) is refused unless the module opts in with `client owner = yes`, and the operator `--no-super` veto is honored). TCP/TLS via the existing `--tls` stack; plaintext still requires `--allow-unauthenticated` (same secure default as the standalone server). Client destinations use rsync's `host::module/path` form. Wire/protocol: the config frame gained a trailing daemon-module string and `PROTOCOL_VERSION` was bumped **2.14.0 → 2.15.0** (see the Daemon Mode notes below). Daemon mode is built in FastSync's own protocol/config grammar, not rsync's SMB/daemon option encoding |
|
| `--daemon` | Run as rsync daemon | ✅ Implemented | Wave A: a real persistent listener. `fastsync-server --daemon --config FILE` (plus `--no-detach` to stay foreground; without it the listener detaches to the background after binding) reads a FastSync-native module config file and serves each connection confined to the requested module's `path` root (never a client-chosen root; every client-chosen-ownership/super-user request (`--numeric-ids`/`--chown`/`--usermap`/`--groupmap`/`--fake-super`/`--copy-as`/explicit `--super`) is refused unless the module opts in with `client owner = yes`, and the operator `--no-super` veto is honored). TCP/TLS via the existing `--tls` stack; plaintext still requires `--allow-unauthenticated` (same secure default as the standalone server). Client destinations use rsync's `host::module/path` form. Wire/protocol: the config frame gained a trailing daemon-module string and `PROTOCOL_VERSION` was bumped **2.14.0 → 2.15.0** (see the Daemon Mode notes below). Daemon mode is built in FastSync's own protocol/config grammar, not rsync's SMB/daemon option encoding |
|
||||||
| `--config=FILE` | Alternate rsyncd.conf file | ✅ Implemented | Wave A: selects the daemon config file. Default when omitted (in `--daemon` mode): `~/.config/fastsync/fastsyncd.conf` if it exists, else `/etc/fastsyncd.conf`. The grammar is FastSync-native (documented in the Daemon Mode notes below) and strictly rejects unknown keys so a typo can never silently change what a module serves; requires `--daemon` |
|
| `--config=FILE` | Alternate rsyncd.conf file | ✅ Implemented | Wave A: selects the daemon config file. Default when omitted (in `--daemon` mode): `~/.config/fastsync/fastsyncd.conf` if it exists, else `/etc/fastsyncd.conf`. The grammar is FastSync-native (documented in the Daemon Mode notes below) and strictly rejects unknown keys so a typo can never silently change what a module serves; requires `--daemon` |
|
||||||
| `--dparam=OVERRIDE` | Override global daemon config | ✅ Implemented | Wave A: overrides one global scalar from the command line (`--dparam port=8734` and `--dparam=KEY=VALUE` both work). Limited to the global scalar keys the grammar defines (`port`, `motd file`, `address`); keys are case-insensitive and unknown keys/invalid values are rejected. Requires `--daemon` |
|
| `--dparam=OVERRIDE` | Override global daemon config | ✅ Implemented | Wave A: overrides one global scalar from the command line (`--dparam port=8734` and `--dparam=KEY=VALUE` both work). Limited to the global keys the grammar defines (`port`, `motd file`, `address`, `max connections`, `auth failure delay`, `hosts allow`, `hosts deny`); keys are case-insensitive and unknown keys/invalid values are rejected. Requires `--daemon` |
|
||||||
| `--no-detach` | Don't detach from parent | ✅ Implemented | Wave A: with `--daemon`, keeps the listener in the foreground (what integration tests use). Without it the daemonizes (fork/setsid, stdio redirected to /dev/null) after the listening socket is bound. Requires `--daemon` |
|
| `--no-detach` | Don't detach from parent | ✅ Implemented | Wave A: with `--daemon`, keeps the listener in the foreground (what integration tests use). Without it the daemonizes (fork/setsid, stdio redirected to /dev/null) after the listening socket is bound. Requires `--daemon` |
|
||||||
| `--password-file=FILE` | Read daemon password from file | ✅ Implemented | A7 daemon auth. Client: `--password-file` supplies `user:password` for a `host::module/path` destination (the username is taken from this file, so `user@host::module` stays rejected); the literal password is held client-side only for the SCRAM handshake and wiped at teardown. Server (`fastsync-server --daemon --password-file FILE`): the salted-PBKDF2 verifier store that modules with `auth users` are verified against. **Neither the password nor any replayable bearer value crosses the wire or is stored server-side** — the store holds a per-user salt plus derived keys, and the daemon proves the secret with a per-connection nonce challenge. The file must be private to its owner: both the client and server verify the exact inode they read (open-then-`fstat`, so the check cannot be raced) and refuse a `--password-file`/`--early-input` that is not owned by the current user or grants any group/other permission bit (mode 0600), mirroring the TLS private-key check. A process-substitution pipe (`--early-input <(vault ...)`) is still accepted when it satisfies those checks. See the Daemon Mode notes below for the file formats and the plaintext/TLS caveat |
|
| `--password-file=FILE` | Read daemon password from file | ✅ Implemented | A7 daemon auth. Client: `--password-file` supplies `user:password` for a `host::module/path` destination (the username is taken from this file, so `user@host::module` stays rejected); the literal password is held client-side only for the SCRAM handshake and wiped at teardown. Server (`fastsync-server --daemon --password-file FILE`): the salted-PBKDF2 verifier store that modules with `auth users` are verified against. **Neither the password nor any replayable bearer value crosses the wire or is stored server-side** — the store holds a per-user salt plus derived keys, and the daemon proves the secret with a per-connection nonce challenge. The file must be private to its owner: both the client and server verify the exact inode they read (open-then-`fstat`, so the check cannot be raced) and refuse a `--password-file`/`--early-input` that is not owned by the current user or grants any group/other permission bit (mode 0600), mirroring the TLS private-key check. A process-substitution pipe (`--early-input <(vault ...)`) is still accepted when it satisfies those checks. See the Daemon Mode notes below for the file formats and the plaintext/TLS caveat |
|
||||||
| `--early-input=FILE` | Use FILE for daemon early exec | ✅ Implemented | Server-only (requires `--daemon`): a second credential-store file, same new-format grammar as `--password-file`, read before the listener accepts connections (a secrets-manager / process-substitution source). Its entries layer over `--password-file`: byte-identical verifiers dedupe, a conflicting verifier for the same user is a startup error. A daemon whose modules declare `auth users` must be given at least one of the two, or it refuses to start (fail closed) |
|
| `--early-input=FILE` | Use FILE for daemon early exec | ✅ Implemented | Server-only (requires `--daemon`): a second credential-store file, same new-format grammar as `--password-file`, read before the listener accepts connections (a secrets-manager / process-substitution source). Its entries layer over `--password-file`: byte-identical verifiers dedupe, a conflicting verifier for the same user is a startup error. A daemon whose modules declare `auth users` must be given at least one of the two, or it refuses to start (fail closed) |
|
||||||
@@ -635,7 +635,9 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
|||||||
|
|
||||||
**Daemon Mode notes (Wave A protocol 2.15.0; A7 auth protocol 2.19.0; MOTD no bump):** FastSync daemon mode is supported in FastSync's own protocol/config grammar, not rsync's SMB/daemon option encoding.
|
**Daemon Mode notes (Wave A protocol 2.15.0; A7 auth protocol 2.19.0; MOTD no bump):** FastSync daemon mode is supported in FastSync's own protocol/config grammar, not rsync's SMB/daemon option encoding.
|
||||||
|
|
||||||
- **Config grammar** (`fastsyncd.conf`): line-based; an implicit global section first, then `[module]` sections. Keys are case-insensitive, values are trimmed and may be wrapped in one layer of double quotes (`path = "/srv/my dir"`). `#` and `;` at the start of a line (after leading whitespace) are full-line comments; inline comments and `\` continuations are not supported. Lines are bounded (4096 chars). Global keys: `port` (default 873), `motd file` (the daemon sends its bounded, escaped content to a client after the module gate/auth accepts, unless the client passes `--no-motd`), `address` (optional bind address). Module keys: `path` (required; the daemon-side authorized root for that module), `read only` (yes/no/true/false/1/0, default no), `client owner` (yes/no/true/false/1/0, default no; opts the module into client-chosen ownership — see below), `auth users` (comma list). **Unknown keys and malformed lines are parse-and-reject errors** (never silently ignored), so a typo cannot change what a module serves.
|
- **Config grammar** (`fastsyncd.conf`): line-based; an implicit global section first, then `[module]` sections. Keys are case-insensitive, values are trimmed and may be wrapped in one layer of double quotes (`path = "/srv/my dir"`). `#` and `;` at the start of a line (after leading whitespace) are full-line comments; inline comments and `\` continuations are not supported. Lines are bounded (4096 chars). Global keys: `port` (default 873), `motd file` (the daemon sends its bounded, escaped content to a client after the module gate/auth accepts, unless the client passes `--no-motd`), `address` (optional bind address), `max connections` (positive integer cap on concurrent connections, default 100; 0/negative/garbage is a parse error), `auth failure delay` (milliseconds to sleep after a failed authentication, default 500; 0 disables, capped at 5000), `hosts allow` and `hosts deny` (comma- and/or whitespace-separated host access patterns — see the host access control note below). Module keys: `path` (required; the daemon-side authorized root for that module), `read only` (yes/no/true/false/1/0, default no), `client owner` (yes/no/true/false/1/0, default no; opts the module into client-chosen ownership — see below), `auth users` (comma list), `max connections` (optional per-module cap; parsed and stored but **not enforced** — the global cap applies to the whole listener), `hosts allow`/`hosts deny` (per-module host access lists). **Unknown keys and malformed lines are parse-and-reject errors** (never silently ignored), so a typo cannot change what a module serves.
|
||||||
|
- **Host access control (`hosts allow`/`hosts deny`):** both keys accept a comma- and/or whitespace-separated list of patterns and may appear globally and/or per module (multiple config-file lines append; a `--dparam` override replaces). Supported patterns are `*` (match all), an IPv4 or IPv6 literal (`10.0.0.1`, `2001:db8::1`), and an IPv4/IPv6 CIDR (`10.0.0.0/8`, `2001:db8::/32`). Hostname patterns are **not** supported: because the peer is always a numeric address and no reverse DNS is performed, a hostname/glob pattern would silently never match, so it is rejected at load time (fail-closed) instead of being accepted as a dead rule. An IPv4 peer on a dual-stack IPv6 listener is normalized from its `::ffff:a.b.c.d` form so IPv4 patterns match it. rsync-like semantics: a matching `hosts deny` rejects; if any `hosts allow` entries exist, a peer matching none of them is rejected; deny takes precedence over allow. The daemon enforces the global list first, then the selected module's list, **before authentication** in `server_module_gate`, with an audit log line naming the peer, the module and the outcome. The numeric peer address is obtained with `getpeername`+`inet_ntop` (`utils_fd_peer_ip`, handling both address families); when it cannot be obtained a module with any ACL fails closed (refused), while an ACL-free module continues and logs at debug. A malformed pattern (e.g. an out-of-range CIDR prefix) is a parse error at load time.
|
||||||
|
- **Connection cap and auth throttle:** the global `max connections` key (default 100) is plumbed into the listener (`transport_tcp.c`), which rejects a connection once the accept-loop parent's active-child count reaches it; the IPv4/IPv6 peer is logged for every accepted connection. The optional per-module `max connections` key is parsed and validated but **not enforced** (connections are counted in the parent before the client's module is known); the daemon logs a startup warning for any module that sets it. On a failed authentication the per-connection child sleeps the global `auth failure delay` (default 500 ms, 0 disables, capped at 5000) via `nanosleep` before the connection closes, rate-limiting online guessing without delaying a success.
|
||||||
- **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.
|
||||||
|
|||||||
+994
-629
File diff suppressed because it is too large.
Load diff
@@ -338,9 +338,10 @@ static bool basis_oversize_preflight(const Config* config) {
|
|||||||
return false;
|
return false;
|
||||||
DirectoryScanner* scanner =
|
DirectoryScanner* scanner =
|
||||||
directory_scanner_create_with_options(config->send_directory, &prepared.options);
|
directory_scanner_create_with_options(config->send_directory, &prepared.options);
|
||||||
prepared_scanner_destroy(&prepared);
|
if (!scanner) {
|
||||||
if (!scanner)
|
prepared_scanner_destroy(&prepared);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
Chunk* chunk;
|
Chunk* chunk;
|
||||||
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
||||||
@@ -364,7 +365,10 @@ static bool basis_oversize_preflight(const Config* config) {
|
|||||||
}
|
}
|
||||||
if (directory_scanner_failed(scanner) || directory_scanner_had_io_error(scanner))
|
if (directory_scanner_failed(scanner) || directory_scanner_had_io_error(scanner))
|
||||||
ok = false;
|
ok = false;
|
||||||
|
/* The scanner borrows prepared.options' base_filters/hardlinks pointers, so
|
||||||
|
prepared must outlive the scanner. */
|
||||||
directory_scanner_destroy(scanner);
|
directory_scanner_destroy(scanner);
|
||||||
|
prepared_scanner_destroy(&prepared);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,6 +1435,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
|||||||
}
|
}
|
||||||
ProtocolSession session;
|
ProtocolSession session;
|
||||||
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
||||||
|
protocol_session_set_io_timeout(&session, context->config->timeout);
|
||||||
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
||||||
protocol_session_bind(&session);
|
protocol_session_bind(&session);
|
||||||
if (!config_send(client->file_descriptor, context->config)) {
|
if (!config_send(client->file_descriptor, context->config)) {
|
||||||
@@ -1886,10 +1891,13 @@ int send_files(Config* config) {
|
|||||||
if (config->transport == TRANSPORT_TCP)
|
if (config->transport == TRANSPORT_TCP)
|
||||||
log_message(LOG_LEVEL_ERROR, "could not connect to server%s",
|
log_message(LOG_LEVEL_ERROR, "could not connect to server%s",
|
||||||
config->use_tls ? " via TLS" : "");
|
config->use_tls ? " via TLS" : "");
|
||||||
|
if (missing_args)
|
||||||
|
array_list_delete(missing_args);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
ProtocolSession session;
|
ProtocolSession session;
|
||||||
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
|
||||||
|
protocol_session_set_io_timeout(&session, config->timeout);
|
||||||
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
||||||
protocol_session_bind(&session);
|
protocol_session_bind(&session);
|
||||||
int ret = 1;
|
int ret = 1;
|
||||||
|
|||||||
@@ -587,12 +587,16 @@ void directory_scanner_destroy(DirectoryScanner* scanner) {
|
|||||||
|
|
||||||
static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) {
|
static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) {
|
||||||
void** chunk_items = array_list_to_array(chunk_data);
|
void** chunk_items = array_list_to_array(chunk_data);
|
||||||
if (!chunk_items)
|
if (!chunk_items) {
|
||||||
|
array_list_delete(chunk_data);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
}
|
||||||
Chunk* chunk = chunk_create((File**)chunk_items, chunk_data->size);
|
Chunk* chunk = chunk_create((File**)chunk_items, chunk_data->size);
|
||||||
free(chunk_items);
|
free(chunk_items);
|
||||||
if (!chunk)
|
if (!chunk) {
|
||||||
|
array_list_delete(chunk_data);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
}
|
||||||
chunk_data->item_destroyer = NULL;
|
chunk_data->item_destroyer = NULL;
|
||||||
array_list_delete(chunk_data);
|
array_list_delete(chunk_data);
|
||||||
return chunk;
|
return chunk;
|
||||||
|
|||||||
+100
-1
@@ -12,6 +12,7 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
bool receiver_outcomes_append(ReceiverOutcomes* outcomes, unsigned char code) {
|
bool receiver_outcomes_append(ReceiverOutcomes* outcomes, unsigned char code) {
|
||||||
if (!outcomes)
|
if (!outcomes)
|
||||||
@@ -153,6 +154,93 @@ static bool receiver_process_batch(Config* config, int file_descriptor) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Anti-slowloris connection bounds ----
|
||||||
|
* A legitimate transfer either streams data frames continuously or, when it
|
||||||
|
* must pause, sends STATUS_KEEPALIVE so the peer sees the connection is alive.
|
||||||
|
* An attacker can therefore squat on a connection slot indefinitely by sending
|
||||||
|
* only keepalives under the per-message timeout. Two CLOCK_MONOTONIC bounds
|
||||||
|
* defeat that without ever punishing a real transfer:
|
||||||
|
*
|
||||||
|
* MAX_SESSION_IDLE_SEC (1 h): the longest a stream may make no forward
|
||||||
|
* progress. Data/status frames count as progress and refresh the timer;
|
||||||
|
* keepalives do not. One hour is far longer than any real pause between
|
||||||
|
* data frames, yet small enough to reap a slowloris well before the 24 h
|
||||||
|
* session cap.
|
||||||
|
*
|
||||||
|
* MAX_SESSION_WALL_SEC (24 h): an absolute ceiling on one connection's
|
||||||
|
* lifetime as defense-in-depth against a trickle of progress frames that
|
||||||
|
* resets the idle timer just below its limit. Larger than any plausible
|
||||||
|
* single transfer while still bounding resource occupancy.
|
||||||
|
*
|
||||||
|
* Both are wall-clock deltas, so the per-message poll timeout (60 s by default,
|
||||||
|
* or --timeout) can never fool them, and both the single-threaded and the -m
|
||||||
|
* receiver paths (receiver_process_pending) share the same logic. */
|
||||||
|
#define MAX_SESSION_IDLE_SEC 3600u
|
||||||
|
#define MAX_SESSION_WALL_SEC 86400u
|
||||||
|
|
||||||
|
static unsigned int g_max_session_idle_sec = MAX_SESSION_IDLE_SEC;
|
||||||
|
static unsigned int g_max_session_wall_sec = MAX_SESSION_WALL_SEC;
|
||||||
|
|
||||||
|
void receiver_set_time_limits(unsigned int idle_sec, unsigned int wall_sec) {
|
||||||
|
g_max_session_idle_sec = idle_sec;
|
||||||
|
g_max_session_wall_sec = wall_sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void receiver_reset_time_limits(void) {
|
||||||
|
g_max_session_idle_sec = MAX_SESSION_IDLE_SEC;
|
||||||
|
g_max_session_wall_sec = MAX_SESSION_WALL_SEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool receiver_time_limit_exceeded(const struct timespec* session_start,
|
||||||
|
const struct timespec* last_progress,
|
||||||
|
const struct timespec* now) {
|
||||||
|
if (!session_start || !last_progress || !now)
|
||||||
|
return false;
|
||||||
|
if (now->tv_sec - session_start->tv_sec >= (time_t)g_max_session_wall_sec)
|
||||||
|
return true;
|
||||||
|
if (now->tv_sec - last_progress->tv_sec >= (time_t)g_max_session_idle_sec)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A frame proves forward progress only when it cannot be fabricated for free.
|
||||||
|
* KEEPALIVE/ABORT are pure liveness, and CHECK_BATCH/DIR_TIMES may carry zero
|
||||||
|
* entries, so a peer must not be able to hold a connection slot forever by
|
||||||
|
* merely emitting empty frames. */
|
||||||
|
static bool status_counts_as_progress(Status status) {
|
||||||
|
switch (status) {
|
||||||
|
case STATUS_KEEPALIVE:
|
||||||
|
case STATUS_ABORT:
|
||||||
|
case STATUS_CHECK_BATCH:
|
||||||
|
case STATUS_DIR_TIMES:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Refresh the progress timestamp for a forward-moving frame and enforce the
|
||||||
|
* bounds above. Returns false when the connection must be dropped; the
|
||||||
|
* terminal STATUS_ERROR is sent only when the sink owns error reporting (the
|
||||||
|
* -m sink sets send_error=false so the main thread emits exactly one). */
|
||||||
|
static bool receiver_note_status(const struct timespec* session_start,
|
||||||
|
struct timespec* last_progress, Status status, int file_descriptor,
|
||||||
|
const ReceiverSink* sink) {
|
||||||
|
struct timespec now;
|
||||||
|
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
|
||||||
|
now = *last_progress;
|
||||||
|
if (status_counts_as_progress(status))
|
||||||
|
*last_progress = now;
|
||||||
|
if (!receiver_time_limit_exceeded(session_start, last_progress, &now))
|
||||||
|
return true;
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"Receive session exceeded its time bound (idle %us / total %us); aborting connection",
|
||||||
|
g_max_session_idle_sec, g_max_session_wall_sec);
|
||||||
|
if (!sink || sink->send_error)
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink) {
|
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink) {
|
||||||
return receiver_process_pending(config, file_descriptor, sink, NULL);
|
return receiver_process_pending(config, file_descriptor, sink, NULL);
|
||||||
}
|
}
|
||||||
@@ -172,6 +260,15 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
Status status;
|
Status status;
|
||||||
if (!receive_status(file_descriptor, &status))
|
if (!receive_status(file_descriptor, &status))
|
||||||
return -1;
|
return -1;
|
||||||
|
/* Wall-clock (=CLOCK_MONOTONIC) anti-slowloris bookkeeping. session_start is
|
||||||
|
* fixed for the whole connection; last_progress is refreshed by every frame
|
||||||
|
* that is not a keepalive/abort. */
|
||||||
|
struct timespec session_start;
|
||||||
|
struct timespec last_progress;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &session_start);
|
||||||
|
last_progress = session_start;
|
||||||
|
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
||||||
|
return -1;
|
||||||
bool early_delete = config_delete_timing_early(config);
|
bool early_delete = config_delete_timing_early(config);
|
||||||
/* Parked keep-set for the late/commit timing. Every exit path below frees it
|
/* Parked keep-set for the late/commit timing. Every exit path below frees it
|
||||||
exactly once; the only exception is the successful FINISHED handoff, which
|
exactly once; the only exception is the successful FINISHED handoff, which
|
||||||
@@ -271,6 +368,8 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
next_status:
|
next_status:
|
||||||
if (!receive_status(file_descriptor, &status))
|
if (!receive_status(file_descriptor, &status))
|
||||||
goto receive_error;
|
goto receive_error;
|
||||||
|
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
||||||
|
goto fail;
|
||||||
}
|
}
|
||||||
if (status != STATUS_FINISHED) {
|
if (status != STATUS_FINISHED) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
||||||
@@ -353,7 +452,7 @@ static bool receiver_save_file(File* file, void* context_pointer) {
|
|||||||
metadata now and apply it at the end. -O/--omit-dir-times is honored by
|
metadata now and apply it at the end. -O/--omit-dir-times is honored by
|
||||||
dir_time_list_apply's caller (see receiver_send_success_frame). */
|
dir_time_list_apply's caller (see receiver_send_success_frame). */
|
||||||
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||||
context->config->use_metadata && !context->config->omit_dir_times &&
|
dir_times_should_capture(context->config) &&
|
||||||
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "file_receive.h"
|
#include "file_receive.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
typedef bool (*ReceiverFileSink)(File* file, void* context);
|
typedef bool (*ReceiverFileSink)(File* file, void* context);
|
||||||
|
|
||||||
@@ -45,4 +47,22 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
DeleteManifest** pending_manifest);
|
DeleteManifest** pending_manifest);
|
||||||
int receiver_receive_files(Config* config, int file_descriptor);
|
int receiver_receive_files(Config* config, int file_descriptor);
|
||||||
|
|
||||||
|
/* ---- Connection time bounds (anti-slowloris) ----
|
||||||
|
* receiver_process_pending() aborts a connection that makes no forward progress
|
||||||
|
* (only STATUS_KEEPALIVE/STATUS_ABORT frames) beyond a wall-clock idle limit,
|
||||||
|
* and enforces a hard cap on the whole session. Both are CLOCK_MONOTONIC
|
||||||
|
* deltas, independent of the per-message poll deadline, so a 60 s (or
|
||||||
|
* --timeout) receive window can never reset them. Defaults are deliberately
|
||||||
|
* generous (see MAX_SESSION_IDLE_SEC / MAX_SESSION_WALL_SEC in receiver.c). */
|
||||||
|
|
||||||
|
/* Test seam: override the idle/session wall-clock limits (0 = abort on the
|
||||||
|
* next status). Always restore with receiver_reset_time_limits(). */
|
||||||
|
void receiver_set_time_limits(unsigned int idle_sec, unsigned int wall_sec);
|
||||||
|
void receiver_reset_time_limits(void);
|
||||||
|
/* Pure predicate over explicit monotonic timestamps, exposed so the bound is
|
||||||
|
* unit-testable without sleeping. True when either the idle or the overall
|
||||||
|
* session limit has elapsed. */
|
||||||
|
bool receiver_time_limit_exceeded(const struct timespec* session_start,
|
||||||
|
const struct timespec* last_progress, const struct timespec* now);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+339
-192
@@ -23,7 +23,10 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
|
|
||||||
static char* authorized_root;
|
static char* authorized_root;
|
||||||
@@ -67,6 +70,11 @@ typedef struct ModuleGateContext {
|
|||||||
activity (operator --no-super, or a daemon module without the
|
activity (operator --no-super, or a daemon module without the
|
||||||
`client owner = yes` opt-in); -1 when the config's own mode stands. */
|
`client owner = yes` opt-in); -1 when the config's own mode stands. */
|
||||||
int super_mode_override;
|
int super_mode_override;
|
||||||
|
/* Numeric peer address (INET6_ADDRSTRLEN is always enough), filled once by
|
||||||
|
* server_module_gate. has_peer_ip is false when getpeername/inet_ntop could
|
||||||
|
* not classify the peer; an ACL-configured module then fails closed. */
|
||||||
|
bool has_peer_ip;
|
||||||
|
char peer_ip[INET6_ADDRSTRLEN];
|
||||||
} ModuleGateContext;
|
} ModuleGateContext;
|
||||||
|
|
||||||
/* Server half of the SCRAM challenge/response (A7 remediation, protocol
|
/* Server half of the SCRAM challenge/response (A7 remediation, protocol
|
||||||
@@ -251,6 +259,220 @@ static bool configure_authorization(const char* root) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Discriminates the outcome of the A7 auth gate so the dispatcher can map it
|
||||||
|
* back to the config_receive_with_validate contract: accepted (including
|
||||||
|
* "module needs no auth"), a config-level refusal carrying an error string, or
|
||||||
|
* a handshake that already wrote its own terminal status frame. */
|
||||||
|
typedef enum {
|
||||||
|
MODULE_AUTH_ACCEPTED = 0,
|
||||||
|
MODULE_AUTH_REFUSED,
|
||||||
|
MODULE_AUTH_TERMINATED,
|
||||||
|
} ModuleAuthResult;
|
||||||
|
|
||||||
|
/* Looks up the daemon module selected by the client's config frame and rejects
|
||||||
|
* a `read only` one (every FastSync network transfer writes; there is no
|
||||||
|
* read-only wire operation yet). Returns the module, or NULL with *error set
|
||||||
|
* to the caller-facing rejection message. */
|
||||||
|
static const DaemonModule* module_gate_lookup_module(const Config* config, const char** error) {
|
||||||
|
const DaemonModule* module = daemon_conf_find_module(g_daemon_conf, config->module);
|
||||||
|
if (module == NULL) {
|
||||||
|
char* escaped_module = output_escape(config->module, config->eight_bit_output);
|
||||||
|
log_message(LOG_LEVEL_ERROR, "unknown daemon module '%s' requested",
|
||||||
|
escaped_module ? escaped_module : "<allocation failed>");
|
||||||
|
free(escaped_module);
|
||||||
|
*error = "requested daemon module does not exist";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (module->read_only) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s' is read only; refusing write transfer",
|
||||||
|
config->module);
|
||||||
|
*error = "requested daemon module is read only";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-module client-chosen ownership / super-user policy (P7 Wave E hardening):
|
||||||
|
* a daemon module refuses EVERY ownership-affecting request (--numeric-ids,
|
||||||
|
* --chown, --usermap/--groupmap, --fake-super, --copy-as, explicit --super)
|
||||||
|
* unless the operator opted THIS module in with `client owner = yes`.
|
||||||
|
* Otherwise any client could force arbitrary ownership inside the module root.
|
||||||
|
* The ownership check is evaluated against the ORIGINAL config so an explicit
|
||||||
|
* --super is refused even when an operator --no-super veto already forced the
|
||||||
|
* effective copy to OFF (the veto must not silently convert a refusal into an
|
||||||
|
* accept); when no ownership flag is present, super-user DEVICE activities are
|
||||||
|
* forced off for this connection instead. Returns an error string on refusal,
|
||||||
|
* NULL on acceptance. */
|
||||||
|
static const char* module_gate_check_ownership(const Config* config, const DaemonModule* module,
|
||||||
|
ModuleGateContext* gate_ctx) {
|
||||||
|
if (module->client_owner)
|
||||||
|
return NULL;
|
||||||
|
/* Ownership: refuse the whole transfer up front (a clear failure). */
|
||||||
|
if (identity_ownership_requested(config)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"daemon module '%s' refuses client-chosen ownership/super-user activities "
|
||||||
|
"(no `client owner = yes` opt-in); refusing",
|
||||||
|
config->module);
|
||||||
|
return "client-chosen ownership is not permitted by this daemon module";
|
||||||
|
}
|
||||||
|
/* Super-user DEVICE activities (char/block mknod and --write-devices) are
|
||||||
|
permitted under the default AUTO mode, so without this override a root
|
||||||
|
daemon would still let a non-opted module create arbitrary device nodes
|
||||||
|
and write raw devices. Force them off for this connection: those entries
|
||||||
|
are skipped (never mknod'ed) while an ordinary `-a` push still succeeds
|
||||||
|
without device nodes, matching the operator's least-privilege choice.
|
||||||
|
The operator-level --no-super veto is already folded into this. */
|
||||||
|
if (gate_ctx)
|
||||||
|
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Online-guessing throttle: sleep the configured `auth failure delay`
|
||||||
|
* milliseconds after a failed authentication. Runs in the per-connection
|
||||||
|
* forked child, so it never blocks the accept loop or another connection. 0
|
||||||
|
* disables it; the parser already caps it at DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS.
|
||||||
|
* Resumes after EINTR so a signal cannot cut the delay short. */
|
||||||
|
static void daemon_auth_failure_delay(void) {
|
||||||
|
if (!g_daemon_conf || g_daemon_conf->global.auth_failure_delay_ms <= 0)
|
||||||
|
return;
|
||||||
|
int ms = g_daemon_conf->global.auth_failure_delay_ms;
|
||||||
|
struct timespec delay;
|
||||||
|
delay.tv_sec = ms / 1000;
|
||||||
|
delay.tv_nsec = (long)(ms % 1000) * 1000000L;
|
||||||
|
while (nanosleep(&delay, &delay) != 0 && errno == EINTR)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Host access control (global then per-module). A configured list makes an
|
||||||
|
* unprovable peer fail closed. Deny always takes precedence over allow, and a
|
||||||
|
* non-empty allow list rejects a peer that matches none of its entries. The
|
||||||
|
* audit line names the peer, the module and the outcome. Returns an
|
||||||
|
* error string on refusal, NULL on acceptance. */
|
||||||
|
static const char* module_gate_check_hosts(const Config* config, const DaemonModule* module,
|
||||||
|
ModuleGateContext* gate_ctx) {
|
||||||
|
bool global_restricted = daemon_hosts_restricted(
|
||||||
|
g_daemon_conf->global.hosts_allow, g_daemon_conf->global.hosts_allow_count,
|
||||||
|
g_daemon_conf->global.hosts_deny, g_daemon_conf->global.hosts_deny_count);
|
||||||
|
bool module_restricted = daemon_hosts_restricted(module->hosts_allow, module->hosts_allow_count,
|
||||||
|
module->hosts_deny, module->hosts_deny_count);
|
||||||
|
if (!global_restricted && !module_restricted)
|
||||||
|
return NULL;
|
||||||
|
if (!gate_ctx || !gate_ctx->has_peer_ip) {
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"daemon module '%s': cannot determine peer address with host ACLs configured; "
|
||||||
|
"refusing (fail closed)",
|
||||||
|
config->module);
|
||||||
|
return "cannot verify the client host against host access controls";
|
||||||
|
}
|
||||||
|
const char* peer = gate_ctx->peer_ip;
|
||||||
|
if (global_restricted && !daemon_hosts_allowed(peer, g_daemon_conf->global.hosts_allow,
|
||||||
|
g_daemon_conf->global.hosts_allow_count,
|
||||||
|
g_daemon_conf->global.hosts_deny,
|
||||||
|
g_daemon_conf->global.hosts_deny_count)) {
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"daemon module '%s': peer %s denied by global 'hosts allow'/'hosts deny'; "
|
||||||
|
"refusing",
|
||||||
|
config->module, peer);
|
||||||
|
return "client host is not permitted by this daemon";
|
||||||
|
}
|
||||||
|
if (module_restricted &&
|
||||||
|
!daemon_hosts_allowed(peer, module->hosts_allow, module->hosts_allow_count,
|
||||||
|
module->hosts_deny, module->hosts_deny_count)) {
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"daemon module '%s': peer %s denied by module 'hosts allow'/'hosts deny'; "
|
||||||
|
"refusing",
|
||||||
|
config->module, peer);
|
||||||
|
return "client host is not permitted by this daemon module";
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A7 auth gate: runs the SCRAM challenge/response for an auth-required module
|
||||||
|
* BEFORE the module root is installed and before any data moves. Returns
|
||||||
|
* MODULE_AUTH_ACCEPTED when the module needs no auth or the handshake succeeds,
|
||||||
|
* MODULE_AUTH_REFUSED with *error set on a config-level rejection, or
|
||||||
|
* MODULE_AUTH_TERMINATED when the handshake already wrote a terminal status. */
|
||||||
|
static ModuleAuthResult module_gate_authenticate(const Config* config, const DaemonModule* module,
|
||||||
|
ModuleGateContext* gate_ctx, const char** error) {
|
||||||
|
if (module->auth_user_count == 0)
|
||||||
|
return MODULE_AUTH_ACCEPTED;
|
||||||
|
/* Fail closed: no store -> refuse (server misconfiguration, STATUS_ERROR). */
|
||||||
|
if (g_credentials == NULL) {
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"daemon module '%s' requires authentication but no credential store is "
|
||||||
|
"configured (--password-file/--early-input); refusing",
|
||||||
|
config->module);
|
||||||
|
*error = "requested daemon module requires authentication and no credential "
|
||||||
|
"store is configured";
|
||||||
|
return MODULE_AUTH_REFUSED;
|
||||||
|
}
|
||||||
|
/* Transport policy (A7-3/S1): an auth-required module only accepts
|
||||||
|
* credentials over (a) an encrypted, verified TLS connection whose client
|
||||||
|
* certificate matches --client-cn, or (b) an actual PLAINTEXT connection
|
||||||
|
* from a loopback peer that the operator explicitly opted into with
|
||||||
|
* --allow-unauthenticated. A remote plaintext peer, an un-flagged loopback
|
||||||
|
* plaintext peer, and a loopback TLS peer whose certificate does not match
|
||||||
|
* --client-cn are all refused HERE, before the challenge is sent, so an
|
||||||
|
* unverified client never receives a nonce: the loopback allowance requires
|
||||||
|
* !gate_ctx->ssl, so --tls + --allow-unauthenticated can never be used to
|
||||||
|
* bypass the client-CN check. The operator flag never permits REMOTE
|
||||||
|
* plaintext auth: remote peers still require verified TLS regardless. */
|
||||||
|
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 = allow_unauthenticated && gate_ctx && !gate_ctx->ssl && 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 an opted-in loopback plaintext transport); refusing",
|
||||||
|
config->module);
|
||||||
|
*error = "daemon module requires authentication over an encrypted, verified TLS "
|
||||||
|
"connection";
|
||||||
|
return MODULE_AUTH_REFUSED;
|
||||||
|
}
|
||||||
|
/* Belt-and-braces: the transport policy above already guarantees a context
|
||||||
|
* with a usable socket (verified TLS implies a live SSL object and loopback
|
||||||
|
* allowance requires gate_ctx->fd >= 0), so this is unreachable today; keep
|
||||||
|
* the guard so the handshake can never be driven over an invalid fd. */
|
||||||
|
if (!gate_ctx || gate_ctx->fd < 0) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available", config->module);
|
||||||
|
*error = "authentication failed for the requested daemon module";
|
||||||
|
return MODULE_AUTH_REFUSED;
|
||||||
|
}
|
||||||
|
/* The handshake writes exactly one terminal status on failure and signals so
|
||||||
|
* via MODULE_AUTH_TERMINATED; the username may be logged (never the password
|
||||||
|
* or any derived proof). */
|
||||||
|
if (!server_auth_handshake(gate_ctx->fd, config, module)) {
|
||||||
|
const char* peer = gate_ctx->has_peer_ip ? gate_ctx->peer_ip : "unknown";
|
||||||
|
char* escaped_user =
|
||||||
|
config->auth_user ? output_escape(config->auth_user, config->eight_bit_output) : NULL;
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"daemon module '%s': authentication failed for user '%s' from %s; refusing",
|
||||||
|
config->module, escaped_user ? escaped_user : "(none)", peer);
|
||||||
|
free(escaped_user);
|
||||||
|
/* Rate-limit online guessing per connection (no delay on success). */
|
||||||
|
daemon_auth_failure_delay();
|
||||||
|
return MODULE_AUTH_TERMINATED;
|
||||||
|
}
|
||||||
|
char* escaped_user = output_escape(config->auth_user, config->eight_bit_output);
|
||||||
|
log_message(LOG_LEVEL_INFO, "daemon module '%s': user '%s' from %s authenticated", config->module,
|
||||||
|
escaped_user ? escaped_user : "<allocation failed>",
|
||||||
|
gate_ctx->has_peer_ip ? gate_ctx->peer_ip : "unknown");
|
||||||
|
free(escaped_user);
|
||||||
|
return MODULE_AUTH_ACCEPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Installs the module's configured path as the connection's authorized root.
|
||||||
|
* Returns an error string when the root is unusable, NULL on success. */
|
||||||
|
static const char* module_gate_install_root(const Config* config, const DaemonModule* module) {
|
||||||
|
if (!configure_authorization(module->path)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s' path '%s' is not usable", config->module,
|
||||||
|
module->path ? module->path : "(null)");
|
||||||
|
return "requested daemon module root is not usable";
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* Config-frame gate (runs inside config_receive_with_validate, BEFORE the
|
/* Config-frame gate (runs inside config_receive_with_validate, BEFORE the
|
||||||
* STATUS_OK ack, so a rejected connection is refused at the config handshake
|
* STATUS_OK ack, so a rejected connection is refused at the config handshake
|
||||||
* and no file data is ever exchanged).
|
* and no file data is ever exchanged).
|
||||||
@@ -324,115 +546,36 @@ static const char* server_module_gate(const Config* config, void* context) {
|
|||||||
return "daemon connection did not select a module (expected a "
|
return "daemon connection did not select a module (expected a "
|
||||||
"host::module/path destination)";
|
"host::module/path destination)";
|
||||||
|
|
||||||
const DaemonModule* module = daemon_conf_find_module(g_daemon_conf, config->module);
|
const char* error = NULL;
|
||||||
if (module == NULL) {
|
const DaemonModule* module = module_gate_lookup_module(config, &error);
|
||||||
char* escaped_module = output_escape(config->module, config->eight_bit_output);
|
if (!module)
|
||||||
log_message(LOG_LEVEL_ERROR, "unknown daemon module '%s' requested",
|
return error;
|
||||||
escaped_module ? escaped_module : "<allocation failed>");
|
/* Resolve the peer once, before any auth or ownership work, so the host ACL
|
||||||
free(escaped_module);
|
* and the audit lines all use the same address. A module with ACLs fails
|
||||||
return "requested daemon module does not exist";
|
* closed when the peer cannot be classified; an ACL-free module continues
|
||||||
|
* (the accept loop still logged the address). */
|
||||||
|
if (gate_ctx) {
|
||||||
|
gate_ctx->has_peer_ip =
|
||||||
|
utils_fd_peer_ip(gate_ctx->fd, gate_ctx->peer_ip, sizeof(gate_ctx->peer_ip));
|
||||||
|
if (!gate_ctx->has_peer_ip)
|
||||||
|
log_message(LOG_LEVEL_DEBUG, "daemon module '%s': peer address unavailable", config->module);
|
||||||
}
|
}
|
||||||
if (module->read_only) {
|
error = module_gate_check_hosts(config, module, gate_ctx);
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s' is read only; refusing write transfer",
|
if (error)
|
||||||
config->module);
|
return error;
|
||||||
return "requested daemon module is read only";
|
error = module_gate_check_ownership(config, module, gate_ctx);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
switch (module_gate_authenticate(config, module, gate_ctx, &error)) {
|
||||||
|
case MODULE_AUTH_REFUSED:
|
||||||
|
return error;
|
||||||
|
case MODULE_AUTH_TERMINATED:
|
||||||
|
return CONFIG_VALIDATE_ALREADY_TERMINATED;
|
||||||
|
case MODULE_AUTH_ACCEPTED:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
/* Client-chosen ownership / super-user policy (P7 Wave E hardening): a daemon
|
/* accepted; the authorized root is now the module's path */
|
||||||
module refuses EVERY ownership-affecting request (--numeric-ids, --chown,
|
return module_gate_install_root(config, module);
|
||||||
--usermap/--groupmap, --fake-super, --copy-as, explicit --super) unless the
|
|
||||||
operator opted THIS module in with `client owner = yes`. Otherwise any
|
|
||||||
client could force arbitrary ownership inside the module root. The
|
|
||||||
standalone/SSH server has a single operator-authorized root and keeps
|
|
||||||
honoring these. */
|
|
||||||
if (!module->client_owner) {
|
|
||||||
/* Ownership: refuse the whole transfer up front (a clear failure).
|
|
||||||
Evaluated against the ORIGINAL config so an explicit --super is refused
|
|
||||||
even when an operator --no-super veto already forced the effective copy
|
|
||||||
to OFF (the veto must not silently convert a refusal into an accept). */
|
|
||||||
if (identity_ownership_requested(config)) {
|
|
||||||
log_message(LOG_LEVEL_ERROR,
|
|
||||||
"daemon module '%s' refuses client-chosen ownership/super-user activities "
|
|
||||||
"(no `client owner = yes` opt-in); refusing",
|
|
||||||
config->module);
|
|
||||||
return "client-chosen ownership is not permitted by this daemon module";
|
|
||||||
}
|
|
||||||
/* Super-user DEVICE activities (char/block mknod and --write-devices) are
|
|
||||||
permitted under the default AUTO mode, so without this override a root
|
|
||||||
daemon would still let a non-opted module create arbitrary device nodes
|
|
||||||
and write raw devices. Force them off for this connection: those entries
|
|
||||||
are skipped (never mknod'ed) while an ordinary `-a` push still succeeds
|
|
||||||
without device nodes, matching the operator's least-privilege choice.
|
|
||||||
The operator-level --no-super veto is already folded into this. */
|
|
||||||
if (gate_ctx)
|
|
||||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
|
||||||
}
|
|
||||||
if (module->auth_user_count > 0) {
|
|
||||||
/* Auth-required module (A7, protocol 2.19.0): run the SCRAM challenge/
|
|
||||||
* response BEFORE the module root is installed and before any data moves.
|
|
||||||
* Fail closed: no store -> refuse (server misconfiguration, STATUS_ERROR);
|
|
||||||
* a handshake that fails before the success response writes exactly one
|
|
||||||
* STATUS_AUTH_FAILED before signalling ALREADY_TERMINATED (a failure while
|
|
||||||
* writing the success signature instead just drops the broken connection).
|
|
||||||
* The username may be logged (never the password or any derived proof). */
|
|
||||||
if (g_credentials == NULL) {
|
|
||||||
log_message(LOG_LEVEL_ERROR,
|
|
||||||
"daemon module '%s' requires authentication but no credential store is "
|
|
||||||
"configured (--password-file/--early-input); refusing",
|
|
||||||
config->module);
|
|
||||||
return "requested daemon module requires authentication and no credential "
|
|
||||||
"store is configured";
|
|
||||||
}
|
|
||||||
/* Transport policy (A7-3/S1): an auth-required module only accepts
|
|
||||||
* credentials over (a) an encrypted, verified TLS connection whose client
|
|
||||||
* certificate matches --client-cn, or (b) an actual PLAINTEXT connection
|
|
||||||
* from a loopback peer that the operator explicitly opted into with
|
|
||||||
* --allow-unauthenticated. A remote plaintext peer, an un-flagged loopback
|
|
||||||
* plaintext peer, and a loopback TLS peer whose certificate does not match
|
|
||||||
* --client-cn are all refused HERE, before the challenge is sent, so an
|
|
||||||
* unverified client never receives a nonce: the loopback allowance requires
|
|
||||||
* !gate_ctx->ssl, so --tls + --allow-unauthenticated can never be used to
|
|
||||||
* bypass the client-CN check. The operator flag never permits REMOTE
|
|
||||||
* plaintext auth: remote peers still require verified TLS regardless. */
|
|
||||||
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 = allow_unauthenticated && gate_ctx && !gate_ctx->ssl && 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 an opted-in loopback plaintext transport); refusing",
|
|
||||||
config->module);
|
|
||||||
return "daemon module requires authentication over an encrypted, verified TLS "
|
|
||||||
"connection";
|
|
||||||
}
|
|
||||||
/* Belt-and-braces: the transport policy above already guarantees a context
|
|
||||||
* with a usable socket (verified TLS implies a live SSL object and loopback
|
|
||||||
* allowance requires gate_ctx->fd >= 0), so this is unreachable today; keep
|
|
||||||
* the guard so the handshake can never be driven over an invalid fd. */
|
|
||||||
if (!gate_ctx || gate_ctx->fd < 0) {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available",
|
|
||||||
config->module);
|
|
||||||
return "authentication failed for the requested daemon module";
|
|
||||||
}
|
|
||||||
if (!server_auth_handshake(gate_ctx->fd, config, module)) {
|
|
||||||
char* escaped_user =
|
|
||||||
config->auth_user ? output_escape(config->auth_user, config->eight_bit_output) : NULL;
|
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': authentication failed for user '%s'",
|
|
||||||
config->module, escaped_user ? escaped_user : "(none)");
|
|
||||||
free(escaped_user);
|
|
||||||
return CONFIG_VALIDATE_ALREADY_TERMINATED;
|
|
||||||
}
|
|
||||||
char* escaped_user = output_escape(config->auth_user, config->eight_bit_output);
|
|
||||||
log_message(LOG_LEVEL_INFO, "daemon module '%s': user '%s' authenticated", config->module,
|
|
||||||
escaped_user ? escaped_user : "<allocation failed>");
|
|
||||||
free(escaped_user);
|
|
||||||
}
|
|
||||||
if (!configure_authorization(module->path)) {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s' path '%s' is not usable", config->module,
|
|
||||||
module->path ? module->path : "(null)");
|
|
||||||
return "requested daemon module root is not usable";
|
|
||||||
}
|
|
||||||
return NULL; /* accepted; authorized root is now the module's path */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handler(int file_descriptor) {
|
void handler(int file_descriptor) {
|
||||||
@@ -445,12 +588,18 @@ void handler(int file_descriptor) {
|
|||||||
gate_ctx.ssl = ssl;
|
gate_ctx.ssl = ssl;
|
||||||
gate_ctx.fd = file_descriptor;
|
gate_ctx.fd = file_descriptor;
|
||||||
gate_ctx.super_mode_override = -1;
|
gate_ctx.super_mode_override = -1;
|
||||||
Config* config = config_receive_with_validate(file_descriptor, server_module_gate, &gate_ctx);
|
gate_ctx.has_peer_ip = false;
|
||||||
|
gate_ctx.peer_ip[0] = '\0';
|
||||||
|
/* All teardown state starts empty so the single `done` epilogue is safe to
|
||||||
|
* reach from any error path (including before the config frame arrives). */
|
||||||
|
Config* config = NULL;
|
||||||
|
PipelineContextReceiver* context = NULL;
|
||||||
|
char* joined_destination = NULL;
|
||||||
|
bool charset_ready = false;
|
||||||
|
config = config_receive_with_validate(file_descriptor, server_module_gate, &gate_ctx);
|
||||||
if (config == NULL) {
|
if (config == NULL) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to receive config");
|
log_message(LOG_LEVEL_ERROR, "Failed to receive config");
|
||||||
close(file_descriptor);
|
goto done;
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
/* Apply the super-mode veto the gate decided on (operator --no-super, or a
|
/* Apply the super-mode veto the gate decided on (operator --no-super, or a
|
||||||
* daemon module without the `client owner = yes` opt-in) exactly once, so
|
* daemon module without the `client owner = yes` opt-in) exactly once, so
|
||||||
@@ -458,27 +607,25 @@ void handler(int file_descriptor) {
|
|||||||
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
|
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
|
||||||
* received config. */
|
* received config. */
|
||||||
if (gate_ctx.super_mode_override != -1)
|
if (gate_ctx.super_mode_override != -1)
|
||||||
config->super_mode = gate_ctx.super_mode_override;
|
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
|
||||||
protocol_set_8_bit_output(config->eight_bit_output);
|
protocol_set_8_bit_output(config->eight_bit_output);
|
||||||
|
/* Server-side per-message protocol deadline for every frame from here on.
|
||||||
|
* `timeout` is not serialized, so this is the server's own config (the server
|
||||||
|
* has no --timeout CLI and defaults it to 0): the built-in 60 s window stays
|
||||||
|
* in effect. A client's --timeout tightens only that client's own protocol
|
||||||
|
* I/O and the server's socket read/write timeout is the transport default. */
|
||||||
|
protocol_session_set_io_timeout(&session, config->timeout);
|
||||||
if (!authorized_root) {
|
if (!authorized_root) {
|
||||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!allow_unauthenticated && ssl == NULL) {
|
if (!allow_unauthenticated && ssl == NULL) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected unauthenticated plaintext connection");
|
log_message(LOG_LEVEL_ERROR, "Rejected unauthenticated plaintext connection");
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (ssl && required_client_cn && !tls_client_identity_allowed(ssl)) {
|
if (ssl && required_client_cn && !tls_client_identity_allowed(ssl)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected TLS client with unauthorized identity");
|
log_message(LOG_LEVEL_ERROR, "Rejected TLS client with unauthorized identity");
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
/* Daemon mode: the module's root is the authorized root (installed by
|
/* Daemon mode: the module's root is the authorized root (installed by
|
||||||
server_module_gate), and the client's destination is a MODULE-RELATIVE
|
server_module_gate), and the client's destination is a MODULE-RELATIVE
|
||||||
@@ -488,13 +635,9 @@ void handler(int file_descriptor) {
|
|||||||
if (g_daemon_conf && config->receive_root_directory && config->receive_root_directory[0] == '/') {
|
if (g_daemon_conf && config->receive_root_directory && config->receive_root_directory[0] == '/') {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected absolute daemon destination (must be relative to the "
|
log_message(LOG_LEVEL_ERROR, "Rejected absolute daemon destination (must be relative to the "
|
||||||
"selected module root)");
|
"selected module root)");
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
char* destination = config->receive_root_directory;
|
char* destination = config->receive_root_directory;
|
||||||
char* joined_destination = NULL;
|
|
||||||
if (destination && destination[0] != '/')
|
if (destination && destination[0] != '/')
|
||||||
joined_destination = path_cat(authorized_root, destination);
|
joined_destination = path_cat(authorized_root, destination);
|
||||||
if (joined_destination)
|
if (joined_destination)
|
||||||
@@ -503,19 +646,16 @@ void handler(int file_descriptor) {
|
|||||||
!path_is_within(authorized_root, destination)) {
|
!path_is_within(authorized_root, destination)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root");
|
log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root");
|
||||||
free(joined_destination);
|
free(joined_destination);
|
||||||
config_delete(config);
|
joined_destination = NULL;
|
||||||
close(file_descriptor);
|
goto done;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (joined_destination) {
|
if (joined_destination) {
|
||||||
free(config->receive_root_directory);
|
free(config->receive_root_directory);
|
||||||
config->receive_root_directory = joined_destination;
|
config->receive_root_directory = joined_destination;
|
||||||
|
joined_destination = NULL;
|
||||||
}
|
}
|
||||||
if (!config->receive_root_directory) {
|
if (!config->receive_root_directory) {
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
config->use_delete = config->use_delete && allow_delete;
|
config->use_delete = config->use_delete && allow_delete;
|
||||||
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
|
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
|
||||||
@@ -524,13 +664,13 @@ void handler(int file_descriptor) {
|
|||||||
any) may override the local charset; a spec the client is known to have
|
any) may override the local charset; a spec the client is known to have
|
||||||
validated cannot fail here unless the server's override names an
|
validated cannot fail here unless the server's override names an
|
||||||
unsupported charset. */
|
unsupported charset. */
|
||||||
if (config->iconv_spec && !charset_wire_init_receiver(config->iconv_spec, server_iconv_spec)) {
|
if (config->iconv_spec) {
|
||||||
log_message(LOG_LEVEL_ERROR,
|
if (!charset_wire_init_receiver(config->iconv_spec, server_iconv_spec)) {
|
||||||
"--iconv: unsupported charset conversion requested (LOCAL[,REMOTE])");
|
log_message(LOG_LEVEL_ERROR,
|
||||||
config_delete(config);
|
"--iconv: unsupported charset conversion requested (LOCAL[,REMOTE])");
|
||||||
close(file_descriptor);
|
goto done;
|
||||||
protocol_session_unbind();
|
}
|
||||||
return;
|
charset_ready = true;
|
||||||
}
|
}
|
||||||
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
|
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
|
||||||
deletion and stays gated by the same --allow-delete server policy. When
|
deletion and stays gated by the same --allow-delete server policy. When
|
||||||
@@ -545,10 +685,7 @@ void handler(int file_descriptor) {
|
|||||||
log_message(LOG_LEVEL_ERROR, "destination root is not available: %s",
|
log_message(LOG_LEVEL_ERROR, "destination root is not available: %s",
|
||||||
escaped_root ? escaped_root : "<allocation failed>");
|
escaped_root ? escaped_root : "<allocation failed>");
|
||||||
free(escaped_root);
|
free(escaped_root);
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
/* A --delay-updates transfer stages under a private 0700 directory inside
|
/* A --delay-updates transfer stages under a private 0700 directory inside
|
||||||
the receive root. Create it up front (wiping leftovers of any previously
|
the receive root. Create it up front (wiping leftovers of any previously
|
||||||
@@ -557,11 +694,7 @@ void handler(int file_descriptor) {
|
|||||||
config->delay_context = delay_updates_context_create(config->receive_root_directory);
|
config->delay_context = delay_updates_context_create(config->receive_root_directory);
|
||||||
if (!config->delay_context || !delay_updates_prepare(config->delay_context)) {
|
if (!config->delay_context || !delay_updates_prepare(config->delay_context)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to initialize --delay-updates staging area");
|
log_message(LOG_LEVEL_ERROR, "Failed to initialize --delay-updates staging area");
|
||||||
delay_updates_cleanup(config->delay_context);
|
goto done;
|
||||||
config_delete(config);
|
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Preserve the negotiated identity policy for the fd-relative ownership
|
/* Preserve the negotiated identity policy for the fd-relative ownership
|
||||||
@@ -571,10 +704,7 @@ void handler(int file_descriptor) {
|
|||||||
rather than silently applying the wrong ownership policy. */
|
rather than silently applying the wrong ownership policy. */
|
||||||
if (!identity_set_active(config)) {
|
if (!identity_set_active(config)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to activate identity policy");
|
log_message(LOG_LEVEL_ERROR, "Failed to activate identity policy");
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
/* Persist the negotiated --keep-dirlinks policy once, here at config-accept,
|
/* Persist the negotiated --keep-dirlinks policy once, here at config-accept,
|
||||||
before any multithreaded receiver/writer threads are spawned, so the
|
before any multithreaded receiver/writer threads are spawned, so the
|
||||||
@@ -605,38 +735,26 @@ void handler(int file_descriptor) {
|
|||||||
if (!motd_send(file_descriptor, motd ? motd : "")) {
|
if (!motd_send(file_descriptor, motd ? motd : "")) {
|
||||||
free(motd);
|
free(motd);
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to send daemon MOTD");
|
log_message(LOG_LEVEL_ERROR, "Failed to send daemon MOTD");
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
identity_clear_active();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
free(motd);
|
free(motd);
|
||||||
}
|
}
|
||||||
if (config->use_multithreading) {
|
if (config->use_multithreading) {
|
||||||
Queue* q = queue_create(100, file_destroy);
|
Queue* q = queue_create(100, file_destroy);
|
||||||
if (q == NULL) {
|
if (q == NULL)
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
context = pipeline_context_receiver_create(config, q, file_descriptor, ssl);
|
||||||
protocol_session_unbind();
|
|
||||||
identity_clear_active();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PipelineContextReceiver* context =
|
|
||||||
pipeline_context_receiver_create(config, q, file_descriptor, ssl);
|
|
||||||
if (context == NULL) {
|
if (context == NULL) {
|
||||||
queue_destroy(q);
|
queue_destroy(q);
|
||||||
config_delete(config);
|
goto done;
|
||||||
close(file_descriptor);
|
|
||||||
protocol_session_unbind();
|
|
||||||
identity_clear_active();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
protocol_session_set_max_alloc(&context->session, config->max_alloc);
|
protocol_session_set_max_alloc(&context->session, config->max_alloc);
|
||||||
|
protocol_session_set_io_timeout(&context->session, config->timeout);
|
||||||
atomic_store(&context->session.total_allocated_bytes,
|
atomic_store(&context->session.total_allocated_bytes,
|
||||||
atomic_load(&session.total_allocated_bytes));
|
atomic_load(&session.total_allocated_bytes));
|
||||||
pipeline_context_receiver_set_queue_byte_limit(context, RECEIVER_QUEUE_MAX_BYTES);
|
pipeline_context_receiver_set_queue_byte_limit(context, RECEIVER_QUEUE_MAX_BYTES);
|
||||||
thrd_t receiver, writer;
|
thrd_t receiver = {0};
|
||||||
|
thrd_t writer = {0};
|
||||||
bool receiver_created = thrd_create(&receiver, receive_thread, context) == thrd_success;
|
bool receiver_created = thrd_create(&receiver, receive_thread, context) == thrd_success;
|
||||||
bool writer_created = false;
|
bool writer_created = false;
|
||||||
if (receiver_created)
|
if (receiver_created)
|
||||||
@@ -649,17 +767,19 @@ void handler(int file_descriptor) {
|
|||||||
cnd_broadcast(&context->condition_not_full);
|
cnd_broadcast(&context->condition_not_full);
|
||||||
cnd_broadcast(&context->condition_not_empty);
|
cnd_broadcast(&context->condition_not_empty);
|
||||||
mtx_unlock(&context->mutex);
|
mtx_unlock(&context->mutex);
|
||||||
close(file_descriptor);
|
/* Unblock a worker parked in socket I/O without closing the fd (the
|
||||||
|
* child owns the single close). shutdown() only affects sockets; for
|
||||||
|
* the --stdio pipe the receiver's per-message poll timeout still
|
||||||
|
* bounds the join, so do nothing there rather than close a descriptor
|
||||||
|
* another thread may still be using. */
|
||||||
|
struct stat fd_stat;
|
||||||
|
if (fstat(file_descriptor, &fd_stat) == 0 && S_ISSOCK(fd_stat.st_mode))
|
||||||
|
shutdown(file_descriptor, SHUT_RDWR);
|
||||||
thrd_join(receiver, NULL);
|
thrd_join(receiver, NULL);
|
||||||
} else {
|
|
||||||
close(file_descriptor);
|
|
||||||
}
|
}
|
||||||
if (writer_created)
|
if (writer_created)
|
||||||
thrd_join(writer, NULL);
|
thrd_join(writer, NULL);
|
||||||
pipeline_context_receiver_destroy(context);
|
goto done;
|
||||||
protocol_session_unbind();
|
|
||||||
identity_clear_active();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
int receiver_result;
|
int receiver_result;
|
||||||
int writer_result;
|
int writer_result;
|
||||||
@@ -703,21 +823,36 @@ void handler(int file_descriptor) {
|
|||||||
} else {
|
} else {
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
}
|
}
|
||||||
if (!transfer_ok) {
|
if (!transfer_ok)
|
||||||
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
||||||
if (config->delay_updates && config->delay_context)
|
|
||||||
delay_updates_cleanup(config->delay_context);
|
|
||||||
}
|
|
||||||
pipeline_context_receiver_destroy(context);
|
|
||||||
} else {
|
} else {
|
||||||
if (receiver_receive_files(config, file_descriptor) != 0)
|
if (receiver_receive_files(config, file_descriptor) != 0)
|
||||||
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
||||||
config_delete(config);
|
|
||||||
}
|
}
|
||||||
protocol_session_unbind();
|
|
||||||
|
done:
|
||||||
|
/* Single cleanup epilogue: every error path jumps here, so the iconv
|
||||||
|
* receiver conversion is released, the identity snapshot cleared, the
|
||||||
|
* protocol session unbound and the config freed exactly once. The
|
||||||
|
* connection fd is deliberately NOT closed here -- the child functions own
|
||||||
|
* its single close (plain_child_fn / tls_child_fn), and the --stdio call
|
||||||
|
* site must leave stdin/stdout open. */
|
||||||
|
if (charset_ready)
|
||||||
|
charset_wire_free();
|
||||||
|
/* The delay-updates staging tree is released by config_delete (which the
|
||||||
|
branch below always reaches), so it is cleaned exactly once. */
|
||||||
identity_clear_active();
|
identity_clear_active();
|
||||||
charset_wire_free();
|
protocol_session_unbind();
|
||||||
close(file_descriptor);
|
if (context != NULL) {
|
||||||
|
/* context owns both the config and the queue it was created with. */
|
||||||
|
pipeline_context_receiver_destroy(context);
|
||||||
|
context = NULL;
|
||||||
|
config = NULL;
|
||||||
|
} else {
|
||||||
|
config_delete(config);
|
||||||
|
config = NULL;
|
||||||
|
}
|
||||||
|
free(joined_destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef FASTSYNC_SERVER_AS_LIB
|
#ifndef FASTSYNC_SERVER_AS_LIB
|
||||||
@@ -744,7 +879,8 @@ static void print_server_usage(void) {
|
|||||||
printf(" --config=FILE Daemon config file (default: ~/.config/fastsync/\n");
|
printf(" --config=FILE Daemon config file (default: ~/.config/fastsync/\n");
|
||||||
printf(" fastsyncd.conf, else /etc/fastsyncd.conf)\n");
|
printf(" fastsyncd.conf, else /etc/fastsyncd.conf)\n");
|
||||||
printf(" --dparam=KEY=VALUE Override one global config key on the command line\n");
|
printf(" --dparam=KEY=VALUE Override one global config key on the command line\n");
|
||||||
printf(" (port, motd file, address)\n");
|
printf(" (port, motd file, address, max connections,\n");
|
||||||
|
printf(" auth failure delay, hosts allow, hosts deny)\n");
|
||||||
printf(" --no-detach Stay in the foreground (default detaches to\n");
|
printf(" --no-detach Stay in the foreground (default detaches to\n");
|
||||||
printf(" background when running --daemon)\n");
|
printf(" background when running --daemon)\n");
|
||||||
printf(" --password-file=FILE Credential store for modules that declare\n");
|
printf(" --password-file=FILE Credential store for modules that declare\n");
|
||||||
@@ -912,6 +1048,9 @@ int main(int argc, char* argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
|
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
|
||||||
|
/* handler() does not own the stdio fds: it never closes its descriptor
|
||||||
|
* argument, so STDIN/STDOUT stay open for this (single-shot) SSH session
|
||||||
|
* and are released by process exit. */
|
||||||
handler(STDIN_FILENO);
|
handler(STDIN_FILENO);
|
||||||
release_authorization();
|
release_authorization();
|
||||||
server_cli_options_free(&opts);
|
server_cli_options_free(&opts);
|
||||||
@@ -956,6 +1095,12 @@ int main(int argc, char* argv[]) {
|
|||||||
"and device nodes within that module root -- pair it with `auth users` "
|
"and device nodes within that module root -- pair it with `auth users` "
|
||||||
"unless the module is intentionally open to the network",
|
"unless the module is intentionally open to the network",
|
||||||
g_daemon_conf->modules[i].name);
|
g_daemon_conf->modules[i].name);
|
||||||
|
if (g_daemon_conf->modules[i].max_connections > 0)
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"daemon module '%s': per-module 'max connections' is stored but not enforced "
|
||||||
|
"per module; the global 'max connections' cap (%d) applies to the whole "
|
||||||
|
"listener",
|
||||||
|
g_daemon_conf->modules[i].name, g_daemon_conf->global.max_connections);
|
||||||
}
|
}
|
||||||
/* Daemon credential store (Wave B). --password-file and --early-input
|
/* Daemon credential store (Wave B). --password-file and --early-input
|
||||||
* feed the same store, loaded BEFORE the listener forks so every
|
* feed the same store, loaded BEFORE the listener forks so every
|
||||||
@@ -1020,6 +1165,8 @@ int main(int argc, char* argv[]) {
|
|||||||
exit_code = 1;
|
exit_code = 1;
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
if (g_daemon_conf)
|
||||||
|
server_set_max_connections(g_server, (unsigned int)g_daemon_conf->global.max_connections);
|
||||||
if (opts.use_tls) {
|
if (opts.use_tls) {
|
||||||
if (!opts.tls_cert || !opts.tls_key || !opts.tls_ca || !opts.client_cn) {
|
if (!opts.tls_cert || !opts.tls_key || !opts.tls_ca || !opts.client_cn) {
|
||||||
fprintf(stderr, "Error: --tls requires --cert, --key, --ca, and --client-cn\n");
|
fprintf(stderr, "Error: --tls requires --cert, --key, --ca, and --client-cn\n");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
|
#include <limits.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -39,6 +40,8 @@ void array_list_delete(ArrayList* array_list) {
|
|||||||
static bool array_list_extend(ArrayList* array_list) {
|
static bool array_list_extend(ArrayList* array_list) {
|
||||||
if (array_list == NULL)
|
if (array_list == NULL)
|
||||||
return false;
|
return false;
|
||||||
|
if (array_list->capacity > INT_MAX / 2)
|
||||||
|
return false;
|
||||||
int new_capacity = array_list->capacity * 2;
|
int new_capacity = array_list->capacity * 2;
|
||||||
if (new_capacity == 0)
|
if (new_capacity == 0)
|
||||||
new_capacity = INITIAL_ARRAY_SIZE;
|
new_capacity = INITIAL_ARRAY_SIZE;
|
||||||
|
|||||||
+92
-39
@@ -67,7 +67,11 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->tls_ca = NULL;
|
config->tls_ca = NULL;
|
||||||
config->server_host = str_dup("127.0.0.1");
|
config->server_host = str_dup("127.0.0.1");
|
||||||
config->server_port = 8080;
|
config->server_port = 8080;
|
||||||
config->timeout = 30;
|
/* 0 means "--timeout not given": the transport keeps its own built-in 30 s
|
||||||
|
* socket timeout (tcp_set_timeouts ignores non-positive values) and the
|
||||||
|
* protocol layer keeps its built-in 60 s per-message deadline. A positive
|
||||||
|
* value overrides BOTH (see protocol_session_set_io_timeout). */
|
||||||
|
config->timeout = 0;
|
||||||
config->contimeout = 10;
|
config->contimeout = 10;
|
||||||
config->quiet = false;
|
config->quiet = false;
|
||||||
config->backup = false;
|
config->backup = false;
|
||||||
@@ -202,6 +206,51 @@ static bool receive_wire_bool(int fd, bool* value) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cumulative budget for the strings retained by one received Config (see
|
||||||
|
* MAX_CONFIG_STRING_BYTES). Config strings are received once per connection
|
||||||
|
* before authentication and live for its whole lifetime, so the charge is never
|
||||||
|
* released. */
|
||||||
|
typedef struct {
|
||||||
|
unsigned long long used;
|
||||||
|
} ConfigStringBudget;
|
||||||
|
|
||||||
|
/* Charge `bytes` (the retained allocation: string body plus NUL) against the
|
||||||
|
* aggregate config-string budget. Returns false when the ceiling would be
|
||||||
|
* exceeded, letting the caller reject the frame with a clear error instead of
|
||||||
|
* retaining unbounded pre-auth memory. */
|
||||||
|
static bool config_string_budget_charge(ConfigStringBudget* budget, size_t bytes) {
|
||||||
|
if ((unsigned long long)bytes > MAX_CONFIG_STRING_BYTES ||
|
||||||
|
budget->used > MAX_CONFIG_STRING_BYTES - (unsigned long long)bytes) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Config string budget exceeded (%llu + %zu > %llu bytes)",
|
||||||
|
budget->used, bytes, (unsigned long long)MAX_CONFIG_STRING_BYTES);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
budget->used += (unsigned long long)bytes;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char* config_receive_str(int fd, ConfigStringBudget* budget) {
|
||||||
|
char* value = receive_str(fd);
|
||||||
|
if (!value)
|
||||||
|
return NULL;
|
||||||
|
if (!config_string_budget_charge(budget, strlen(value) + 1)) {
|
||||||
|
free(value);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char* config_receive_str_redacted(int fd, ConfigStringBudget* budget) {
|
||||||
|
char* value = receive_str_redacted(fd);
|
||||||
|
if (!value)
|
||||||
|
return NULL;
|
||||||
|
if (!config_string_budget_charge(budget, strlen(value) + 1)) {
|
||||||
|
free(value);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
static bool validate_received_config(const Config* config) {
|
static bool validate_received_config(const Config* config) {
|
||||||
return valid_wire_bool(config->save_to_disk) && valid_wire_bool(config->use_multithreading) &&
|
return valid_wire_bool(config->save_to_disk) && valid_wire_bool(config->use_multithreading) &&
|
||||||
valid_wire_bool(config->use_chunk_serialization) &&
|
valid_wire_bool(config->use_chunk_serialization) &&
|
||||||
@@ -257,7 +306,7 @@ static bool validate_received_config(const Config* config) {
|
|||||||
config->delta_block_size <= DELTA_BLOCK_SIZE_MAX &&
|
config->delta_block_size <= DELTA_BLOCK_SIZE_MAX &&
|
||||||
config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->modify_window >= 0 &&
|
config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->modify_window >= 0 &&
|
||||||
config->max_delete >= -1 && config->skip_compress_count >= 0 &&
|
config->max_delete >= -1 && config->skip_compress_count >= 0 &&
|
||||||
config->skip_compress_count <= 10000 && config->max_alloc > 0 &&
|
config->skip_compress_count <= MAX_SKIP_COMPRESS_SUFFIXES && config->max_alloc > 0 &&
|
||||||
(!config->chmod_spec || !*config->chmod_spec ||
|
(!config->chmod_spec || !*config->chmod_spec ||
|
||||||
chmod_apply(0, config->chmod_spec, &(mode_t){0})) &&
|
chmod_apply(0, config->chmod_spec, &(mode_t){0})) &&
|
||||||
/* The received --iconv CONVERT_SPEC is untrusted input that drives
|
/* The received --iconv CONVERT_SPEC is untrusted input that drives
|
||||||
@@ -648,6 +697,9 @@ void config_delete(Config* config) {
|
|||||||
if (config == NULL)
|
if (config == NULL)
|
||||||
return;
|
return;
|
||||||
if (config->log_file) {
|
if (config->log_file) {
|
||||||
|
/* The logging subsystem borrows this FILE*; detach it before closing so a
|
||||||
|
* concurrent log call can never touch the freed handle. */
|
||||||
|
log_set_file(NULL);
|
||||||
fclose(config->log_file);
|
fclose(config->log_file);
|
||||||
config->log_file = NULL;
|
config->log_file = NULL;
|
||||||
}
|
}
|
||||||
@@ -819,7 +871,7 @@ static bool send_checksum_options(int fd, const Config* c) {
|
|||||||
send_n_data(fd, &c->checksum_seed, sizeof(c->checksum_seed));
|
send_n_data(fd, &c->checksum_seed, sizeof(c->checksum_seed));
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_core_fields(int fd, Config* c) {
|
static bool receive_core_fields(int fd, Config* c, ConfigStringBudget* budget) {
|
||||||
int value;
|
int value;
|
||||||
if (!receive_wire_bool(fd, &c->eight_bit_output))
|
if (!receive_wire_bool(fd, &c->eight_bit_output))
|
||||||
return false;
|
return false;
|
||||||
@@ -829,8 +881,8 @@ static bool receive_core_fields(int fd, Config* c) {
|
|||||||
if (c->max_alloc > MAX_SERVER_ALLOC)
|
if (c->max_alloc > MAX_SERVER_ALLOC)
|
||||||
c->max_alloc = MAX_SERVER_ALLOC;
|
c->max_alloc = MAX_SERVER_ALLOC;
|
||||||
protocol_session_set_max_alloc(NULL, c->max_alloc);
|
protocol_session_set_max_alloc(NULL, c->max_alloc);
|
||||||
c->send_directory = receive_str(fd);
|
c->send_directory = config_receive_str(fd, budget);
|
||||||
c->receive_root_directory = receive_str(fd);
|
c->receive_root_directory = config_receive_str(fd, budget);
|
||||||
if (!c->send_directory || !c->receive_root_directory)
|
if (!c->send_directory || !c->receive_root_directory)
|
||||||
return false;
|
return false;
|
||||||
if (!receive_wire_bool(fd, &c->save_to_disk) || !receive_wire_bool(fd, &c->use_multithreading) ||
|
if (!receive_wire_bool(fd, &c->save_to_disk) || !receive_wire_bool(fd, &c->use_multithreading) ||
|
||||||
@@ -863,10 +915,10 @@ static bool receive_delta_fields(int fd, Config* c) {
|
|||||||
receive_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long));
|
receive_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long));
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_file_options(int fd, Config* c) {
|
static bool receive_file_options(int fd, Config* c, ConfigStringBudget* budget) {
|
||||||
if (!receive_wire_bool(fd, &c->backup))
|
if (!receive_wire_bool(fd, &c->backup))
|
||||||
return false;
|
return false;
|
||||||
char* backup_dir = receive_str(fd);
|
char* backup_dir = config_receive_str(fd, budget);
|
||||||
if (!backup_dir)
|
if (!backup_dir)
|
||||||
return false;
|
return false;
|
||||||
if (*backup_dir != '\0') {
|
if (*backup_dir != '\0') {
|
||||||
@@ -920,8 +972,8 @@ static bool receive_selection_options(int fd, Config* c) {
|
|||||||
return receive_wire_bool(fd, &c->delete_delay);
|
return receive_wire_bool(fd, &c->delete_delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_resume_options(int fd, Config* c) {
|
static bool receive_resume_options(int fd, Config* c, ConfigStringBudget* budget) {
|
||||||
char* temp_dir = receive_str(fd);
|
char* temp_dir = config_receive_str(fd, budget);
|
||||||
if (!temp_dir)
|
if (!temp_dir)
|
||||||
return false;
|
return false;
|
||||||
if (*temp_dir != '\0') {
|
if (*temp_dir != '\0') {
|
||||||
@@ -935,7 +987,7 @@ static bool receive_resume_options(int fd, Config* c) {
|
|||||||
string for "unset". Canonicalize the empty wire value back to NULL so
|
string for "unset". Canonicalize the empty wire value back to NULL so
|
||||||
receivers observe exactly what the client configured (plain --backup, for
|
receivers observe exactly what the client configured (plain --backup, for
|
||||||
example, must not look like --backup-dir ""). */
|
example, must not look like --backup-dir ""). */
|
||||||
char* partial_dir = receive_str(fd);
|
char* partial_dir = config_receive_str(fd, budget);
|
||||||
if (!partial_dir)
|
if (!partial_dir)
|
||||||
return false;
|
return false;
|
||||||
if (*partial_dir != '\0') {
|
if (*partial_dir != '\0') {
|
||||||
@@ -943,7 +995,7 @@ static bool receive_resume_options(int fd, Config* c) {
|
|||||||
} else {
|
} else {
|
||||||
free(partial_dir);
|
free(partial_dir);
|
||||||
}
|
}
|
||||||
char* suffix = receive_str(fd);
|
char* suffix = config_receive_str(fd, budget);
|
||||||
if (!suffix)
|
if (!suffix)
|
||||||
return false;
|
return false;
|
||||||
if (*suffix != '\0') {
|
if (*suffix != '\0') {
|
||||||
@@ -957,20 +1009,20 @@ static bool receive_resume_options(int fd, Config* c) {
|
|||||||
return false;
|
return false;
|
||||||
if (!receive_n_data(fd, &c->modify_window, sizeof(c->modify_window)))
|
if (!receive_n_data(fd, &c->modify_window, sizeof(c->modify_window)))
|
||||||
return false;
|
return false;
|
||||||
c->compress_choice = receive_str(fd);
|
c->compress_choice = config_receive_str(fd, budget);
|
||||||
if (!c->compress_choice)
|
if (!c->compress_choice)
|
||||||
return false;
|
return false;
|
||||||
c->chmod_spec = receive_str(fd);
|
c->chmod_spec = config_receive_str(fd, budget);
|
||||||
if (!c->chmod_spec || !receive_wire_bool(fd, &c->skip_compress_set) ||
|
if (!c->chmod_spec || !receive_wire_bool(fd, &c->skip_compress_set) ||
|
||||||
!receive_int(fd, &c->skip_compress_count) || c->skip_compress_count < 0 ||
|
!receive_int(fd, &c->skip_compress_count) || c->skip_compress_count < 0 ||
|
||||||
c->skip_compress_count > 10000)
|
c->skip_compress_count > MAX_SKIP_COMPRESS_SUFFIXES)
|
||||||
return false;
|
return false;
|
||||||
if (c->skip_compress_count > 0) {
|
if (c->skip_compress_count > 0) {
|
||||||
c->skip_compress_suffixes = calloc((size_t)c->skip_compress_count, sizeof(char*));
|
c->skip_compress_suffixes = calloc((size_t)c->skip_compress_count, sizeof(char*));
|
||||||
if (!c->skip_compress_suffixes)
|
if (!c->skip_compress_suffixes)
|
||||||
return false;
|
return false;
|
||||||
for (int i = 0; i < c->skip_compress_count; i++) {
|
for (int i = 0; i < c->skip_compress_count; i++) {
|
||||||
c->skip_compress_suffixes[i] = receive_str(fd);
|
c->skip_compress_suffixes[i] = config_receive_str(fd, budget);
|
||||||
if (!c->skip_compress_suffixes[i])
|
if (!c->skip_compress_suffixes[i])
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -978,7 +1030,7 @@ static bool receive_resume_options(int fd, Config* c) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_basis_options(int fd, Config* c) {
|
static bool receive_basis_options(int fd, Config* c, ConfigStringBudget* budget) {
|
||||||
int count;
|
int count;
|
||||||
if (!receive_int(fd, &count))
|
if (!receive_int(fd, &count))
|
||||||
return false;
|
return false;
|
||||||
@@ -988,7 +1040,7 @@ static bool receive_basis_options(int fd, Config* c) {
|
|||||||
int type;
|
int type;
|
||||||
if (!receive_int(fd, &type) || type <= BASIS_DEST_NONE || type > BASIS_DEST_LINK)
|
if (!receive_int(fd, &type) || type <= BASIS_DEST_NONE || type > BASIS_DEST_LINK)
|
||||||
return false;
|
return false;
|
||||||
char* path = receive_str(fd);
|
char* path = config_receive_str(fd, budget);
|
||||||
if (!path)
|
if (!path)
|
||||||
return false;
|
return false;
|
||||||
/* config_basis_append validates and canonicalizes the path; a rejected
|
/* config_basis_append validates and canonicalizes the path; a rejected
|
||||||
@@ -1119,8 +1171,8 @@ static bool send_daemon_module(int fd, const Config* c) {
|
|||||||
return send_str(fd, c->module ? c->module : "");
|
return send_str(fd, c->module ? c->module : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_daemon_module(int fd, Config* c) {
|
static bool receive_daemon_module(int fd, Config* c, ConfigStringBudget* budget) {
|
||||||
char* module = receive_str(fd);
|
char* module = config_receive_str(fd, budget);
|
||||||
if (!module)
|
if (!module)
|
||||||
return false;
|
return false;
|
||||||
/* Guard against a hostile client flooding the log with an over-long module
|
/* Guard against a hostile client flooding the log with an over-long module
|
||||||
@@ -1155,14 +1207,14 @@ static bool send_daemon_auth(int fd, const Config* c) {
|
|||||||
return send_str_redacted(fd, c->auth_user);
|
return send_str_redacted(fd, c->auth_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_daemon_auth(int fd, Config* c) {
|
static bool receive_daemon_auth(int fd, Config* c, ConfigStringBudget* budget) {
|
||||||
int present;
|
int present;
|
||||||
if (!receive_int(fd, &present) || !valid_wire_bool(present))
|
if (!receive_int(fd, &present) || !valid_wire_bool(present))
|
||||||
return false;
|
return false;
|
||||||
if (!present)
|
if (!present)
|
||||||
return true;
|
return true;
|
||||||
/* Redacted receive: never log the incoming username body. */
|
/* Redacted receive: never log the incoming username body. */
|
||||||
char* user = receive_str_redacted(fd);
|
char* user = config_receive_str_redacted(fd, budget);
|
||||||
if (!user)
|
if (!user)
|
||||||
return false;
|
return false;
|
||||||
if (!credentials_username_valid(user)) {
|
if (!credentials_username_valid(user)) {
|
||||||
@@ -1272,8 +1324,8 @@ static bool send_iconv_spec(int fd, const Config* c) {
|
|||||||
return send_str(fd, c->iconv_spec ? c->iconv_spec : "");
|
return send_str(fd, c->iconv_spec ? c->iconv_spec : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_iconv_spec(int fd, Config* c) {
|
static bool receive_iconv_spec(int fd, Config* c, ConfigStringBudget* budget) {
|
||||||
char* spec = receive_str(fd);
|
char* spec = config_receive_str(fd, budget);
|
||||||
if (!spec)
|
if (!spec)
|
||||||
return false;
|
return false;
|
||||||
if (*spec == '\0') {
|
if (*spec == '\0') {
|
||||||
@@ -1293,14 +1345,14 @@ static bool receive_iconv_spec(int fd, Config* c) {
|
|||||||
* validated to the SUPER_MODE_AUTO..SUPER_MODE_OFF range (also re-checked by
|
* validated to the SUPER_MODE_AUTO..SUPER_MODE_OFF range (also re-checked by
|
||||||
* validate_received_config). */
|
* validate_received_config). */
|
||||||
static bool send_privilege_options(int fd, const Config* c) {
|
static bool send_privilege_options(int fd, const Config* c) {
|
||||||
return send_int(fd, c->super_mode);
|
return send_int(fd, (int)c->super_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_privilege_options(int fd, Config* c) {
|
static bool receive_privilege_options(int fd, Config* c) {
|
||||||
int mode;
|
int mode;
|
||||||
if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF)
|
if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF)
|
||||||
return false;
|
return false;
|
||||||
c->super_mode = mode;
|
c->super_mode = (SuperMode)mode;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1376,47 +1428,48 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
|||||||
Config* config = config_create();
|
Config* config = config_create();
|
||||||
if (!config)
|
if (!config)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
ConfigStringBudget budget = {0};
|
||||||
free(config->version);
|
free(config->version);
|
||||||
config->version = receive_str(file_descriptor);
|
config->version = config_receive_str(file_descriptor, &budget);
|
||||||
if (!config->version)
|
if (!config->version)
|
||||||
goto error;
|
goto error;
|
||||||
if (strcmp(config->version, PROTOCOL_VERSION) != 0) {
|
if (strcmp(config->version, PROTOCOL_VERSION) != 0) {
|
||||||
char* escaped_version = output_escape(config->version, false);
|
char* escaped_version = output_escape(config->version, false);
|
||||||
fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n",
|
log_message(LOG_LEVEL_ERROR, "Protocol version mismatch: client=%s, server=%s",
|
||||||
escaped_version ? escaped_version : "<allocation failed>", PROTOCOL_VERSION);
|
escaped_version ? escaped_version : "<allocation failed>", PROTOCOL_VERSION);
|
||||||
free(escaped_version);
|
free(escaped_version);
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
if (!receive_core_fields(file_descriptor, config) ||
|
if (!receive_core_fields(file_descriptor, config, &budget) ||
|
||||||
!receive_delta_fields(file_descriptor, config) ||
|
!receive_delta_fields(file_descriptor, config) ||
|
||||||
!receive_file_options(file_descriptor, config) ||
|
!receive_file_options(file_descriptor, config, &budget) ||
|
||||||
!receive_selection_options(file_descriptor, config) ||
|
!receive_selection_options(file_descriptor, config) ||
|
||||||
!receive_resume_options(file_descriptor, config) ||
|
!receive_resume_options(file_descriptor, config, &budget) ||
|
||||||
!receive_basis_options(file_descriptor, config) ||
|
!receive_basis_options(file_descriptor, config, &budget) ||
|
||||||
!receive_fuzzy_option(file_descriptor, config) ||
|
!receive_fuzzy_option(file_descriptor, config) ||
|
||||||
!receive_checksum_options(file_descriptor, config) ||
|
!receive_checksum_options(file_descriptor, config) ||
|
||||||
!receive_identity_options(file_descriptor, config) ||
|
!receive_identity_options(file_descriptor, config) ||
|
||||||
!receive_metadata_times_options(file_descriptor, config) ||
|
!receive_metadata_times_options(file_descriptor, config) ||
|
||||||
!receive_symlink_trust_options(file_descriptor, config) ||
|
!receive_symlink_trust_options(file_descriptor, config) ||
|
||||||
!receive_phase4_xattr_options(file_descriptor, config) ||
|
!receive_phase4_xattr_options(file_descriptor, config) ||
|
||||||
!receive_daemon_module(file_descriptor, config) ||
|
!receive_daemon_module(file_descriptor, config, &budget) ||
|
||||||
!receive_daemon_auth(file_descriptor, config) ||
|
!receive_daemon_auth(file_descriptor, config, &budget) ||
|
||||||
!receive_iconv_spec(file_descriptor, config) ||
|
!receive_iconv_spec(file_descriptor, config, &budget) ||
|
||||||
!receive_privilege_options(file_descriptor, config) ||
|
!receive_privilege_options(file_descriptor, config) ||
|
||||||
!receive_copy_as_options(file_descriptor, config))
|
!receive_copy_as_options(file_descriptor, config))
|
||||||
goto error;
|
goto error;
|
||||||
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
||||||
strcmp(config->compress_choice, "none") != 0) {
|
strcmp(config->compress_choice, "none") != 0) {
|
||||||
char* escaped_choice = output_escape(config->compress_choice, config->eight_bit_output);
|
char* escaped_choice = output_escape(config->compress_choice, config->eight_bit_output);
|
||||||
fprintf(stderr, "Unsupported compression choice: %s\n",
|
log_message(LOG_LEVEL_ERROR, "Unsupported compression choice: %s",
|
||||||
escaped_choice ? escaped_choice : "<allocation failed>");
|
escaped_choice ? escaped_choice : "<allocation failed>");
|
||||||
free(escaped_choice);
|
free(escaped_choice);
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
if (!validate_received_config(config)) {
|
if (!validate_received_config(config)) {
|
||||||
fprintf(stderr, "Invalid configuration received from client\n");
|
log_message(LOG_LEVEL_ERROR, "Invalid configuration received from client");
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
@@ -1430,7 +1483,7 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
|||||||
* the CONFIG_VALIDATE_ALREADY_TERMINATED sentinel, so no second status is
|
* the CONFIG_VALIDATE_ALREADY_TERMINATED sentinel, so no second status is
|
||||||
* written. */
|
* written. */
|
||||||
if (rejection != CONFIG_VALIDATE_ALREADY_TERMINATED) {
|
if (rejection != CONFIG_VALIDATE_ALREADY_TERMINATED) {
|
||||||
fprintf(stderr, "%s\n", rejection);
|
log_message(LOG_LEVEL_ERROR, "%s", rejection);
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
}
|
}
|
||||||
goto error;
|
goto error;
|
||||||
|
|||||||
+31
-10
@@ -68,6 +68,13 @@ typedef struct {
|
|||||||
int value; /* 0/1 for booleans, byte count for SO_RCVBUF/SO_SNDBUF */
|
int value; /* 0/1 for booleans, byte count for SO_RCVBUF/SO_SNDBUF */
|
||||||
} SockOptEntry;
|
} SockOptEntry;
|
||||||
|
|
||||||
|
/* --super / --no-super tri-state (Config->super_mode). AUTO (default) and ON
|
||||||
|
* both permit a confined super-user attempt (AUTO preserves FastSync's
|
||||||
|
* historical best-effort behavior; an unprivileged attempt is refused by the
|
||||||
|
* kernel and skipped per entry); OFF forbids the attempt even for root. See
|
||||||
|
* privilege_super_mode_permitted() in identity.h. */
|
||||||
|
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
|
||||||
|
|
||||||
typedef struct Config {
|
typedef struct Config {
|
||||||
char* version;
|
char* version;
|
||||||
char* send_directory;
|
char* send_directory;
|
||||||
@@ -150,7 +157,12 @@ typedef struct Config {
|
|||||||
char* tls_cert;
|
char* tls_cert;
|
||||||
char* tls_key;
|
char* tls_key;
|
||||||
char* tls_ca;
|
char* tls_ca;
|
||||||
|
/* --timeout: per-message I/O deadline in seconds. 0 (the default/unset
|
||||||
|
* sentinel) leaves the transport's built-in 30 s socket timeout and the
|
||||||
|
* protocol's built-in 60 s per-message deadline in place; a positive value
|
||||||
|
* overrides both. See protocol_session_set_io_timeout. */
|
||||||
int timeout;
|
int timeout;
|
||||||
|
/* --contimeout: connect()/accept timeout, transport layer only. */
|
||||||
int contimeout;
|
int contimeout;
|
||||||
bool quiet;
|
bool quiet;
|
||||||
bool backup;
|
bool backup;
|
||||||
@@ -416,7 +428,7 @@ typedef struct Config {
|
|||||||
* as a trailing int so the receiver can enforce the policy. See
|
* as a trailing int so the receiver can enforce the policy. See
|
||||||
* privilege_super_permitted() and identity_ownership_requested() in
|
* privilege_super_permitted() and identity_ownership_requested() in
|
||||||
* identity.h. */
|
* identity.h. */
|
||||||
int super_mode;
|
SuperMode super_mode;
|
||||||
|
|
||||||
// Receiver-side runtime staging registry for --delay-updates. Never sent
|
// Receiver-side runtime staging registry for --delay-updates. Never sent
|
||||||
// over the wire and never set on the sender side.
|
// over the wire and never set on the sender side.
|
||||||
@@ -636,6 +648,24 @@ typedef struct Config {
|
|||||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||||
#define MAX_BASIS_DIRS 64
|
#define MAX_BASIS_DIRS 64
|
||||||
|
|
||||||
|
/* Upper bound on the number of --skip-compress suffixes accepted from the wire.
|
||||||
|
* Each suffix is an independent wire string (up to MAX_STRING_SIZE = 64 KiB), so
|
||||||
|
* without this a hostile pre-auth client could otherwise retain
|
||||||
|
* skip_count * MAX_STRING_SIZE bytes on the server before authentication; 256
|
||||||
|
* covers any realistic suffix list while keeping the worst case small. */
|
||||||
|
#define MAX_SKIP_COMPRESS_SUFFIXES 256
|
||||||
|
|
||||||
|
/* Aggregate ceiling on the bytes retained by ALL strings in one received config
|
||||||
|
* frame (version, send/receive roots, backup/temp/partial/suffix, compression
|
||||||
|
* choice, chmod spec, skip-compress suffixes, basis paths, module, auth user,
|
||||||
|
* iconv spec, ...). The config frame is parsed BEFORE authentication and every
|
||||||
|
* one of these strings lives for the whole connection, so this cumulative
|
||||||
|
* (never released) budget bounds the pre-auth memory a single connection can
|
||||||
|
* pin. MAX_SKIP_COMPRESS_SUFFIXES / MAX_BASIS_DIRS bound the individual
|
||||||
|
* repeatable counts; this budget bounds their product and any single oversized
|
||||||
|
* field. */
|
||||||
|
#define MAX_CONFIG_STRING_BYTES (1ULL * 1024 * 1024)
|
||||||
|
|
||||||
/* Identity-mapping sentinels and bounds (see identity.h for semantics).
|
/* Identity-mapping sentinels and bounds (see identity.h for semantics).
|
||||||
* IDENTITY_MATCH_ANY is a usermap/groupmap FROM '*' (matches any id);
|
* IDENTITY_MATCH_ANY is a usermap/groupmap FROM '*' (matches any id);
|
||||||
* IDENTITY_CURRENT is a chown / map TO '*' (resolve to the receiver's current
|
* IDENTITY_CURRENT is a chown / map TO '*' (resolve to the receiver's current
|
||||||
@@ -644,15 +674,6 @@ typedef struct Config {
|
|||||||
#define IDENTITY_CURRENT (-1)
|
#define IDENTITY_CURRENT (-1)
|
||||||
#define MAX_IDENTITY_MAP 128
|
#define MAX_IDENTITY_MAP 128
|
||||||
|
|
||||||
/* --super / --no-super tri-state (Config->super_mode). AUTO (default) and ON
|
|
||||||
* both permit a confined super-user attempt (AUTO preserves FastSync's
|
|
||||||
* historical best-effort behavior; an unprivileged attempt is refused by the
|
|
||||||
* kernel and skipped per entry); OFF forbids the attempt even for root. See
|
|
||||||
* privilege_super_mode_permitted() in identity.h. */
|
|
||||||
#define SUPER_MODE_AUTO 0
|
|
||||||
#define SUPER_MODE_ON 1
|
|
||||||
#define SUPER_MODE_OFF 2
|
|
||||||
|
|
||||||
Config* config_create(void);
|
Config* config_create(void);
|
||||||
void config_delete(Config* config);
|
void config_delete(Config* config);
|
||||||
|
|
||||||
|
|||||||
+281
-4
@@ -1,8 +1,13 @@
|
|||||||
#include "daemon_conf.h"
|
#include "daemon_conf.h"
|
||||||
|
#include "credentials.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
#include <arpa/inet.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -48,6 +53,158 @@ static bool parse_bool_value(const char* value, bool* out) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Parse an IPv4/IPv6 CIDR "addr/prefix" into `bytes`/`*family`. Returns false
|
||||||
|
* for a malformed address, a missing/oversized prefix, or a prefix that does
|
||||||
|
* not fit the address family. */
|
||||||
|
static bool parse_cidr(const char* cidr, int* prefix_out, uint8_t* bytes, int* family_out) {
|
||||||
|
const char* slash = strchr(cidr, '/');
|
||||||
|
if (!slash)
|
||||||
|
return false;
|
||||||
|
size_t addr_len = (size_t)(slash - cidr);
|
||||||
|
if (addr_len == 0 || addr_len >= INET6_ADDRSTRLEN)
|
||||||
|
return false;
|
||||||
|
char addr[INET6_ADDRSTRLEN];
|
||||||
|
memcpy(addr, cidr, addr_len);
|
||||||
|
addr[addr_len] = '\0';
|
||||||
|
char* end = NULL;
|
||||||
|
long prefix = strtol(slash + 1, &end, 10);
|
||||||
|
if (end == slash + 1 || *end != '\0')
|
||||||
|
return false;
|
||||||
|
struct in_addr v4;
|
||||||
|
struct in6_addr v6;
|
||||||
|
if (inet_pton(AF_INET, addr, &v4) == 1) {
|
||||||
|
if (prefix < 0 || prefix > 32)
|
||||||
|
return false;
|
||||||
|
memcpy(bytes, &v4, sizeof(v4));
|
||||||
|
*prefix_out = (int)prefix;
|
||||||
|
*family_out = AF_INET;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (inet_pton(AF_INET6, addr, &v6) == 1) {
|
||||||
|
if (prefix < 0 || prefix > 128)
|
||||||
|
return false;
|
||||||
|
memcpy(bytes, &v6, sizeof(v6));
|
||||||
|
*prefix_out = (int)prefix;
|
||||||
|
*family_out = AF_INET6;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A host pattern is valid when it is `*`, a valid IPv4/IPv6 literal, or a valid
|
||||||
|
* CIDR. Peer addresses reaching the matcher are always numeric, so hostname
|
||||||
|
* globs are rejected at parse time: accepting one would create a deny rule that
|
||||||
|
* silently never matches (fail-open). */
|
||||||
|
static bool host_pattern_valid(const char* pattern) {
|
||||||
|
if (!pattern || *pattern == '\0')
|
||||||
|
return false;
|
||||||
|
if (strcmp(pattern, "*") == 0)
|
||||||
|
return true;
|
||||||
|
if (strchr(pattern, '/')) {
|
||||||
|
uint8_t bytes[16];
|
||||||
|
int prefix;
|
||||||
|
int family;
|
||||||
|
return parse_cidr(pattern, &prefix, bytes, &family);
|
||||||
|
}
|
||||||
|
struct in_addr v4;
|
||||||
|
struct in6_addr v6;
|
||||||
|
return inet_pton(AF_INET, pattern, &v4) == 1 || inet_pton(AF_INET6, pattern, &v6) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Append every comma- and/or whitespace-separated host pattern in `value` to
|
||||||
|
* the heap-owned list (or replace the list when `replace` is set, which --dparam
|
||||||
|
* uses so an override can narrow access rather than only widen it). Returns
|
||||||
|
* false (err filled) on an invalid pattern or an allocation failure. */
|
||||||
|
static bool store_host_list(char*** list, int* count, const char* value, const char* key,
|
||||||
|
const char* module_name, bool replace, char* err, size_t err_size) {
|
||||||
|
if (replace) {
|
||||||
|
for (int i = 0; i < *count; i++)
|
||||||
|
free((*list)[i]);
|
||||||
|
free(*list);
|
||||||
|
*list = NULL;
|
||||||
|
*count = 0;
|
||||||
|
}
|
||||||
|
char* copy = str_dup(value);
|
||||||
|
if (!copy) {
|
||||||
|
if (module_name)
|
||||||
|
set_error(err, err_size, "out of memory parsing '%s' for module '%s'", key, module_name);
|
||||||
|
else
|
||||||
|
set_error(err, err_size, "out of memory parsing '%s'", key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char* save = NULL;
|
||||||
|
for (char* token = strtok_r(copy, ", \t", &save); token; token = strtok_r(NULL, ", \t", &save)) {
|
||||||
|
if (!host_pattern_valid(token)) {
|
||||||
|
if (module_name)
|
||||||
|
set_error(err, err_size, "module '%s': invalid host pattern '%s' in '%s'", module_name,
|
||||||
|
token, key);
|
||||||
|
else
|
||||||
|
set_error(err, err_size, "invalid host pattern '%s' in '%s'", token, key);
|
||||||
|
free(copy);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char** grown = realloc(*list, (size_t)(*count + 1) * sizeof(char*));
|
||||||
|
if (!grown) {
|
||||||
|
if (module_name)
|
||||||
|
set_error(err, err_size, "out of memory parsing '%s' for module '%s'", key, module_name);
|
||||||
|
else
|
||||||
|
set_error(err, err_size, "out of memory parsing '%s'", key);
|
||||||
|
free(copy);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*list = grown;
|
||||||
|
char* dup = str_dup(token);
|
||||||
|
if (!dup) {
|
||||||
|
if (module_name)
|
||||||
|
set_error(err, err_size, "out of memory parsing '%s' for module '%s'", key, module_name);
|
||||||
|
else
|
||||||
|
set_error(err, err_size, "out of memory parsing '%s'", key);
|
||||||
|
free(copy);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(*list)[(*count)++] = dup;
|
||||||
|
}
|
||||||
|
free(copy);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse a `max connections` value: a positive integer (0/negative/garbage are
|
||||||
|
* rejected because they would silently disable the cap or admit nothing). */
|
||||||
|
static bool store_max_connections(int* slot, const char* value, const char* module_name, char* err,
|
||||||
|
size_t err_size) {
|
||||||
|
char* end = NULL;
|
||||||
|
errno = 0;
|
||||||
|
long n = strtol(value, &end, 10);
|
||||||
|
if (*value == '\0' || errno != 0 || *end != '\0' || n <= 0 || n > INT_MAX) {
|
||||||
|
if (module_name)
|
||||||
|
set_error(err, err_size,
|
||||||
|
"module '%s': invalid 'max connections' '%s' (must be a positive "
|
||||||
|
"integer)",
|
||||||
|
module_name, value);
|
||||||
|
else
|
||||||
|
set_error(err, err_size, "invalid 'max connections' '%s' (must be a positive integer)",
|
||||||
|
value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*slot = (int)n;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse an `auth failure delay` value: 0 (disabled) through the configured cap. */
|
||||||
|
static bool store_auth_failure_delay(int* slot, const char* value, char* err, size_t err_size) {
|
||||||
|
char* end = NULL;
|
||||||
|
errno = 0;
|
||||||
|
long n = strtol(value, &end, 10);
|
||||||
|
if (*value == '\0' || errno != 0 || *end != '\0' || n < 0 ||
|
||||||
|
n > DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS) {
|
||||||
|
set_error(err, err_size, "invalid 'auth failure delay' '%s' (must be 0-%d milliseconds)", value,
|
||||||
|
DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*slot = (int)n;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool daemon_module_name_valid(const char* name) {
|
bool daemon_module_name_valid(const char* name) {
|
||||||
if (!name || *name == '\0')
|
if (!name || *name == '\0')
|
||||||
return false;
|
return false;
|
||||||
@@ -68,14 +225,25 @@ DaemonConf* daemon_conf_create(void) {
|
|||||||
if (!conf)
|
if (!conf)
|
||||||
return NULL;
|
return NULL;
|
||||||
conf->global.port = DAEMON_CONF_DEFAULT_PORT;
|
conf->global.port = DAEMON_CONF_DEFAULT_PORT;
|
||||||
|
conf->global.max_connections = DAEMON_CONF_DEFAULT_MAX_CONNECTIONS;
|
||||||
|
conf->global.auth_failure_delay_ms = DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS;
|
||||||
return conf;
|
return conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Free a heap-owned pattern list of `count` entries. */
|
||||||
|
static void free_string_list(char** list, int count) {
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
free(list[i]);
|
||||||
|
free(list);
|
||||||
|
}
|
||||||
|
|
||||||
void daemon_conf_free(DaemonConf* conf) {
|
void daemon_conf_free(DaemonConf* conf) {
|
||||||
if (!conf)
|
if (!conf)
|
||||||
return;
|
return;
|
||||||
free(conf->global.motd_file);
|
free(conf->global.motd_file);
|
||||||
free(conf->global.address);
|
free(conf->global.address);
|
||||||
|
free_string_list(conf->global.hosts_allow, conf->global.hosts_allow_count);
|
||||||
|
free_string_list(conf->global.hosts_deny, conf->global.hosts_deny_count);
|
||||||
for (int i = 0; i < conf->module_count; i++) {
|
for (int i = 0; i < conf->module_count; i++) {
|
||||||
DaemonModule* m = &conf->modules[i];
|
DaemonModule* m = &conf->modules[i];
|
||||||
free(m->name);
|
free(m->name);
|
||||||
@@ -83,6 +251,8 @@ void daemon_conf_free(DaemonConf* conf) {
|
|||||||
for (int j = 0; j < m->auth_user_count; j++)
|
for (int j = 0; j < m->auth_user_count; j++)
|
||||||
free(m->auth_users[j]);
|
free(m->auth_users[j]);
|
||||||
free(m->auth_users);
|
free(m->auth_users);
|
||||||
|
free_string_list(m->hosts_allow, m->hosts_allow_count);
|
||||||
|
free_string_list(m->hosts_deny, m->hosts_deny_count);
|
||||||
}
|
}
|
||||||
free(conf->modules);
|
free(conf->modules);
|
||||||
free(conf);
|
free(conf);
|
||||||
@@ -122,8 +292,8 @@ static bool store_port(int* slot, const char* value, char* err, size_t err_size)
|
|||||||
|
|
||||||
/* Apply a global scalar key/value. Keys are case-insensitive. Returns false
|
/* Apply a global scalar key/value. Keys are case-insensitive. Returns false
|
||||||
* (err filled) on an unknown key or an invalid value. */
|
* (err filled) on an unknown key or an invalid value. */
|
||||||
static bool apply_global_key(DaemonConf* conf, char* key, const char* value, char* err,
|
static bool apply_global_key(DaemonConf* conf, char* key, const char* value, bool replace_hosts,
|
||||||
size_t err_size) {
|
char* err, size_t err_size) {
|
||||||
if (key_equals(key, "port"))
|
if (key_equals(key, "port"))
|
||||||
return store_port(&conf->global.port, value, err, err_size);
|
return store_port(&conf->global.port, value, err, err_size);
|
||||||
if (key_equals(key, "motd file")) {
|
if (key_equals(key, "motd file")) {
|
||||||
@@ -140,6 +310,16 @@ static bool apply_global_key(DaemonConf* conf, char* key, const char* value, cha
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (key_equals(key, "max connections"))
|
||||||
|
return store_max_connections(&conf->global.max_connections, value, NULL, err, err_size);
|
||||||
|
if (key_equals(key, "auth failure delay"))
|
||||||
|
return store_auth_failure_delay(&conf->global.auth_failure_delay_ms, value, err, err_size);
|
||||||
|
if (key_equals(key, "hosts allow"))
|
||||||
|
return store_host_list(&conf->global.hosts_allow, &conf->global.hosts_allow_count, value,
|
||||||
|
"hosts allow", NULL, replace_hosts, err, err_size);
|
||||||
|
if (key_equals(key, "hosts deny"))
|
||||||
|
return store_host_list(&conf->global.hosts_deny, &conf->global.hosts_deny_count, value,
|
||||||
|
"hosts deny", NULL, replace_hosts, err, err_size);
|
||||||
set_error(err, err_size, "unknown global key '%s'", key);
|
set_error(err, err_size, "unknown global key '%s'", key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -192,6 +372,12 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
|
|||||||
const char* user = trim_ws(token);
|
const char* user = trim_ws(token);
|
||||||
if (*user == '\0')
|
if (*user == '\0')
|
||||||
continue;
|
continue;
|
||||||
|
if (!credentials_username_valid(user)) {
|
||||||
|
set_error(err, err_size, "module '%s': invalid 'auth users' entry '%s'", module->name,
|
||||||
|
user);
|
||||||
|
free(list);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
char** grown =
|
char** grown =
|
||||||
realloc(module->auth_users, (size_t)(module->auth_user_count + 1) * sizeof(char*));
|
realloc(module->auth_users, (size_t)(module->auth_user_count + 1) * sizeof(char*));
|
||||||
if (!grown) {
|
if (!grown) {
|
||||||
@@ -213,6 +399,14 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
|
|||||||
free(list);
|
free(list);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (key_equals(key, "max connections"))
|
||||||
|
return store_max_connections(&module->max_connections, value, module->name, err, err_size);
|
||||||
|
if (key_equals(key, "hosts allow"))
|
||||||
|
return store_host_list(&module->hosts_allow, &module->hosts_allow_count, value, "hosts allow",
|
||||||
|
false, module->name, err, err_size);
|
||||||
|
if (key_equals(key, "hosts deny"))
|
||||||
|
return store_host_list(&module->hosts_deny, &module->hosts_deny_count, value, "hosts deny",
|
||||||
|
false, module->name, err, err_size);
|
||||||
set_error(err, err_size, "unknown key '%s' in module '%s'", key, module->name);
|
set_error(err, err_size, "unknown key '%s' in module '%s'", key, module->name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -393,7 +587,7 @@ DaemonConf* daemon_conf_load(const char* path, char* err, size_t err_size) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!apply_global_key(conf, key, value, err, err_size)) {
|
if (!apply_global_key(conf, key, value, false, err, err_size)) {
|
||||||
ok = false;
|
ok = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -448,7 +642,90 @@ int daemon_conf_apply_dparam(DaemonConf* conf, const char* assignment, char* err
|
|||||||
set_error(err, err_size, "--dparam '%s' has an empty value", assignment);
|
set_error(err, err_size, "--dparam '%s' has an empty value", assignment);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
bool ok = apply_global_key(conf, key, value, err, err_size);
|
bool ok = apply_global_key(conf, key, value, true, err, err_size);
|
||||||
free(copy);
|
free(copy);
|
||||||
return ok ? 0 : -1;
|
return ok ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compare the first `prefix` bits of two 16-byte address buffers. */
|
||||||
|
static bool bit_prefix_match(const uint8_t* a, const uint8_t* b, int prefix) {
|
||||||
|
int whole = prefix / 8;
|
||||||
|
if (whole > 0 && memcmp(a, b, (size_t)whole) != 0)
|
||||||
|
return false;
|
||||||
|
int remainder = prefix % 8;
|
||||||
|
if (remainder == 0)
|
||||||
|
return true;
|
||||||
|
uint8_t mask = (uint8_t)(0xffu << (8 - remainder));
|
||||||
|
return (a[whole] & mask) == (b[whole] & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case-insensitive glob match used for hostname patterns. Falls back to the
|
||||||
|
* shared case-sensitive matcher when an operand is too long for the stack
|
||||||
|
* buffers. */
|
||||||
|
static bool host_glob_match(const char* pattern, const char* str) {
|
||||||
|
char pbuf[256];
|
||||||
|
char sbuf[256];
|
||||||
|
size_t plen = strlen(pattern);
|
||||||
|
size_t slen = strlen(str);
|
||||||
|
if (plen >= sizeof(pbuf) || slen >= sizeof(sbuf))
|
||||||
|
return glob_match(pattern, str);
|
||||||
|
for (size_t i = 0; i <= plen; i++)
|
||||||
|
pbuf[i] = (char)tolower((unsigned char)pattern[i]);
|
||||||
|
for (size_t i = 0; i <= slen; i++)
|
||||||
|
sbuf[i] = (char)tolower((unsigned char)str[i]);
|
||||||
|
return glob_match(pbuf, sbuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool daemon_host_pattern_match(const char* pattern, const char* peer_ip) {
|
||||||
|
if (!pattern || *pattern == '\0' || !peer_ip || *peer_ip == '\0')
|
||||||
|
return false;
|
||||||
|
if (strcmp(pattern, "*") == 0)
|
||||||
|
return true;
|
||||||
|
if (strchr(pattern, '/')) {
|
||||||
|
uint8_t pattern_bytes[16];
|
||||||
|
uint8_t peer_bytes[16];
|
||||||
|
int prefix = 0;
|
||||||
|
int family = AF_UNSPEC;
|
||||||
|
if (!parse_cidr(pattern, &prefix, pattern_bytes, &family))
|
||||||
|
return false;
|
||||||
|
if (inet_pton(family, peer_ip, peer_bytes) != 1)
|
||||||
|
return false;
|
||||||
|
return bit_prefix_match(pattern_bytes, peer_bytes, prefix);
|
||||||
|
}
|
||||||
|
struct in_addr pattern_v4;
|
||||||
|
struct in_addr peer_v4;
|
||||||
|
if (inet_pton(AF_INET, pattern, &pattern_v4) == 1)
|
||||||
|
return inet_pton(AF_INET, peer_ip, &peer_v4) == 1 && pattern_v4.s_addr == peer_v4.s_addr;
|
||||||
|
struct in6_addr pattern_v6;
|
||||||
|
struct in6_addr peer_v6;
|
||||||
|
if (inet_pton(AF_INET6, pattern, &pattern_v6) == 1)
|
||||||
|
return inet_pton(AF_INET6, peer_ip, &peer_v6) == 1 &&
|
||||||
|
memcmp(&pattern_v6, &peer_v6, sizeof(pattern_v6)) == 0;
|
||||||
|
/* Not a literal: a hostname/glob pattern. */
|
||||||
|
return host_glob_match(pattern, peer_ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool daemon_hosts_allowed(const char* peer_ip, char* const* allow, int allow_count,
|
||||||
|
char* const* deny, int deny_count) {
|
||||||
|
if (!peer_ip)
|
||||||
|
return false;
|
||||||
|
for (int i = 0; i < deny_count; i++) {
|
||||||
|
if (daemon_host_pattern_match(deny[i], peer_ip))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (allow_count > 0) {
|
||||||
|
for (int i = 0; i < allow_count; i++) {
|
||||||
|
if (daemon_host_pattern_match(allow[i], peer_ip))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool daemon_hosts_restricted(char* const* allow, int allow_count, char* const* deny,
|
||||||
|
int deny_count) {
|
||||||
|
(void)allow;
|
||||||
|
(void)deny;
|
||||||
|
return allow_count > 0 || deny_count > 0;
|
||||||
|
}
|
||||||
@@ -52,14 +52,32 @@ typedef struct DaemonModule {
|
|||||||
activities. Without it the daemon refuses all of them. */
|
activities. Without it the daemon refuses all of them. */
|
||||||
char** auth_users; /* `auth users = a,b`; Wave B credential list */
|
char** auth_users; /* `auth users = a,b`; Wave B credential list */
|
||||||
int auth_user_count;
|
int auth_user_count;
|
||||||
|
/* `max connections = N` (optional per-module cap). 0 means "not set"
|
||||||
|
* (inherit the global cap). Parsed, stored, and validated, but NOT enforced
|
||||||
|
* per-module: connections are counted in the accept-loop parent before the
|
||||||
|
* client's module is known, so only the global cap is enforced (see
|
||||||
|
* transport_tcp.c and the Daemon Mode notes in RSYNC_COMPAT.md). */
|
||||||
|
int max_connections;
|
||||||
|
char** hosts_allow; /* `hosts allow = a,b`; host access allow patterns */
|
||||||
|
int hosts_allow_count;
|
||||||
|
char** hosts_deny; /* `hosts deny = a,b`; host access deny patterns */
|
||||||
|
int hosts_deny_count;
|
||||||
} DaemonModule;
|
} DaemonModule;
|
||||||
|
|
||||||
/* Global (pre-module) scalar keys. `motd file` is parsed and stored but has
|
/* Global (pre-module) scalar keys. `motd file` is parsed and stored but has
|
||||||
* no wire effect yet (MOTD display is Wave C). */
|
* no wire effect yet (MOTD display is Wave C). */
|
||||||
typedef struct DaemonConfGlobals {
|
typedef struct DaemonConfGlobals {
|
||||||
int port; /* `port`, default DAEMON_CONF_DEFAULT_PORT (873) */
|
int port; /* `port`, default DAEMON_CONF_DEFAULT_PORT (873) */
|
||||||
char* motd_file; /* `motd file`, may be NULL */
|
char* motd_file; /* `motd file`, may be NULL */
|
||||||
char* address; /* `address` (optional bind address), may be NULL */
|
char* address; /* `address` (optional bind address), may be NULL */
|
||||||
|
int max_connections; /* `max connections`, default
|
||||||
|
DAEMON_CONF_DEFAULT_MAX_CONNECTIONS (100) */
|
||||||
|
int auth_failure_delay_ms; /* `auth failure delay`, milliseconds; default
|
||||||
|
DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS */
|
||||||
|
char** hosts_allow; /* `hosts allow`; global host access allow patterns */
|
||||||
|
int hosts_allow_count;
|
||||||
|
char** hosts_deny; /* `hosts deny`; global host access deny patterns */
|
||||||
|
int hosts_deny_count;
|
||||||
} DaemonConfGlobals;
|
} DaemonConfGlobals;
|
||||||
|
|
||||||
typedef struct DaemonConf {
|
typedef struct DaemonConf {
|
||||||
@@ -69,6 +87,16 @@ typedef struct DaemonConf {
|
|||||||
} DaemonConf;
|
} DaemonConf;
|
||||||
|
|
||||||
#define DAEMON_CONF_DEFAULT_PORT 873
|
#define DAEMON_CONF_DEFAULT_PORT 873
|
||||||
|
/* Default global connection cap when `max connections` is absent. Matches the
|
||||||
|
* historical hardcoded listener value. */
|
||||||
|
#define DAEMON_CONF_DEFAULT_MAX_CONNECTIONS 100
|
||||||
|
/* Default `auth failure delay` in milliseconds (0 disables the throttle). */
|
||||||
|
#define DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS 500
|
||||||
|
/* Largest accepted `auth failure delay`, so a typo cannot pin a connection
|
||||||
|
* child in nanosleep for an absurd time. */
|
||||||
|
/* Bounded well below the socket I/O timeout so a failed-auth child cannot hold
|
||||||
|
* a connection slot for long enough to amplify connection-cap exhaustion. */
|
||||||
|
#define DAEMON_CONF_MAX_AUTH_FAILURE_DELAY_MS 5000
|
||||||
/* Longest accepted config line (excluding the trailing newline). Longer lines
|
/* Longest accepted config line (excluding the trailing newline). Longer lines
|
||||||
* are rejected rather than buffered unboundedly. */
|
* are rejected rather than buffered unboundedly. */
|
||||||
#define DAEMON_CONF_MAX_LINE 4096
|
#define DAEMON_CONF_MAX_LINE 4096
|
||||||
@@ -99,9 +127,30 @@ const DaemonModule* daemon_conf_find_module(const DaemonConf* conf, const char*
|
|||||||
bool daemon_module_name_valid(const char* name);
|
bool daemon_module_name_valid(const char* name);
|
||||||
|
|
||||||
/* Parse one --dparam=KEY=VALUE (or "--dparam KEY=VALUE") override string and
|
/* Parse one --dparam=KEY=VALUE (or "--dparam KEY=VALUE") override string and
|
||||||
* apply it to the global scalars only. Keys are case-insensitive and limited
|
* apply it to the global keys only. Keys are case-insensitive and limited to
|
||||||
* to the global scalar keys defined by the grammar (port, motd file, address).
|
* the global keys defined by the grammar (port, motd file, address,
|
||||||
* Returns 0 on success, -1 on error (err filled). */
|
* max connections, auth failure delay, hosts allow, hosts deny). Returns 0 on
|
||||||
|
* success, -1 on error (err filled). */
|
||||||
int daemon_conf_apply_dparam(DaemonConf* conf, const char* assignment, char* err, size_t err_size);
|
int daemon_conf_apply_dparam(DaemonConf* conf, const char* assignment, char* err, size_t err_size);
|
||||||
|
|
||||||
|
/* Host access-control matching (pure; no I/O). `daemon_host_pattern_match`
|
||||||
|
* matches one configured pattern against a numeric peer IP string. Supported
|
||||||
|
* patterns: `*` (match anything), an IPv4/IPv6 literal, an IPv4/IPv6 CIDR
|
||||||
|
* (`10.0.0.0/8`, `2001:db8::/32`), or a glob (`*.example.com`) evaluated with
|
||||||
|
* the same matcher as file globs; a glob only matches a peer string of the
|
||||||
|
* same shape, so a numeric peer never matches a hostname glob. */
|
||||||
|
bool daemon_host_pattern_match(const char* pattern, const char* peer_ip);
|
||||||
|
|
||||||
|
/* rsync-like combined decision over a deny list and an allow list: a matching
|
||||||
|
* deny rejects (deny takes precedence); otherwise, when any allow entries
|
||||||
|
* exist, a peer that matches none is rejected; with no allow entries every
|
||||||
|
* peer not denied is accepted. An empty/unset pair returns true. */
|
||||||
|
bool daemon_hosts_allowed(const char* peer_ip, char* const* allow, int allow_count,
|
||||||
|
char* const* deny, int deny_count);
|
||||||
|
|
||||||
|
/* True when at least one allow or deny pattern is configured (i.e. an
|
||||||
|
* unprovable peer must fail closed rather than being treated as unrestricted). */
|
||||||
|
bool daemon_hosts_restricted(char* const* allow, int allow_count, char* const* deny,
|
||||||
|
int deny_count);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -51,7 +52,8 @@ static int normalize_entry(const char* raw, size_t len, bool strip_line_endings,
|
|||||||
if (len == 0)
|
if (len == 0)
|
||||||
return 0;
|
return 0;
|
||||||
if (raw[0] == '/') {
|
if (raw[0] == '/') {
|
||||||
snprintf(err, err_size, "absolute path entries are not allowed: '%.*s'", (int)len, raw);
|
int print_len = len > (size_t)INT_MAX ? INT_MAX : (int)len;
|
||||||
|
snprintf(err, err_size, "absolute path entries are not allowed: '%.*s'", print_len, raw);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
/* Reject NUL bytes inside a token defensively (NUL-delimited mode splits on
|
/* Reject NUL bytes inside a token defensively (NUL-delimited mode splits on
|
||||||
|
|||||||
@@ -1696,9 +1696,9 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has_path_traversal(check_path)) {
|
if (check_path[0] == '\0' || has_path_traversal(check_path)) {
|
||||||
char* escaped_path = output_escape(check_path, log_get_8_bit_output());
|
char* escaped_path = output_escape(check_path, log_get_8_bit_output());
|
||||||
log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s",
|
log_message(LOG_LEVEL_ERROR, "Invalid received check path: %s",
|
||||||
escaped_path ? escaped_path : "<allocation failed>");
|
escaped_path ? escaped_path : "<allocation failed>");
|
||||||
free(escaped_path);
|
free(escaped_path);
|
||||||
free(check_path);
|
free(check_path);
|
||||||
@@ -1832,6 +1832,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
|||||||
existing/ignore-existing/update/backup/delay-updates policy. */
|
existing/ignore-existing/update/backup/delay-updates policy. */
|
||||||
File* materialized = file_create(check_path);
|
File* materialized = file_create(check_path);
|
||||||
if (materialized && basis.content) {
|
if (materialized && basis.content) {
|
||||||
|
data_destroy(materialized->data);
|
||||||
materialized->data = basis.content;
|
materialized->data = basis.content;
|
||||||
basis.content = NULL;
|
basis.content = NULL;
|
||||||
materialized->metadata = file_metadata_create(NULL, &basis.st, false, false);
|
materialized->metadata = file_metadata_create(NULL, &basis.st, false, false);
|
||||||
@@ -2095,6 +2096,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
|||||||
file->metadata = meta;
|
file->metadata = meta;
|
||||||
file->xattrs = append_xattrs;
|
file->xattrs = append_xattrs;
|
||||||
append_xattrs = NULL;
|
append_xattrs = NULL;
|
||||||
|
data_destroy(file->data);
|
||||||
file->data = data_create(full, full_size);
|
file->data = data_create(full, full_size);
|
||||||
if (!file->data) { /* data_create already freed full on failure */
|
if (!file->data) { /* data_create already freed full on failure */
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
@@ -2236,6 +2238,10 @@ File* file_receive(const Config* config, int file_descriptor) {
|
|||||||
|
|
||||||
/* ---- P7 Wave D: deferred directory times ---- */
|
/* ---- P7 Wave D: deferred directory times ---- */
|
||||||
|
|
||||||
|
bool dir_times_should_capture(const Config* config) {
|
||||||
|
return config->use_metadata && !config->omit_dir_times;
|
||||||
|
}
|
||||||
|
|
||||||
void dir_time_list_init(DirTimeList* list) {
|
void dir_time_list_init(DirTimeList* list) {
|
||||||
if (!list)
|
if (!list)
|
||||||
return;
|
return;
|
||||||
@@ -2243,6 +2249,7 @@ void dir_time_list_init(DirTimeList* list) {
|
|||||||
list->entries = NULL;
|
list->entries = NULL;
|
||||||
list->count = 0;
|
list->count = 0;
|
||||||
list->capacity = 0;
|
list->capacity = 0;
|
||||||
|
list->bytes = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dir_time_list_free(DirTimeList* list) {
|
void dir_time_list_free(DirTimeList* list) {
|
||||||
@@ -2256,11 +2263,23 @@ void dir_time_list_free(DirTimeList* list) {
|
|||||||
list->entries = NULL;
|
list->entries = NULL;
|
||||||
list->count = 0;
|
list->count = 0;
|
||||||
list->capacity = 0;
|
list->capacity = 0;
|
||||||
|
list->bytes = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata) {
|
bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata) {
|
||||||
if (!list || !wire_path || !metadata)
|
if (!list || !wire_path || !metadata)
|
||||||
return true; /* nothing to remember; never a hard error */
|
return true; /* nothing to remember; never a hard error */
|
||||||
|
/* Cumulative, not per-frame: the sender may stream a tree across unbounded
|
||||||
|
STATUS_DIR_TIMES frames, so bound the TOTAL retained here. Reject before
|
||||||
|
touching the list, leaving it exactly as it was (the caller fails the
|
||||||
|
transfer, which becomes a clean protocol error). */
|
||||||
|
size_t path_len = strlen(wire_path);
|
||||||
|
/* Charge the whole per-entry cost (path copy + pointer slot + metadata
|
||||||
|
struct), not just the path, so the array growth is bounded by the same
|
||||||
|
cumulative budget. */
|
||||||
|
size_t entry_cost = path_len + sizeof(FileMetadata) + sizeof(char*);
|
||||||
|
if (list->count >= MAX_DIR_TIME_ENTRIES || entry_cost > MAX_DIR_TIME_BYTES - list->bytes)
|
||||||
|
return false;
|
||||||
if (list->count == list->capacity) {
|
if (list->count == list->capacity) {
|
||||||
size_t new_capacity = list->capacity == 0 ? 16 : list->capacity * 2;
|
size_t new_capacity = list->capacity == 0 ? 16 : list->capacity * 2;
|
||||||
if (new_capacity < list->capacity)
|
if (new_capacity < list->capacity)
|
||||||
@@ -2287,6 +2306,7 @@ bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetad
|
|||||||
list->paths[list->count] = copy;
|
list->paths[list->count] = copy;
|
||||||
list->entries[list->count] = *metadata;
|
list->entries[list->count] = *metadata;
|
||||||
list->count++;
|
list->count++;
|
||||||
|
list->bytes += entry_cost;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,15 @@
|
|||||||
|
|
||||||
/* Server-side file receive/save path. */
|
/* Server-side file receive/save path. */
|
||||||
|
|
||||||
|
/* Cumulative caps for the deferred directory-time accumulator. The sender may
|
||||||
|
* legitimately split a large tree across repeated STATUS_DIR_TIMES frames, so a
|
||||||
|
* per-frame bound is not enough: the receiver must bound the TOTAL it retains
|
||||||
|
* against a hostile sender. Mirror the delete-manifest limits
|
||||||
|
* (MAX_MANIFEST_ENTRIES / MAX_MANIFEST_BYTES): the entry count bounds the
|
||||||
|
* metadata array and the byte budget bounds the concatenated path strings. */
|
||||||
|
#define MAX_DIR_TIME_ENTRIES (1024 * 1024)
|
||||||
|
#define MAX_DIR_TIME_BYTES (16ULL * 1024 * 1024)
|
||||||
|
|
||||||
File* file_receive(const Config* config, int file_descriptor);
|
File* file_receive(const Config* config, int file_descriptor);
|
||||||
File* file_receive_directory(int file_descriptor, const Config* config);
|
File* file_receive_directory(int file_descriptor, const Config* config);
|
||||||
File* file_receive_dir_time(int file_descriptor, const Config* config);
|
File* file_receive_dir_time(int file_descriptor, const Config* config);
|
||||||
@@ -28,12 +37,20 @@ typedef struct {
|
|||||||
FileMetadata* entries; /* owned, parallel to paths */
|
FileMetadata* entries; /* owned, parallel to paths */
|
||||||
size_t count;
|
size_t count;
|
||||||
size_t capacity;
|
size_t capacity;
|
||||||
|
size_t bytes; /* cumulative strlen of every retained path */
|
||||||
} DirTimeList;
|
} DirTimeList;
|
||||||
|
|
||||||
|
/* Capture gate shared by the sender-side and receiver-side sinks: directory
|
||||||
|
* metadata is accumulated only when --times/--metadata is in effect and
|
||||||
|
* -O/--omit-dir-times does not suppress it. Kept here, next to the accumulator
|
||||||
|
* it guards, so both call sites express the same condition. */
|
||||||
|
bool dir_times_should_capture(const Config* config);
|
||||||
|
|
||||||
void dir_time_list_init(DirTimeList* list);
|
void dir_time_list_init(DirTimeList* list);
|
||||||
void dir_time_list_free(DirTimeList* list);
|
void dir_time_list_free(DirTimeList* list);
|
||||||
/* Deep-copy one directory's path + metadata into the list. Returns false on
|
/* Deep-copy one directory's path + metadata into the list. Returns false on
|
||||||
* allocation failure (the caller fails the transfer). */
|
* allocation failure OR when the cumulative entry/byte caps would be exceeded
|
||||||
|
* (the caller fails the transfer). */
|
||||||
bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata);
|
bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata);
|
||||||
/* Apply every accumulated directory's mtime (and atime when captured) beneath
|
/* Apply every accumulated directory's mtime (and atime when captured) beneath
|
||||||
* `root_directory`, confined fd-relative. Best-effort per entry: an absent
|
* `root_directory`, confined fd-relative. Best-effort per entry: an absent
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta
|
|||||||
off_t offset = 0;
|
off_t offset = 0;
|
||||||
struct timespec deadline;
|
struct timespec deadline;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||||
deadline.tv_sec += 60;
|
deadline.tv_sec += protocol_get_io_timeout_sec();
|
||||||
while ((unsigned long long)offset < file_size) {
|
while ((unsigned long long)offset < file_size) {
|
||||||
struct timespec now;
|
struct timespec now;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ typedef struct {
|
|||||||
/* --super / --no-super tri-state (SUPER_MODE_AUTO when unset). Snapshotted
|
/* --super / --no-super tri-state (SUPER_MODE_AUTO when unset). Snapshotted
|
||||||
* per connection so privilege_super_permitted() can gate super-user
|
* per connection so privilege_super_permitted() can gate super-user
|
||||||
* activities without a Config argument. */
|
* activities without a Config argument. */
|
||||||
int super_mode;
|
SuperMode super_mode;
|
||||||
/* --copy-as=USER[:GROUP]: snapshotted so the ownership resolver can force the
|
/* --copy-as=USER[:GROUP]: snapshotted so the ownership resolver can force the
|
||||||
* target ids without a Config argument. */
|
* target ids without a Config argument. */
|
||||||
bool copy_as_set;
|
bool copy_as_set;
|
||||||
@@ -128,7 +128,7 @@ bool privilege_super_permitted(void) {
|
|||||||
return privilege_super_mode_permitted(g_identity.super_mode);
|
return privilege_super_mode_permitted(g_identity.super_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool privilege_super_mode_permitted(int mode) {
|
bool privilege_super_mode_permitted(SuperMode mode) {
|
||||||
/* AUTO and ON both attempt the confined operation; OFF forbids it even for a
|
/* AUTO and ON both attempt the confined operation; OFF forbids it even for a
|
||||||
* root receiver. AUTO is the historical FastSync behavior (always attempt
|
* root receiver. AUTO is the historical FastSync behavior (always attempt
|
||||||
* and let the kernel refuse an unprivileged call, which the caller skips), so
|
* and let the kernel refuse an unprivileged call, which the caller skips), so
|
||||||
|
|||||||
@@ -128,6 +128,6 @@ bool identity_wire_valid(const Config* config);
|
|||||||
* best-effort behavior where an unprivileged attempt is refused by the kernel
|
* best-effort behavior where an unprivileged attempt is refused by the kernel
|
||||||
* and skipped. Neither EVER elevates privileges. */
|
* and skipped. Neither EVER elevates privileges. */
|
||||||
bool privilege_super_permitted(void);
|
bool privilege_super_permitted(void);
|
||||||
bool privilege_super_mode_permitted(int mode);
|
bool privilege_super_mode_permitted(SuperMode mode);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+73
-27
@@ -3,7 +3,9 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <threads.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
|
static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
|
||||||
@@ -15,6 +17,18 @@ static FILE* log_fp = NULL;
|
|||||||
static _Thread_local bool eight_bit_output;
|
static _Thread_local bool eight_bit_output;
|
||||||
static LogStderrMode stderr_mode = LOG_STDERR_ERRORS;
|
static LogStderrMode stderr_mode = LOG_STDERR_ERRORS;
|
||||||
|
|
||||||
|
/* Serializes access to log_fp and makes each emitted line atomic: the
|
||||||
|
* timestamp prefix, formatted body, and trailing newline are written as one
|
||||||
|
* critical section so concurrent threads cannot interleave partial lines.
|
||||||
|
* Initialized lazily (matching the protocol.c bw_mutex idiom) because logging
|
||||||
|
* can happen before main() installs any synchronization. */
|
||||||
|
static mtx_t log_mutex;
|
||||||
|
static once_flag log_mutex_once = ONCE_FLAG_INIT;
|
||||||
|
|
||||||
|
static void log_mutex_init(void) {
|
||||||
|
mtx_init(&log_mutex, mtx_plain);
|
||||||
|
}
|
||||||
|
|
||||||
void set_log_level(LogLevel level) {
|
void set_log_level(LogLevel level) {
|
||||||
current_log_level = level;
|
current_log_level = level;
|
||||||
}
|
}
|
||||||
@@ -41,7 +55,10 @@ uint32_t get_log_info_flags(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void log_set_file(FILE* fp) {
|
void log_set_file(FILE* fp) {
|
||||||
|
call_once(&log_mutex_once, log_mutex_init);
|
||||||
|
mtx_lock(&log_mutex);
|
||||||
log_fp = fp;
|
log_fp = fp;
|
||||||
|
mtx_unlock(&log_mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_set_8_bit_output(bool enabled) {
|
void log_set_8_bit_output(bool enabled) {
|
||||||
@@ -60,13 +77,48 @@ LogStderrMode log_get_stderr_mode(void) {
|
|||||||
return stderr_mode;
|
return stderr_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void write_message(FILE* dest_io, LogLevel log_level, struct tm t, const char* format,
|
/* Format one complete log line (timestamp prefix + body + newline) into a
|
||||||
va_list args) {
|
* freshly allocated buffer. This is pure CPU/malloc work and must happen
|
||||||
fprintf(dest_io, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1,
|
* OUTSIDE the log mutex: the mutex only guards the log_fp pointer, so a
|
||||||
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]);
|
* stalled stderr/stdout pipe cannot block every logging thread. Returns NULL
|
||||||
|
* on allocation/formatting failure. */
|
||||||
|
static char* format_log_line(LogLevel log_level, const struct tm* t, const char* format,
|
||||||
|
va_list args) {
|
||||||
|
char prefix[64];
|
||||||
|
int prefix_len = snprintf(
|
||||||
|
prefix, sizeof(prefix), "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t->tm_year + 1900,
|
||||||
|
t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, log_level_strings[log_level]);
|
||||||
|
if (prefix_len < 0 || prefix_len >= (int)sizeof(prefix))
|
||||||
|
return NULL;
|
||||||
|
va_list copy;
|
||||||
|
va_copy(copy, args);
|
||||||
|
int body_len = vsnprintf(NULL, 0, format, copy);
|
||||||
|
va_end(copy);
|
||||||
|
if (body_len < 0)
|
||||||
|
return NULL;
|
||||||
|
size_t total = (size_t)prefix_len + (size_t)body_len;
|
||||||
|
char* line = malloc(total + 2); /* body bytes + '\n' + NUL */
|
||||||
|
if (!line)
|
||||||
|
return NULL;
|
||||||
|
memcpy(line, prefix, (size_t)prefix_len);
|
||||||
|
vsnprintf(line + prefix_len, (size_t)body_len + 1, format, args);
|
||||||
|
line[total] = '\n';
|
||||||
|
line[total + 1] = '\0';
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
vfprintf(dest_io, format, args);
|
/* Write an already-formatted line to the console and, if configured, the log
|
||||||
fprintf(dest_io, "\n");
|
* file. Only the log_fp pointer is read under the mutex (so log_set_file /
|
||||||
|
* config_delete cannot free it while it is in use); the single console fputs
|
||||||
|
* runs unlocked but is internally atomic per stdio stream. */
|
||||||
|
static void emit_log_line(FILE* console, const char* line) {
|
||||||
|
fputs(line, console);
|
||||||
|
call_once(&log_mutex_once, log_mutex_init);
|
||||||
|
mtx_lock(&log_mutex);
|
||||||
|
FILE* file = log_fp;
|
||||||
|
if (file)
|
||||||
|
fputs(line, file);
|
||||||
|
mtx_unlock(&log_mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_message(LogLevel log_level, const char* format, ...) {
|
void log_message(LogLevel log_level, const char* format, ...) {
|
||||||
@@ -86,14 +138,12 @@ void log_message(LogLevel log_level, const char* format, ...) {
|
|||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
write_message(dest_io, log_level, t, format, args);
|
char* line = format_log_line(log_level, &t, format, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
if (!line)
|
||||||
if (log_fp) {
|
return;
|
||||||
va_start(args, format);
|
emit_log_line(dest_io, line);
|
||||||
write_message(log_fp, log_level, t, format, args);
|
free(line);
|
||||||
va_end(args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_debug_message(LogDebugFlag flag, const char* format, ...) {
|
void log_debug_message(LogDebugFlag flag, const char* format, ...) {
|
||||||
@@ -107,14 +157,12 @@ void log_debug_message(LogDebugFlag flag, const char* format, ...) {
|
|||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
write_message(stdout, LOG_LEVEL_DEBUG, t, format, args);
|
char* line = format_log_line(LOG_LEVEL_DEBUG, &t, format, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
if (!line)
|
||||||
if (log_fp) {
|
return;
|
||||||
va_start(args, format);
|
emit_log_line(stdout, line);
|
||||||
write_message(log_fp, LOG_LEVEL_DEBUG, t, format, args);
|
free(line);
|
||||||
va_end(args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_info_message(LogInfoFlag flag, const char* format, ...) {
|
void log_info_message(LogInfoFlag flag, const char* format, ...) {
|
||||||
@@ -129,14 +177,12 @@ void log_info_message(LogInfoFlag flag, const char* format, ...) {
|
|||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
write_message(stdout, LOG_LEVEL_INFO, t, format, args);
|
char* line = format_log_line(LOG_LEVEL_INFO, &t, format, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
if (!line)
|
||||||
if (log_fp) {
|
return;
|
||||||
va_start(args, format);
|
emit_log_line(stdout, line);
|
||||||
write_message(log_fp, LOG_LEVEL_INFO, t, format, args);
|
free(line);
|
||||||
va_end(args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_perror(const char* context) {
|
void log_perror(const char* context) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
#include "file_receive.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
@@ -331,7 +332,7 @@ int write_thread(void* pipeline_context) {
|
|||||||
write would clobber them); accumulate the metadata here and let the
|
write would clobber them); accumulate the metadata here and let the
|
||||||
caller apply it once every writer has drained. */
|
caller apply it once every writer has drained. */
|
||||||
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||||
context->config->use_metadata && !context->config->omit_dir_times &&
|
dir_times_should_capture(context->config) &&
|
||||||
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||||
|
|||||||
+19
-2
@@ -76,10 +76,23 @@ void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd)
|
|||||||
session->read_fd = read_fd;
|
session->read_fd = read_fd;
|
||||||
session->write_fd = write_fd;
|
session->write_fd = write_fd;
|
||||||
session->max_alloc = DEFAULT_MAX_ALLOC;
|
session->max_alloc = DEFAULT_MAX_ALLOC;
|
||||||
|
session->io_timeout_sec = RECEIVE_TIMEOUT_SEC;
|
||||||
atomic_init(&session->total_allocated_bytes, 0);
|
atomic_init(&session->total_allocated_bytes, 0);
|
||||||
protocol_session_set_bwlimit(session, global_bwlimit());
|
protocol_session_set_bwlimit(session, global_bwlimit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void protocol_session_set_io_timeout(ProtocolSession* session, int sec) {
|
||||||
|
if (!session)
|
||||||
|
return;
|
||||||
|
session->io_timeout_sec = sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
int protocol_get_io_timeout_sec(void) {
|
||||||
|
const ProtocolSession* session = bound_session ? bound_session : &legacy_io_session;
|
||||||
|
int sec = session->io_timeout_sec;
|
||||||
|
return sec > 0 ? sec : RECEIVE_TIMEOUT_SEC;
|
||||||
|
}
|
||||||
|
|
||||||
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc) {
|
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc) {
|
||||||
if (!session)
|
if (!session)
|
||||||
session = bound_session ? bound_session : &legacy_io_session;
|
session = bound_session ? bound_session : &legacy_io_session;
|
||||||
@@ -257,10 +270,11 @@ bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t dat
|
|||||||
log_debug_message(LOG_DEBUG_IO, " Sending n Data: %zu", data_size);
|
log_debug_message(LOG_DEBUG_IO, " Sending n Data: %zu", data_size);
|
||||||
if (!session)
|
if (!session)
|
||||||
return false;
|
return false;
|
||||||
|
int timeout_sec = session->io_timeout_sec > 0 ? session->io_timeout_sec : SEND_TIMEOUT_SEC;
|
||||||
int fd = session->write_fd;
|
int fd = session->write_fd;
|
||||||
struct timespec deadline;
|
struct timespec deadline;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||||
deadline.tv_sec += SEND_TIMEOUT_SEC;
|
deadline.tv_sec += timeout_sec;
|
||||||
short wait_events = POLLOUT;
|
short wait_events = POLLOUT;
|
||||||
ssize_t total_bytes_send = 0;
|
ssize_t total_bytes_send = 0;
|
||||||
while ((size_t)total_bytes_send < data_size) {
|
while ((size_t)total_bytes_send < data_size) {
|
||||||
@@ -306,7 +320,10 @@ bool protocol_receive_n_data_timed(ProtocolSession* session, void* data, size_t
|
|||||||
int timeout_sec);
|
int timeout_sec);
|
||||||
|
|
||||||
bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size) {
|
bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size) {
|
||||||
return protocol_receive_n_data_timed(session, data, data_size, RECEIVE_TIMEOUT_SEC);
|
/* Honor the session's configured deadline; protocol_receive_n_data_timed
|
||||||
|
* re-applies the built-in 60 s default when the value is <= 0. */
|
||||||
|
int timeout_sec = session ? session->io_timeout_sec : 0;
|
||||||
|
return protocol_receive_n_data_timed(session, data, data_size, timeout_sec);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool protocol_receive_n_data_timed(ProtocolSession* session, void* data, size_t data_size,
|
bool protocol_receive_n_data_timed(ProtocolSession* session, void* data, size_t data_size,
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ typedef struct ProtocolSession {
|
|||||||
atomic_ullong total_allocated_bytes;
|
atomic_ullong total_allocated_bytes;
|
||||||
bool eight_bit_output;
|
bool eight_bit_output;
|
||||||
unsigned long long max_alloc;
|
unsigned long long max_alloc;
|
||||||
|
/* Per-session deadline (seconds) applied to every protocol send/receive by
|
||||||
|
* protocol_send_n_data / protocol_receive_n_data. Defaults to the built-in
|
||||||
|
* 60 s window; a value <= 0 falls back to that default. Set from the
|
||||||
|
* negotiated Config->timeout so --timeout is honored by the poll()-driven
|
||||||
|
* protocol I/O, not just the socket SO_RCVTIMEO/SO_SNDTIMEO. */
|
||||||
|
int io_timeout_sec;
|
||||||
} ProtocolSession;
|
} ProtocolSession;
|
||||||
|
|
||||||
typedef int Status;
|
typedef int Status;
|
||||||
@@ -141,6 +147,15 @@ void protocol_session_unbind(void);
|
|||||||
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl);
|
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl);
|
||||||
void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec);
|
void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec);
|
||||||
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc);
|
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc);
|
||||||
|
/* Override the per-message send/receive deadline for this session.
|
||||||
|
* `sec` <= 0 restores the built-in 60 s default (used for --timeout=0/unset).
|
||||||
|
* An explicit long deadline (e.g. the delete-ack wait) is applied per-call by
|
||||||
|
* protocol_receive_status_timed and is unaffected by this setter. */
|
||||||
|
void protocol_session_set_io_timeout(ProtocolSession* session, int sec);
|
||||||
|
/* Effective per-message I/O deadline (seconds) for the currently-bound session,
|
||||||
|
* falling back to the built-in default. Used by the plaintext sendfile path
|
||||||
|
* which bypasses the protocol send primitive. */
|
||||||
|
int protocol_get_io_timeout_sec(void);
|
||||||
void* protocol_alloc(size_t size);
|
void* protocol_alloc(size_t size);
|
||||||
void* protocol_realloc(void* ptr, size_t size);
|
void* protocol_realloc(void* ptr, size_t size);
|
||||||
void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled);
|
void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled);
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ char* ssh_build_remote_command(const char* server_path, bool old_args, char* con
|
|||||||
q++;
|
q++;
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
if (len > SIZE_MAX - q * 3 || len + q * 3 + 3 > SIZE_MAX - command_len)
|
if (q > (SIZE_MAX - len) / 3 || len + q * 3 + 3 > SIZE_MAX - command_len)
|
||||||
return NULL;
|
return NULL;
|
||||||
command_len += len + q * 3 + 3;
|
command_len += len + q * 3 + 3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ Server* server_create(int port) {
|
|||||||
return server_create_ex(port, NULL);
|
return server_create_ex(port, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void server_set_max_connections(Server* server, unsigned int max_connections) {
|
||||||
|
if (server && max_connections > 0)
|
||||||
|
server->max_connections = max_connections;
|
||||||
|
}
|
||||||
|
|
||||||
void server_delete(Server** server) {
|
void server_delete(Server** server) {
|
||||||
if (server == NULL || *server == NULL)
|
if (server == NULL || *server == NULL)
|
||||||
return;
|
return;
|
||||||
@@ -135,7 +140,7 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
|||||||
}
|
}
|
||||||
signal(SIGCHLD, sigchld_handler);
|
signal(SIGCHLD, sigchld_handler);
|
||||||
while (1) {
|
while (1) {
|
||||||
struct sockaddr_in client_addr;
|
struct sockaddr_storage client_addr;
|
||||||
socklen_t client_len = sizeof(client_addr);
|
socklen_t client_len = sizeof(client_addr);
|
||||||
int fd = accept(server->file_descriptor, (struct sockaddr*)&client_addr, &client_len);
|
int fd = accept(server->file_descriptor, (struct sockaddr*)&client_addr, &client_len);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
@@ -143,18 +148,30 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tcp_apply_socket_timeout(fd);
|
tcp_apply_socket_timeout(fd);
|
||||||
|
char peer[128];
|
||||||
|
if (!utils_sockaddr_to_string((const struct sockaddr*)&client_addr, peer, sizeof(peer)))
|
||||||
|
snprintf(peer, sizeof(peer), "unknown");
|
||||||
if ((unsigned int)g_active_connections >= server->max_connections) {
|
if ((unsigned int)g_active_connections >= server->max_connections) {
|
||||||
log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting",
|
log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting %s",
|
||||||
server->max_connections);
|
server->max_connections, peer);
|
||||||
close(fd);
|
close(fd);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
log_message(LOG_LEVEL_INFO, "%s", log_fmt);
|
log_message(LOG_LEVEL_INFO, "%s from %s", log_fmt, peer);
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
|
/* Connection children must not run the parent's global cleanup(): it
|
||||||
|
* frees state (credentials / daemon conf) that the child's worker
|
||||||
|
* threads may still be reading and closes fd numbers the child could
|
||||||
|
* already have reused. Reset the inherited handlers so a signal
|
||||||
|
* terminates the child directly; SIGCHLD is reset too since a child
|
||||||
|
* must never reap the parent's children. This runs before the child
|
||||||
|
* spawns any thread, so it cannot race one. */
|
||||||
|
signal(SIGINT, SIG_DFL);
|
||||||
|
signal(SIGTERM, SIG_DFL);
|
||||||
|
signal(SIGCHLD, SIG_DFL);
|
||||||
close(server->file_descriptor);
|
close(server->file_descriptor);
|
||||||
child_fn(fd, child_ctx);
|
child_fn(fd, child_ctx);
|
||||||
close(fd);
|
|
||||||
_exit(0);
|
_exit(0);
|
||||||
} else if (pid > 0) {
|
} else if (pid > 0) {
|
||||||
g_active_connections++;
|
g_active_connections++;
|
||||||
@@ -169,6 +186,9 @@ struct plain_ctx {
|
|||||||
|
|
||||||
static void plain_child_fn(int fd, void* ctx) {
|
static void plain_child_fn(int fd, void* ctx) {
|
||||||
((struct plain_ctx*)ctx)->handler(fd);
|
((struct plain_ctx*)ctx)->handler(fd);
|
||||||
|
/* handler() never closes the connection fd; the child owns its single
|
||||||
|
* close here after the handler has fully torn down. */
|
||||||
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool server_listen(Server* server, void (*handler)(int file_descriptor)) {
|
bool server_listen(Server* server, void (*handler)(int file_descriptor)) {
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ typedef struct {
|
|||||||
|
|
||||||
Server* server_create_ex(int port, const ServerBindOptions* bind_opts);
|
Server* server_create_ex(int port, const ServerBindOptions* bind_opts);
|
||||||
Server* server_create(int port);
|
Server* server_create(int port);
|
||||||
|
/* Override the listener's connection cap (the global daemon `max connections`
|
||||||
|
* value). A non-positive value is ignored so the default cap stands. */
|
||||||
|
void server_set_max_connections(Server* server, unsigned int max_connections);
|
||||||
bool server_listen(Server* server, void (*handler)(int file_descriptor));
|
bool server_listen(Server* server, void (*handler)(int file_descriptor));
|
||||||
void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
|
void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
|
||||||
const char* log_fmt);
|
const char* log_fmt);
|
||||||
|
|||||||
@@ -65,6 +65,18 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
|
|||||||
SSL_CTX_free(ctx);
|
SSL_CTX_free(ctx);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
/* TLS 1.3 ciphersuites are configured separately from the TLS 1.2 and below
|
||||||
|
* cipher list above. Pin the three AEAD suites OpenSSL offers, dropping
|
||||||
|
* TLS_AES_128_CCM_SHA256 and the CCM_8 variant, and fail closed if the
|
||||||
|
* library rejects the policy. SSL_CTX_set_ciphersuites needs OpenSSL 1.1.1;
|
||||||
|
* earlier versions have no TLS 1.3, so the call is compile-guarded. */
|
||||||
|
#if OPENSSL_VERSION_NUMBER >= 0x10101000L
|
||||||
|
if (SSL_CTX_set_ciphersuites(
|
||||||
|
ctx, "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256") != 1) {
|
||||||
|
SSL_CTX_free(ctx);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (cert && key) {
|
if (cert && key) {
|
||||||
struct stat key_stat;
|
struct stat key_stat;
|
||||||
@@ -180,13 +192,18 @@ static void tls_child_fn(int fd, void* arg) {
|
|||||||
SSL* ssl = wrap_fd_with_ssl(fd, ctx->ssl_ctx, true, NULL);
|
SSL* ssl = wrap_fd_with_ssl(fd, ctx->ssl_ctx, true, NULL);
|
||||||
if (!ssl) {
|
if (!ssl) {
|
||||||
io_set_ssl(NULL);
|
io_set_ssl(NULL);
|
||||||
|
close(fd);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
io_set_ssl(ssl);
|
io_set_ssl(ssl);
|
||||||
ctx->handler(fd);
|
ctx->handler(fd);
|
||||||
|
/* Shut the TLS layer down before releasing the fd: handler() no longer
|
||||||
|
* closes it, so SSL_shutdown still has a valid socket. The child owns the
|
||||||
|
* single fd close, performed last. */
|
||||||
SSL_shutdown(ssl);
|
SSL_shutdown(ssl);
|
||||||
SSL_free(ssl);
|
SSL_free(ssl);
|
||||||
io_set_ssl(NULL);
|
io_set_ssl(NULL);
|
||||||
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool server_listen_tls(Server* server, void (*handler)(int file_descriptor)) {
|
bool server_listen_tls(Server* server, void (*handler)(int file_descriptor)) {
|
||||||
|
|||||||
@@ -587,6 +587,71 @@ bool utils_fd_peer_is_local(int fd) {
|
|||||||
return utils_sockaddr_is_loopback((const struct sockaddr*)&peer);
|
return utils_sockaddr_is_loopback((const struct sockaddr*)&peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Numeric peer address of a connected fd. Only AF_INET/AF_INET6 peers are
|
||||||
|
formatted; every other descriptor/family (pipe, AF_UNIX socketpair, ...) or a
|
||||||
|
getpeername failure returns false with buf emptied. The caller must treat
|
||||||
|
that as "cannot tell". */
|
||||||
|
bool utils_fd_peer_ip(int fd, char* buf, size_t len) {
|
||||||
|
if (!buf || len == 0)
|
||||||
|
return false;
|
||||||
|
buf[0] = '\0';
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
struct sockaddr_storage peer;
|
||||||
|
socklen_t peer_len = sizeof(peer);
|
||||||
|
if (getpeername(fd, (struct sockaddr*)&peer, &peer_len) != 0)
|
||||||
|
return false;
|
||||||
|
const void* src = NULL;
|
||||||
|
int family = peer.ss_family;
|
||||||
|
if (family == AF_INET) {
|
||||||
|
src = &((const struct sockaddr_in*)&peer)->sin_addr;
|
||||||
|
} else if (family == AF_INET6) {
|
||||||
|
const struct sockaddr_in6* peer6 = (const struct sockaddr_in6*)&peer;
|
||||||
|
/* A dual-stack IPv6 listener reports IPv4 peers as ::ffff:a.b.c.d. Emit
|
||||||
|
* the IPv4 form so IPv4 ACL patterns (and logs) see the real address. */
|
||||||
|
if (IN6_IS_ADDR_V4MAPPED(&peer6->sin6_addr)) {
|
||||||
|
struct in_addr v4;
|
||||||
|
memcpy(&v4, &peer6->sin6_addr.s6_addr[12], sizeof(v4));
|
||||||
|
return inet_ntop(AF_INET, &v4, buf, (socklen_t)len) != NULL;
|
||||||
|
}
|
||||||
|
src = &peer6->sin6_addr;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return inet_ntop(family, src, buf, (socklen_t)len) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "ip:port" / "[ip]:port" for a connected peer, used to log the connecting
|
||||||
|
address in the accept loop. Returns false for a non-INET family. */
|
||||||
|
bool utils_sockaddr_to_string(const struct sockaddr* addr, char* buf, size_t len) {
|
||||||
|
if (!addr || !buf || len == 0)
|
||||||
|
return false;
|
||||||
|
buf[0] = '\0';
|
||||||
|
char ip[INET6_ADDRSTRLEN];
|
||||||
|
unsigned short port;
|
||||||
|
int written;
|
||||||
|
if (addr->sa_family == AF_INET) {
|
||||||
|
const struct sockaddr_in* v4 = (const struct sockaddr_in*)addr;
|
||||||
|
if (!inet_ntop(AF_INET, &v4->sin_addr, ip, sizeof(ip)))
|
||||||
|
return false;
|
||||||
|
port = ntohs(v4->sin_port);
|
||||||
|
written = snprintf(buf, len, "%s:%u", ip, port);
|
||||||
|
} else if (addr->sa_family == AF_INET6) {
|
||||||
|
const struct sockaddr_in6* v6 = (const struct sockaddr_in6*)addr;
|
||||||
|
if (!inet_ntop(AF_INET6, &v6->sin6_addr, ip, sizeof(ip)))
|
||||||
|
return false;
|
||||||
|
port = ntohs(v6->sin6_port);
|
||||||
|
written = snprintf(buf, len, "[%s]:%u", ip, port);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (written < 0 || (size_t)written >= len) {
|
||||||
|
buf[0] = '\0';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* True when a client-supplied host string names a loopback destination:
|
/* True when a client-supplied host string names a loopback destination:
|
||||||
"localhost", any 127.0.0.0/8 literal, "::1", or "[::1]". */
|
"localhost", any 127.0.0.0/8 literal, "::1", or "[::1]". */
|
||||||
bool utils_host_is_loopback(const char* host) {
|
bool utils_host_is_loopback(const char* host) {
|
||||||
|
|||||||
@@ -77,5 +77,13 @@ bool append_tail_length(unsigned long long old_size, unsigned long long check_si
|
|||||||
bool utils_sockaddr_is_loopback(const struct sockaddr* addr);
|
bool utils_sockaddr_is_loopback(const struct sockaddr* addr);
|
||||||
bool utils_fd_peer_is_local(int fd);
|
bool utils_fd_peer_is_local(int fd);
|
||||||
bool utils_host_is_loopback(const char* host);
|
bool utils_host_is_loopback(const char* host);
|
||||||
|
/* Numeric peer address of a connected fd (INET6_ADDRSTRLEN is always enough).
|
||||||
|
* Returns false and leaves buf empty when the fd is not a connected INET socket
|
||||||
|
* or getpeername/inet_ntop fails. Used by the daemon host-access gate; a false
|
||||||
|
* return is "cannot tell" and must be treated as fail-closed when ACLs apply. */
|
||||||
|
bool utils_fd_peer_ip(int fd, char* buf, size_t len);
|
||||||
|
/* Format a sockaddr as "ip:port" (IPv4) or "[ip]:port" (IPv6) for logging.
|
||||||
|
* Returns false (buf emptied) for a non-INET family or a formatting failure. */
|
||||||
|
bool utils_sockaddr_to_string(const struct sockaddr* addr, char* buf, size_t len);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -51,6 +51,7 @@ READONLY_MODULE = os.path.join(MODULE_ROOT, "readonly")
|
|||||||
AUTH_MODULE = os.path.join(MODULE_ROOT, "auth")
|
AUTH_MODULE = os.path.join(MODULE_ROOT, "auth")
|
||||||
TEAM_MODULE = os.path.join(MODULE_ROOT, "team")
|
TEAM_MODULE = os.path.join(MODULE_ROOT, "team")
|
||||||
OWNER_MODULE = os.path.join(MODULE_ROOT, "owner")
|
OWNER_MODULE = os.path.join(MODULE_ROOT, "owner")
|
||||||
|
DENIED_MODULE = os.path.join(MODULE_ROOT, "denied")
|
||||||
CONF_FILE = os.path.join(TEST_DATA_DIR, "fastsyncd.conf")
|
CONF_FILE = os.path.join(TEST_DATA_DIR, "fastsyncd.conf")
|
||||||
CRED_FILE = os.path.join(TEST_DATA_DIR, "fastsyncd.passwd")
|
CRED_FILE = os.path.join(TEST_DATA_DIR, "fastsyncd.passwd")
|
||||||
STARTFAIL_CONF = os.path.join(TEST_DATA_DIR, "fastsyncd_startfail.conf")
|
STARTFAIL_CONF = os.path.join(TEST_DATA_DIR, "fastsyncd_startfail.conf")
|
||||||
@@ -191,7 +192,7 @@ def _config_port(config_path):
|
|||||||
@pytest.fixture(scope="module", autouse=True)
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
def daemon_env():
|
def daemon_env():
|
||||||
for d in (MODULE_ROOT, FILES_MODULE, READONLY_MODULE, AUTH_MODULE, TEAM_MODULE, OWNER_MODULE,
|
for d in (MODULE_ROOT, FILES_MODULE, READONLY_MODULE, AUTH_MODULE, TEAM_MODULE, OWNER_MODULE,
|
||||||
DETACH_MODULE):
|
DENIED_MODULE, DETACH_MODULE):
|
||||||
shutil.rmtree(d, ignore_errors=True)
|
shutil.rmtree(d, ignore_errors=True)
|
||||||
os.makedirs(d, exist_ok=True)
|
os.makedirs(d, exist_ok=True)
|
||||||
generate_test_files(SOURCE_DIR, full=False)
|
generate_test_files(SOURCE_DIR, full=False)
|
||||||
@@ -231,7 +232,12 @@ def daemon_env():
|
|||||||
"[owner]\n"
|
"[owner]\n"
|
||||||
"path = %s\n"
|
"path = %s\n"
|
||||||
"client owner = yes\n"
|
"client owner = yes\n"
|
||||||
% (config_port, FILES_MODULE, READONLY_MODULE, AUTH_MODULE, TEAM_MODULE, OWNER_MODULE))
|
"\n"
|
||||||
|
"[denied]\n"
|
||||||
|
"path = %s\n"
|
||||||
|
"hosts deny = 127.0.0.1\n"
|
||||||
|
% (config_port, FILES_MODULE, READONLY_MODULE, AUTH_MODULE, TEAM_MODULE, OWNER_MODULE,
|
||||||
|
DENIED_MODULE))
|
||||||
|
|
||||||
# A dedicated config for the fail-closed startup check: an auth-required
|
# A dedicated config for the fail-closed startup check: an auth-required
|
||||||
# module with no credential store must refuse to start. Its own free port
|
# module with no credential store must refuse to start. Its own free port
|
||||||
@@ -368,6 +374,15 @@ class TestDaemonRejection:
|
|||||||
result = _push("127.0.0.1::/sub", daemon.port)
|
result = _push("127.0.0.1::/sub", daemon.port)
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
@pytest.mark.ci
|
||||||
|
def test_hosts_deny_rejects_loopback(self, daemon):
|
||||||
|
"""Host access control: a module with `hosts deny = 127.0.0.1` refuses a
|
||||||
|
loopback client at the config gate, before any data is exchanged."""
|
||||||
|
before = self._tree_files()
|
||||||
|
result = _push("127.0.0.1::denied", daemon.port)
|
||||||
|
assert result.returncode != 0
|
||||||
|
assert self._tree_files() == before, "host-denied connection wrote under the module root"
|
||||||
|
|
||||||
def test_dotdot_destination_rejected(self, daemon):
|
def test_dotdot_destination_rejected(self, daemon):
|
||||||
"""A '..' path expansion in the module-relative path is refused at parse
|
"""A '..' path expansion in the module-relative path is refused at parse
|
||||||
time so a client cannot escape the module root while it is still on the
|
time so a client cannot escape the module root while it is still on the
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "test_property.h"
|
#include "test_property.h"
|
||||||
#include "test_protocol.h"
|
#include "test_protocol.h"
|
||||||
#include "test_queue.h"
|
#include "test_queue.h"
|
||||||
|
#include "test_receiver_timeout.h"
|
||||||
#include "test_robustness.h"
|
#include "test_robustness.h"
|
||||||
#include "test_scanner.h"
|
#include "test_scanner.h"
|
||||||
#include "test_server.h"
|
#include "test_server.h"
|
||||||
@@ -61,6 +62,7 @@ int main() {
|
|||||||
RUN_TEST(test_delta);
|
RUN_TEST(test_delta);
|
||||||
RUN_TEST(test_data);
|
RUN_TEST(test_data);
|
||||||
RUN_TEST(test_protocol);
|
RUN_TEST(test_protocol);
|
||||||
|
RUN_TEST(test_receiver_timeout);
|
||||||
RUN_TEST(test_metadata);
|
RUN_TEST(test_metadata);
|
||||||
RUN_TEST(test_glob);
|
RUN_TEST(test_glob);
|
||||||
RUN_TEST(test_iconv);
|
RUN_TEST(test_iconv);
|
||||||
|
|||||||
+19
-1
@@ -1,6 +1,7 @@
|
|||||||
#include "test_array_list.h"
|
#include "test_array_list.h"
|
||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
|
#include <limits.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
static int destroyer_calls = 0;
|
static int destroyer_calls = 0;
|
||||||
@@ -9,7 +10,7 @@ static void test_destroyer(void* item) {
|
|||||||
free(item);
|
free(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_array_list() {
|
static void test_array_list_basic() {
|
||||||
ArrayList* list = array_list_create(free);
|
ArrayList* list = array_list_create(free);
|
||||||
EXPECT_NOT_NULL(list);
|
EXPECT_NOT_NULL(list);
|
||||||
EXPECT_EQ_INT(list->size, 0);
|
EXPECT_EQ_INT(list->size, 0);
|
||||||
@@ -54,3 +55,20 @@ void test_array_list() {
|
|||||||
array_list_delete(list);
|
array_list_delete(list);
|
||||||
EXPECT_EQ_INT(destroyer_calls, 106);
|
EXPECT_EQ_INT(destroyer_calls, 106);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A capacity that would overflow `capacity * 2` must be refused instead of
|
||||||
|
* wrapping into signed-overflow UB; array_list_add surfaces the failure. */
|
||||||
|
static void test_array_list_extend_overflow_guard() {
|
||||||
|
ArrayList* list = array_list_create(NULL);
|
||||||
|
EXPECT_NOT_NULL(list);
|
||||||
|
list->capacity = INT_MAX / 2 + 1;
|
||||||
|
list->size = list->capacity;
|
||||||
|
EXPECT_FALSE(array_list_add(list, NULL));
|
||||||
|
list->size = 0;
|
||||||
|
array_list_delete(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_array_list() {
|
||||||
|
test_array_list_basic();
|
||||||
|
test_array_list_extend_overflow_guard();
|
||||||
|
}
|
||||||
+86
-1
@@ -6,6 +6,7 @@
|
|||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
#include <signal.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -1672,7 +1673,7 @@ static void test_config_receive_rejects_invalid_iconv_spec() {
|
|||||||
static void test_config_super_mode_wire_roundtrip() {
|
static void test_config_super_mode_wire_roundtrip() {
|
||||||
if (is_running_under_valgrind())
|
if (is_running_under_valgrind())
|
||||||
return;
|
return;
|
||||||
int modes[] = {SUPER_MODE_AUTO, SUPER_MODE_ON, SUPER_MODE_OFF};
|
SuperMode modes[] = {SUPER_MODE_AUTO, SUPER_MODE_ON, SUPER_MODE_OFF};
|
||||||
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
|
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
|
||||||
int p[2];
|
int p[2];
|
||||||
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||||
@@ -1846,6 +1847,89 @@ static void test_config_receive_rejects_copy_as_without_metadata() {
|
|||||||
config_delete(c);
|
config_delete(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Like roundtrip_config_ok, but the parent is the RECEIVER so the frame can be
|
||||||
|
rejected MID-way, before the sender finishes writing it. The sender child
|
||||||
|
ignores SIGPIPE so the receiver closing early cannot kill it; the parent
|
||||||
|
waits for the child to exit after observing the rejection. */
|
||||||
|
static bool roundtrip_config_rejected(const Config* send_cfg) {
|
||||||
|
int p[2];
|
||||||
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, p) != 0)
|
||||||
|
return false;
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
(void)signal(SIGPIPE, SIG_IGN);
|
||||||
|
close(p[0]);
|
||||||
|
io_set_fds(p[1], p[1]);
|
||||||
|
config_send(p[1], send_cfg);
|
||||||
|
close(p[1]);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
close(p[1]);
|
||||||
|
io_set_fds(p[0], p[0]);
|
||||||
|
Config* recv = config_receive(p[0]);
|
||||||
|
bool rejected = recv == NULL;
|
||||||
|
config_delete(recv);
|
||||||
|
close(p[0]);
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build a Config with `count` --skip-compress suffixes, each `suffix_len` bytes
|
||||||
|
long, for the pre-auth config-string budget tests. */
|
||||||
|
static Config* make_skip_compress_config(int count, size_t suffix_len) {
|
||||||
|
Config* c = config_create();
|
||||||
|
if (!c)
|
||||||
|
return NULL;
|
||||||
|
c->send_directory = str_dup("/src");
|
||||||
|
c->receive_root_directory = str_dup("/dst");
|
||||||
|
c->skip_compress_set = true;
|
||||||
|
c->skip_compress_count = count;
|
||||||
|
c->skip_compress_suffixes = calloc((size_t)count, sizeof(char*));
|
||||||
|
if (!c->skip_compress_suffixes) {
|
||||||
|
config_delete(c);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
char* suffix = malloc(suffix_len + 1);
|
||||||
|
if (!suffix) {
|
||||||
|
config_delete(c);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
memset(suffix, 'x', suffix_len);
|
||||||
|
suffix[suffix_len] = '\0';
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
c->skip_compress_suffixes[i] = str_dup(suffix);
|
||||||
|
free(suffix);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pre-auth memory bound: one connection must not retain unbounded config
|
||||||
|
strings. An over-limit --skip-compress count is refused, and even an
|
||||||
|
in-range count cannot exceed the aggregate per-connection string budget. */
|
||||||
|
static void test_config_receive_rejects_oversized_string_budget() {
|
||||||
|
if (is_running_under_valgrind())
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Exactly MAX_SKIP_COMPRESS_SUFFIXES tiny suffixes are accepted. */
|
||||||
|
Config* ok = make_skip_compress_config(MAX_SKIP_COMPRESS_SUFFIXES, 1);
|
||||||
|
EXPECT_NOT_NULL(ok);
|
||||||
|
EXPECT_TRUE(roundtrip_config_ok(ok));
|
||||||
|
config_delete(ok);
|
||||||
|
|
||||||
|
/* One suffix over the count cap is rejected before any suffix is read. */
|
||||||
|
Config* over_count = make_skip_compress_config(MAX_SKIP_COMPRESS_SUFFIXES + 1, 1);
|
||||||
|
EXPECT_NOT_NULL(over_count);
|
||||||
|
EXPECT_TRUE(roundtrip_config_rejected(over_count));
|
||||||
|
config_delete(over_count);
|
||||||
|
|
||||||
|
/* In-range count, but the strings together exceed MAX_CONFIG_STRING_BYTES
|
||||||
|
(64 suffixes * ~64 KiB > 1 MiB), so the aggregate budget rejects it. */
|
||||||
|
Config* over_bytes = make_skip_compress_config(64, MAX_STRING_SIZE - 1);
|
||||||
|
EXPECT_NOT_NULL(over_bytes);
|
||||||
|
EXPECT_TRUE(roundtrip_config_rejected(over_bytes));
|
||||||
|
config_delete(over_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
/* identity_copy_as_refused() is the pure, pre-snapshot refusal predicate: a
|
/* identity_copy_as_refused() is the pure, pre-snapshot refusal predicate: a
|
||||||
--copy-as is refused when the receiver is not root OR the effective super
|
--copy-as is refused when the receiver is not root OR the effective super
|
||||||
mode is OFF (an operator veto), and never when --copy-as is unset. */
|
mode is OFF (an operator veto), and never when --copy-as is unset. */
|
||||||
@@ -2009,6 +2093,7 @@ void test_config() {
|
|||||||
test_config_copy_as_wire_roundtrip();
|
test_config_copy_as_wire_roundtrip();
|
||||||
test_config_receive_rejects_negative_copy_as();
|
test_config_receive_rejects_negative_copy_as();
|
||||||
test_config_receive_rejects_copy_as_without_metadata();
|
test_config_receive_rejects_copy_as_without_metadata();
|
||||||
|
test_config_receive_rejects_oversized_string_budget();
|
||||||
test_config_receive_with_validate_rejects();
|
test_config_receive_with_validate_rejects();
|
||||||
}
|
}
|
||||||
test_identity_copy_as_refused();
|
test_identity_copy_as_refused();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "test_daemon_conf.h"
|
#include "test_daemon_conf.h"
|
||||||
|
#include "credentials.h"
|
||||||
#include "daemon_conf.h"
|
#include "daemon_conf.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -30,6 +31,10 @@ static void test_daemon_conf_create_defaults() {
|
|||||||
EXPECT_EQ_INT(conf->global.port, DAEMON_CONF_DEFAULT_PORT);
|
EXPECT_EQ_INT(conf->global.port, DAEMON_CONF_DEFAULT_PORT);
|
||||||
EXPECT_NULL(conf->global.motd_file);
|
EXPECT_NULL(conf->global.motd_file);
|
||||||
EXPECT_NULL(conf->global.address);
|
EXPECT_NULL(conf->global.address);
|
||||||
|
EXPECT_EQ_INT(conf->global.max_connections, DAEMON_CONF_DEFAULT_MAX_CONNECTIONS);
|
||||||
|
EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, DAEMON_CONF_DEFAULT_AUTH_FAILURE_DELAY_MS);
|
||||||
|
EXPECT_EQ_INT(conf->global.hosts_allow_count, 0);
|
||||||
|
EXPECT_EQ_INT(conf->global.hosts_deny_count, 0);
|
||||||
EXPECT_EQ_INT(conf->module_count, 0);
|
EXPECT_EQ_INT(conf->module_count, 0);
|
||||||
daemon_conf_free(conf);
|
daemon_conf_free(conf);
|
||||||
}
|
}
|
||||||
@@ -309,6 +314,18 @@ static void test_daemon_conf_dparam_override() {
|
|||||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port = 9000", err, sizeof(err)), 0);
|
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port = 9000", err, sizeof(err)), 0);
|
||||||
EXPECT_EQ_INT(conf->global.port, 9000);
|
EXPECT_EQ_INT(conf->global.port, 9000);
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "max connections=7", err, sizeof(err)), 0);
|
||||||
|
EXPECT_EQ_INT(conf->global.max_connections, 7);
|
||||||
|
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "AUTH FAILURE DELAY=1500", err, sizeof(err)), 0);
|
||||||
|
EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, 1500);
|
||||||
|
EXPECT_EQ_INT(
|
||||||
|
daemon_conf_apply_dparam(conf, "hosts allow=127.0.0.1,10.0.0.0/8", err, sizeof(err)), 0);
|
||||||
|
EXPECT_EQ_INT(conf->global.hosts_allow_count, 2);
|
||||||
|
/* A later --dparam replaces the list (an override must be able to narrow). */
|
||||||
|
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "hosts allow=127.0.0.1", err, sizeof(err)), 0);
|
||||||
|
EXPECT_EQ_INT(conf->global.hosts_allow_count, 1);
|
||||||
|
EXPECT_EQ_STR(conf->global.hosts_allow[0], "127.0.0.1");
|
||||||
|
|
||||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port=notaport", err, sizeof(err)), -1);
|
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "port=notaport", err, sizeof(err)), -1);
|
||||||
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "bogus=1", err, sizeof(err)), -1);
|
EXPECT_EQ_INT(daemon_conf_apply_dparam(conf, "bogus=1", err, sizeof(err)), -1);
|
||||||
EXPECT_TRUE(strstr(err, "unknown global key") != NULL);
|
EXPECT_TRUE(strstr(err, "unknown global key") != NULL);
|
||||||
@@ -320,6 +337,161 @@ static void test_daemon_conf_dparam_override() {
|
|||||||
daemon_conf_free(conf);
|
daemon_conf_free(conf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Each `auth users` entry is validated with the same username rule as the
|
||||||
|
* credential store, so invisible whitespace/control characters can never make
|
||||||
|
* an exact strcmp match ambiguous. */
|
||||||
|
static void test_daemon_conf_auth_users_validated() {
|
||||||
|
char* path;
|
||||||
|
char err[256];
|
||||||
|
const DaemonConf* conf;
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nauth users = alice, bad user\n", &path), 0);
|
||||||
|
conf = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NULL(conf);
|
||||||
|
EXPECT_TRUE(strstr(err, "invalid 'auth users' entry") != NULL);
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nauth users = good\tbad\n", &path), 0);
|
||||||
|
conf = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NULL(conf);
|
||||||
|
EXPECT_TRUE(strstr(err, "invalid 'auth users' entry") != NULL);
|
||||||
|
|
||||||
|
/* An over-long name exceeds CREDENTIAL_MAX_USER_LEN and is rejected. */
|
||||||
|
{
|
||||||
|
char body[CREDENTIAL_MAX_USER_LEN + 128];
|
||||||
|
int n = snprintf(body, sizeof(body), "[m]\npath = /x\nauth users = ");
|
||||||
|
memset(body + n, 'a', CREDENTIAL_MAX_USER_LEN + 1);
|
||||||
|
body[n + CREDENTIAL_MAX_USER_LEN + 1] = '\n';
|
||||||
|
body[n + CREDENTIAL_MAX_USER_LEN + 2] = '\0';
|
||||||
|
EXPECT_EQ_INT(write_conf(body, &path), 0);
|
||||||
|
conf = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NULL(conf);
|
||||||
|
EXPECT_TRUE(strstr(err, "invalid 'auth users' entry") != NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty entries between commas are skipped, not treated as invalid. */
|
||||||
|
EXPECT_EQ_INT(write_conf("[m]\npath = /x\nauth users = alice,, bob\n", &path), 0);
|
||||||
|
DaemonConf* ok_conf = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NOT_NULL(ok_conf);
|
||||||
|
EXPECT_EQ_INT(ok_conf->modules[0].auth_user_count, 2);
|
||||||
|
EXPECT_EQ_STR(ok_conf->modules[0].auth_users[0], "alice");
|
||||||
|
EXPECT_EQ_STR(ok_conf->modules[0].auth_users[1], "bob");
|
||||||
|
daemon_conf_free(ok_conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wave 3 daemon hardening: configurable global/per-module connection caps,
|
||||||
|
* auth-failure throttle and host access lists parse strictly (valid values are
|
||||||
|
* stored, malformed values fail the whole load). */
|
||||||
|
static void test_daemon_conf_limits_and_hosts_parse() {
|
||||||
|
char* path;
|
||||||
|
char err[256];
|
||||||
|
EXPECT_EQ_INT(write_conf("max connections = 25\n"
|
||||||
|
"auth failure delay = 0\n"
|
||||||
|
"hosts allow = 10.0.0.0/8, 192.168.1.0/24\n"
|
||||||
|
"hosts deny = 192.168.0.1 2001:db8::/32\n"
|
||||||
|
"\n"
|
||||||
|
"[m]\n"
|
||||||
|
"path = /x\n"
|
||||||
|
"max connections = 3\n"
|
||||||
|
"hosts allow = 127.0.0.1\n"
|
||||||
|
"hosts deny = *\n",
|
||||||
|
&path),
|
||||||
|
0);
|
||||||
|
DaemonConf* conf = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NOT_NULL(conf);
|
||||||
|
EXPECT_EQ_INT(conf->global.max_connections, 25);
|
||||||
|
EXPECT_EQ_INT(conf->global.auth_failure_delay_ms, 0);
|
||||||
|
EXPECT_EQ_INT(conf->global.hosts_allow_count, 2);
|
||||||
|
EXPECT_EQ_STR(conf->global.hosts_allow[0], "10.0.0.0/8");
|
||||||
|
EXPECT_EQ_STR(conf->global.hosts_allow[1], "192.168.1.0/24");
|
||||||
|
EXPECT_EQ_INT(conf->global.hosts_deny_count, 2);
|
||||||
|
EXPECT_EQ_STR(conf->global.hosts_deny[0], "192.168.0.1");
|
||||||
|
EXPECT_EQ_STR(conf->global.hosts_deny[1], "2001:db8::/32");
|
||||||
|
EXPECT_EQ_INT(conf->modules[0].max_connections, 3);
|
||||||
|
EXPECT_EQ_INT(conf->modules[0].hosts_allow_count, 1);
|
||||||
|
EXPECT_EQ_STR(conf->modules[0].hosts_allow[0], "127.0.0.1");
|
||||||
|
EXPECT_EQ_INT(conf->modules[0].hosts_deny_count, 1);
|
||||||
|
EXPECT_EQ_STR(conf->modules[0].hosts_deny[0], "*");
|
||||||
|
daemon_conf_free(conf);
|
||||||
|
|
||||||
|
const char* bad_values[] = {
|
||||||
|
"max connections = 0\n", "max connections = -1\n",
|
||||||
|
"max connections = abc\n", "auth failure delay = -1\n",
|
||||||
|
"auth failure delay = 70000\n", "auth failure delay = soon\n",
|
||||||
|
"hosts allow = 10.0.0.0/99\n", "hosts deny = 2001:db8::/129\n",
|
||||||
|
"hosts allow = *.example.com\n", "hosts deny = not-an-ip\n",
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(bad_values) / sizeof(bad_values[0]); i++) {
|
||||||
|
EXPECT_EQ_INT(write_conf(bad_values[i], &path), 0);
|
||||||
|
const DaemonConf* rejected = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NULL(rejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The same strictness applies inside a module section. */
|
||||||
|
const char* bad_module[] = {
|
||||||
|
"[m]\npath = /x\nmax connections = 0\n",
|
||||||
|
"[m]\npath = /x\nhosts allow = 10.0.0.0/40\n",
|
||||||
|
"[m]\npath = /x\nhosts deny = 999.1.1.1/8\n",
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(bad_module) / sizeof(bad_module[0]); i++) {
|
||||||
|
EXPECT_EQ_INT(write_conf(bad_module[i], &path), 0);
|
||||||
|
const DaemonConf* rejected = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NULL(rejected);
|
||||||
|
EXPECT_TRUE(strstr(err, "invalid") != NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An empty hosts list is not an error (no patterns are added). */
|
||||||
|
EXPECT_EQ_INT(write_conf("hosts allow = \n[m]\npath = /x\n", &path), 0);
|
||||||
|
conf = daemon_conf_load(path, err, sizeof(err));
|
||||||
|
free(path);
|
||||||
|
EXPECT_NOT_NULL(conf);
|
||||||
|
EXPECT_EQ_INT(conf->global.hosts_allow_count, 0);
|
||||||
|
daemon_conf_free(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_daemon_hosts_allowed() {
|
||||||
|
/* Pattern forms. */
|
||||||
|
EXPECT_TRUE(daemon_host_pattern_match("*", "203.0.113.9"));
|
||||||
|
EXPECT_TRUE(daemon_host_pattern_match("10.0.0.1", "10.0.0.1"));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match("10.0.0.1", "10.0.0.2"));
|
||||||
|
EXPECT_TRUE(daemon_host_pattern_match("10.0.0.0/8", "10.255.1.2"));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match("10.0.0.0/8", "11.0.0.1"));
|
||||||
|
EXPECT_TRUE(daemon_host_pattern_match("2001:db8::/32", "2001:db8:1234::5"));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match("2001:db8::/32", "2001:db9::1"));
|
||||||
|
EXPECT_TRUE(daemon_host_pattern_match("::1", "::1"));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match("::1", "::2"));
|
||||||
|
EXPECT_TRUE(daemon_host_pattern_match("*.example.com", "host.example.com"));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match("*.example.com", "example.org"));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match(NULL, "10.0.0.1"));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match("10.0.0.1", NULL));
|
||||||
|
EXPECT_FALSE(daemon_host_pattern_match("", "10.0.0.1"));
|
||||||
|
|
||||||
|
char* allow[] = {"10.0.0.0/8"};
|
||||||
|
char* deny[] = {"10.0.0.1"};
|
||||||
|
/* Deny takes precedence over a matching allow. */
|
||||||
|
EXPECT_FALSE(daemon_hosts_allowed("10.0.0.1", allow, 1, deny, 1));
|
||||||
|
EXPECT_TRUE(daemon_hosts_allowed("10.0.0.2", allow, 1, deny, 1));
|
||||||
|
/* A non-empty allow list rejects a peer that matches none of its entries. */
|
||||||
|
EXPECT_FALSE(daemon_hosts_allowed("192.168.1.1", allow, 1, NULL, 0));
|
||||||
|
/* With only a deny list, everything not denied is accepted. */
|
||||||
|
EXPECT_TRUE(daemon_hosts_allowed("192.168.1.1", NULL, 0, deny, 1));
|
||||||
|
EXPECT_FALSE(daemon_hosts_allowed("10.0.0.1", NULL, 0, deny, 1));
|
||||||
|
/* No lists at all accepts everyone. */
|
||||||
|
EXPECT_TRUE(daemon_hosts_allowed("192.168.1.1", NULL, 0, NULL, 0));
|
||||||
|
/* An unprovable peer (NULL) never matches an allow list. */
|
||||||
|
EXPECT_FALSE(daemon_hosts_allowed(NULL, allow, 1, NULL, 0));
|
||||||
|
|
||||||
|
EXPECT_FALSE(daemon_hosts_restricted(NULL, 0, NULL, 0));
|
||||||
|
EXPECT_TRUE(daemon_hosts_restricted(allow, 1, NULL, 0));
|
||||||
|
EXPECT_TRUE(daemon_hosts_restricted(NULL, 0, deny, 1));
|
||||||
|
}
|
||||||
|
|
||||||
static void test_daemon_module_name_valid() {
|
static void test_daemon_module_name_valid() {
|
||||||
EXPECT_TRUE(daemon_module_name_valid("backup"));
|
EXPECT_TRUE(daemon_module_name_valid("backup"));
|
||||||
EXPECT_TRUE(daemon_module_name_valid("Backup_2"));
|
EXPECT_TRUE(daemon_module_name_valid("Backup_2"));
|
||||||
@@ -351,5 +523,8 @@ void test_daemon_conf() {
|
|||||||
test_daemon_conf_missing_file_rejected();
|
test_daemon_conf_missing_file_rejected();
|
||||||
test_daemon_conf_find_module();
|
test_daemon_conf_find_module();
|
||||||
test_daemon_conf_dparam_override();
|
test_daemon_conf_dparam_override();
|
||||||
|
test_daemon_conf_auth_users_validated();
|
||||||
|
test_daemon_conf_limits_and_hosts_parse();
|
||||||
|
test_daemon_hosts_allowed();
|
||||||
test_daemon_module_name_valid();
|
test_daemon_module_name_valid();
|
||||||
}
|
}
|
||||||
@@ -1370,6 +1370,76 @@ static void test_dir_time_list() {
|
|||||||
rmdir(root);
|
rmdir(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A hostile sender can stream unbounded STATUS_DIR_TIMES frames; the
|
||||||
|
* accumulator must bound the CUMULATIVE path bytes (not just one frame) and
|
||||||
|
* reject the add that would cross the cap, leaving the list untouched. */
|
||||||
|
static void test_dir_time_list_cap() {
|
||||||
|
DirTimeList list;
|
||||||
|
dir_time_list_init(&list);
|
||||||
|
EXPECT_EQ_INT((int)list.bytes, 0);
|
||||||
|
FileMetadata metadata = {.mtime_sec = 1, .mtime_nsec = 0};
|
||||||
|
|
||||||
|
size_t path_len = MAX_STRING_SIZE - 1;
|
||||||
|
char* path = malloc(path_len + 1);
|
||||||
|
EXPECT_NOT_NULL(path);
|
||||||
|
memset(path, 'a', path_len);
|
||||||
|
path[path_len] = '\0';
|
||||||
|
|
||||||
|
bool rejected = false;
|
||||||
|
for (size_t i = 0; i < MAX_DIR_TIME_ENTRIES + 1 && !rejected; i++) {
|
||||||
|
size_t before_count = list.count;
|
||||||
|
size_t before_bytes = list.bytes;
|
||||||
|
if (!dir_time_list_add(&list, path, &metadata)) {
|
||||||
|
rejected = true;
|
||||||
|
/* The rejected add must not have partially mutated the list. */
|
||||||
|
EXPECT_TRUE(list.count == before_count);
|
||||||
|
EXPECT_TRUE(list.bytes == before_bytes);
|
||||||
|
} else {
|
||||||
|
EXPECT_TRUE(list.count == before_count + 1);
|
||||||
|
EXPECT_TRUE(list.bytes == before_bytes + path_len + sizeof(FileMetadata) + sizeof(char*));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(rejected);
|
||||||
|
EXPECT_TRUE(list.count <= MAX_DIR_TIME_ENTRIES);
|
||||||
|
EXPECT_TRUE(list.bytes <= MAX_DIR_TIME_BYTES);
|
||||||
|
|
||||||
|
/* The retained entries are still intact and freeable after the rejection. */
|
||||||
|
EXPECT_TRUE(list.count > 0);
|
||||||
|
EXPECT_TRUE(strcmp(list.paths[0], path) == 0);
|
||||||
|
dir_time_list_free(&list);
|
||||||
|
EXPECT_EQ_INT((int)list.bytes, 0);
|
||||||
|
free(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* receive_incremental_check must reject an empty check_path; every other
|
||||||
|
* receive path rejects path[0]=='\0'. Feed the check header (empty wire path
|
||||||
|
* + size/mtime/nsec) and assert the check is refused without being skipped. */
|
||||||
|
static void test_receive_incremental_check_empty_path() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
EXPECT_NOT_NULL(cfg);
|
||||||
|
cfg->checksum = false;
|
||||||
|
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(pipe(p), 0);
|
||||||
|
size_t wire_len = 0;
|
||||||
|
unsigned long long check_size = 0;
|
||||||
|
long long check_mtime = 0;
|
||||||
|
long long check_mtime_nsec = 0;
|
||||||
|
EXPECT_TRUE(send_n_data(p[1], &wire_len, sizeof(wire_len)));
|
||||||
|
EXPECT_TRUE(send_n_data(p[1], &check_size, sizeof(check_size)));
|
||||||
|
EXPECT_TRUE(send_n_data(p[1], &check_mtime, sizeof(check_mtime)));
|
||||||
|
EXPECT_TRUE(send_n_data(p[1], &check_mtime_nsec, sizeof(check_mtime_nsec)));
|
||||||
|
|
||||||
|
bool skipped = true;
|
||||||
|
const File* file = receive_incremental_check(p[0], cfg, &skipped);
|
||||||
|
EXPECT_NULL(file);
|
||||||
|
EXPECT_FALSE(skipped);
|
||||||
|
|
||||||
|
close(p[0]);
|
||||||
|
close(p[1]);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
/* -K/--keep-dirlinks secure open: with an authorized root, a destination path
|
/* -K/--keep-dirlinks secure open: with an authorized root, a destination path
|
||||||
* component that is a symlink to an IN-ROOT directory is used as that directory
|
* component that is a symlink to an IN-ROOT directory is used as that directory
|
||||||
* (its referent is opened through a relative O_NOFOLLOW walk from the root fd,
|
* (its referent is opened through a relative O_NOFOLLOW walk from the root fd,
|
||||||
@@ -1531,6 +1601,8 @@ void test_file() {
|
|||||||
}
|
}
|
||||||
test_file_metadata_create();
|
test_file_metadata_create();
|
||||||
test_dir_time_list();
|
test_dir_time_list();
|
||||||
|
test_dir_time_list_cap();
|
||||||
|
test_receive_incremental_check_empty_path();
|
||||||
test_keep_dirlinks_secure_open();
|
test_keep_dirlinks_secure_open();
|
||||||
test_inplace_overwrite_clears_special_mode_bits();
|
test_inplace_overwrite_clears_special_mode_bits();
|
||||||
test_inplace_overwrite_metadata_strips_special_bits();
|
test_inplace_overwrite_metadata_strips_special_bits();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "test_log.h"
|
#include "test_log.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
|
#include <fcntl.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <threads.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
/* Test default log level: WARNING and ERROR should print, DEBUG and INFO should not.
|
/* Test default log level: WARNING and ERROR should print, DEBUG and INFO should not.
|
||||||
@@ -147,6 +149,118 @@ static void test_log_debug_enabled_matches_gate() {
|
|||||||
set_log_debug_flags(LOG_DEBUG_ALL);
|
set_log_debug_flags(LOG_DEBUG_ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define LOG_CONCURRENCY_THREADS 8
|
||||||
|
#define LOG_CONCURRENCY_LINES 250
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int id;
|
||||||
|
} LogConcurrencyArg;
|
||||||
|
|
||||||
|
static int log_concurrency_worker(void* context) {
|
||||||
|
LogConcurrencyArg* arg = context;
|
||||||
|
for (int i = 0; i < LOG_CONCURRENCY_LINES; i++) {
|
||||||
|
log_message(LOG_LEVEL_WARNING, "worker %d line %d", arg->id, i);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int count_substring(const char* haystack, const char* needle) {
|
||||||
|
int count = 0;
|
||||||
|
size_t needle_length = strlen(needle);
|
||||||
|
const char* cursor = haystack;
|
||||||
|
while ((cursor = strstr(cursor, needle)) != NULL) {
|
||||||
|
count++;
|
||||||
|
cursor += needle_length;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Concurrent log_message() calls from many threads must never interleave a
|
||||||
|
* single line: every emitted line has exactly one timestamp prefix and one
|
||||||
|
* body. Before write_message() was serialized, the three separate fprintf
|
||||||
|
* calls (prefix, body, newline) let lines tear. */
|
||||||
|
static void test_log_concurrent_no_torn_lines(void) {
|
||||||
|
FILE* fp = tmpfile();
|
||||||
|
EXPECT_NOT_NULL(fp);
|
||||||
|
|
||||||
|
/* Mute the console mirror so the workers don't flood the test output. */
|
||||||
|
fflush(stdout);
|
||||||
|
fflush(stderr);
|
||||||
|
int saved_stdout = dup(STDOUT_FILENO);
|
||||||
|
int saved_stderr = dup(STDERR_FILENO);
|
||||||
|
int null_fd = open("/dev/null", O_WRONLY);
|
||||||
|
EXPECT_TRUE(saved_stdout >= 0);
|
||||||
|
EXPECT_TRUE(saved_stderr >= 0);
|
||||||
|
EXPECT_TRUE(null_fd >= 0);
|
||||||
|
EXPECT_TRUE(dup2(null_fd, STDOUT_FILENO) >= 0);
|
||||||
|
EXPECT_TRUE(dup2(null_fd, STDERR_FILENO) >= 0);
|
||||||
|
close(null_fd);
|
||||||
|
|
||||||
|
set_log_level(LOG_LEVEL_WARNING);
|
||||||
|
log_set_stderr_mode(LOG_STDERR_ERRORS);
|
||||||
|
log_set_file(fp);
|
||||||
|
|
||||||
|
thrd_t threads[LOG_CONCURRENCY_THREADS];
|
||||||
|
LogConcurrencyArg args[LOG_CONCURRENCY_THREADS];
|
||||||
|
int created = 0;
|
||||||
|
for (int i = 0; i < LOG_CONCURRENCY_THREADS; i++) {
|
||||||
|
args[i].id = i;
|
||||||
|
if (thrd_create(&threads[i], log_concurrency_worker, &args[i]) != thrd_success)
|
||||||
|
break;
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < created; i++) {
|
||||||
|
thrd_join(threads[i], NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_set_file(NULL);
|
||||||
|
fflush(fp);
|
||||||
|
|
||||||
|
fflush(stdout);
|
||||||
|
fflush(stderr);
|
||||||
|
dup2(saved_stdout, STDOUT_FILENO);
|
||||||
|
dup2(saved_stderr, STDERR_FILENO);
|
||||||
|
close(saved_stdout);
|
||||||
|
close(saved_stderr);
|
||||||
|
|
||||||
|
rewind(fp);
|
||||||
|
char line[512];
|
||||||
|
int total_lines = 0;
|
||||||
|
int malformed_lines = 0;
|
||||||
|
bool saw_missing_newline = false;
|
||||||
|
while (fgets(line, sizeof(line), fp) != NULL) {
|
||||||
|
size_t length = strlen(line);
|
||||||
|
if (length == 0 || line[length - 1] != '\n')
|
||||||
|
saw_missing_newline = true;
|
||||||
|
if (strncmp(line, "20", 2) != 0 || count_substring(line, "[WARN]: worker ") != 1)
|
||||||
|
malformed_lines++;
|
||||||
|
total_lines++;
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(created, LOG_CONCURRENCY_THREADS);
|
||||||
|
EXPECT_FALSE(saw_missing_newline);
|
||||||
|
EXPECT_EQ_INT(malformed_lines, 0);
|
||||||
|
EXPECT_EQ_INT(total_lines, LOG_CONCURRENCY_THREADS * LOG_CONCURRENCY_LINES);
|
||||||
|
log_set_stderr_mode(LOG_STDERR_ERRORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detaching the logger from a FILE* before it is closed must leave the logging
|
||||||
|
* subsystem safe: later calls must not touch the freed handle. */
|
||||||
|
static void test_log_set_file_null_before_fclose(void) {
|
||||||
|
FILE* fp = tmpfile();
|
||||||
|
EXPECT_NOT_NULL(fp);
|
||||||
|
|
||||||
|
set_log_level(LOG_LEVEL_ERROR);
|
||||||
|
log_set_file(fp);
|
||||||
|
log_message(LOG_LEVEL_ERROR, "line before detach");
|
||||||
|
log_set_file(NULL);
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
log_message(LOG_LEVEL_ERROR, "line after close");
|
||||||
|
EXPECT_TRUE(true);
|
||||||
|
}
|
||||||
|
|
||||||
void test_log() {
|
void test_log() {
|
||||||
test_log_message_debug();
|
test_log_message_debug();
|
||||||
test_log_message_info();
|
test_log_message_info();
|
||||||
@@ -159,4 +273,6 @@ void test_log() {
|
|||||||
test_log_stderr_mode_all();
|
test_log_stderr_mode_all();
|
||||||
test_log_message_formats();
|
test_log_message_formats();
|
||||||
test_log_debug_enabled_matches_gate();
|
test_log_debug_enabled_matches_gate();
|
||||||
|
test_log_concurrent_no_torn_lines();
|
||||||
|
test_log_set_file_null_before_fclose();
|
||||||
}
|
}
|
||||||
@@ -412,6 +412,35 @@ static void test_protocol_accounting_release_does_not_underflow() {
|
|||||||
protocol_session_unbind();
|
protocol_session_unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_protocol_session_io_timeout() {
|
||||||
|
/* Default is the built-in 60 s window; the setter stores exactly what it is
|
||||||
|
* given (<= 0 means "fall back to the default") so callers can propagate
|
||||||
|
* --timeout without special-casing 0. */
|
||||||
|
ProtocolSession session;
|
||||||
|
protocol_session_init(&session, -1, -1);
|
||||||
|
EXPECT_EQ_INT(session.io_timeout_sec, 60);
|
||||||
|
|
||||||
|
protocol_session_set_io_timeout(&session, 120);
|
||||||
|
EXPECT_EQ_INT(session.io_timeout_sec, 120);
|
||||||
|
protocol_session_set_io_timeout(&session, 0);
|
||||||
|
EXPECT_EQ_INT(session.io_timeout_sec, 0);
|
||||||
|
/* A NULL session is a no-op, not a crash. */
|
||||||
|
protocol_session_set_io_timeout(NULL, 5);
|
||||||
|
|
||||||
|
/* A short per-session deadline must actually bound a non-responsive read:
|
||||||
|
* with no writer the poll waits for the configured 1 s and then fails,
|
||||||
|
* rather than the built-in 60 s. */
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(pipe(p), 0);
|
||||||
|
ProtocolSession timed;
|
||||||
|
protocol_session_init(&timed, p[0], p[1]);
|
||||||
|
protocol_session_set_io_timeout(&timed, 1);
|
||||||
|
char buf[4];
|
||||||
|
EXPECT_FALSE(protocol_receive_n_data(&timed, buf, sizeof(buf)));
|
||||||
|
close(p[0]);
|
||||||
|
close(p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_send_receive_status_timed() {
|
static void test_send_receive_status_timed() {
|
||||||
int p[2];
|
int p[2];
|
||||||
EXPECT_EQ_INT(pipe(p), 0);
|
EXPECT_EQ_INT(pipe(p), 0);
|
||||||
@@ -440,6 +469,7 @@ void test_protocol() {
|
|||||||
test_send_receive_data();
|
test_send_receive_data();
|
||||||
test_send_receive_int();
|
test_send_receive_int();
|
||||||
test_send_receive_status();
|
test_send_receive_status();
|
||||||
|
test_protocol_session_io_timeout();
|
||||||
test_send_receive_status_timed();
|
test_send_receive_status_timed();
|
||||||
test_receive_n_data_truncated();
|
test_receive_n_data_truncated();
|
||||||
test_receive_str_truncated();
|
test_receive_str_truncated();
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "test_receiver_timeout.h"
|
||||||
|
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "receiver.h"
|
||||||
|
#include "test_utils.h"
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static bool sink_discard(File* file, void* context) {
|
||||||
|
(void)context;
|
||||||
|
file_destroy(file);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The idle/session bound is a pure function of three monotonic timestamps, so
|
||||||
|
* it can be exercised deterministically without sleeping an hour. A tiny
|
||||||
|
* overridden limit covers the same arithmetic the loop uses. */
|
||||||
|
static void test_receiver_time_limit_predicate() {
|
||||||
|
receiver_set_time_limits(10, 100);
|
||||||
|
struct timespec start = {.tv_sec = 1000, .tv_nsec = 0};
|
||||||
|
struct timespec fresh = {.tv_sec = 1000, .tv_nsec = 0};
|
||||||
|
struct timespec just_idle = {.tv_sec = 1009, .tv_nsec = 0}; /* 9 s no progress */
|
||||||
|
struct timespec idle = {.tv_sec = 1010, .tv_nsec = 0}; /* 10 s no progress */
|
||||||
|
struct timespec just_wall = {.tv_sec = 1099, .tv_nsec = 0};
|
||||||
|
struct timespec wall = {.tv_sec = 1100, .tv_nsec = 0}; /* 100 s session */
|
||||||
|
struct timespec wp_just = {.tv_sec = 1098, .tv_nsec = 0}; /* idle 1 s */
|
||||||
|
struct timespec wp_wall = {.tv_sec = 1099, .tv_nsec = 0}; /* idle 1 s */
|
||||||
|
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &fresh, &fresh));
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &fresh, &just_idle));
|
||||||
|
EXPECT_TRUE(receiver_time_limit_exceeded(&start, &fresh, &idle));
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &wp_just, &just_wall));
|
||||||
|
EXPECT_TRUE(receiver_time_limit_exceeded(&start, &wp_wall, &wall));
|
||||||
|
|
||||||
|
/* Reset restores the generous production defaults (1 h idle / 24 h total). */
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
struct timespec under_hour = {.tv_sec = 1000 + 3599, .tv_nsec = 0};
|
||||||
|
EXPECT_FALSE(receiver_time_limit_exceeded(&start, &start, &under_hour));
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drive the actual receive loop with a test-only idle limit of 0 so the very
|
||||||
|
* first keepalive is rejected: this exercises the loop's abort path (log +
|
||||||
|
* STATUS_ERROR + return -1) with no timing dependence. */
|
||||||
|
static void test_receiver_aborts_idle_keepalive() {
|
||||||
|
receiver_set_time_limits(0, 3600);
|
||||||
|
int sv[2];
|
||||||
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
|
||||||
|
Config* config = config_create();
|
||||||
|
EXPECT_NOT_NULL(config);
|
||||||
|
ReceiverSink sink = {.store_file = sink_discard,
|
||||||
|
.context = NULL,
|
||||||
|
.send_error = true,
|
||||||
|
.send_success = false,
|
||||||
|
.send_success_frame = NULL};
|
||||||
|
|
||||||
|
/* Bind an explicit session so the fd-based receive helpers use the
|
||||||
|
* socketpair rather than any transport left over from an earlier test. */
|
||||||
|
ProtocolSession session;
|
||||||
|
protocol_session_init(&session, sv[1], sv[1]);
|
||||||
|
protocol_session_bind(&session);
|
||||||
|
|
||||||
|
Status keepalive = STATUS_KEEPALIVE;
|
||||||
|
ssize_t wrote = write(sv[0], &keepalive, sizeof(keepalive));
|
||||||
|
int result = -2;
|
||||||
|
if (wrote == (ssize_t)sizeof(keepalive))
|
||||||
|
result = receiver_process_pending(config, sv[1], &sink, NULL);
|
||||||
|
Status reply = STATUS_OK;
|
||||||
|
ssize_t got = -1;
|
||||||
|
if (result == -1)
|
||||||
|
got = read(sv[0], &reply, sizeof(reply));
|
||||||
|
|
||||||
|
/* Tear down the binding/descriptors BEFORE asserting: an EXPECT_* failure
|
||||||
|
* returns immediately, and a dangling bound_session would poison later
|
||||||
|
* fd-level protocol I/O tests. */
|
||||||
|
protocol_session_unbind();
|
||||||
|
config_delete(config);
|
||||||
|
close(sv[0]);
|
||||||
|
close(sv[1]);
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
|
||||||
|
EXPECT_EQ_INT((int)wrote, (int)sizeof(keepalive));
|
||||||
|
EXPECT_EQ_INT(result, -1);
|
||||||
|
EXPECT_EQ_INT((int)got, (int)sizeof(reply));
|
||||||
|
EXPECT_EQ_INT((int)reply, (int)STATUS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_receiver_timeout(void) {
|
||||||
|
/* EXPECT_* returns from the current function on failure, so reset the
|
||||||
|
* process-global limits around the subtests (and again after) to guarantee a
|
||||||
|
* failed assertion cannot leave the receiver aborted for later tests. */
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
test_receiver_time_limit_predicate();
|
||||||
|
test_receiver_aborts_idle_keepalive();
|
||||||
|
receiver_reset_time_limits();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef TEST_RECEIVER_TIMEOUT_H
|
||||||
|
#define TEST_RECEIVER_TIMEOUT_H
|
||||||
|
|
||||||
|
void test_receiver_timeout(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1317,6 +1317,58 @@ static void test_scanner_captures_directory_times() {
|
|||||||
rmdir(root);
|
rmdir(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ownership guard for chunk_data_to_chunk(): a returned Chunk owns its File
|
||||||
|
* objects, so destroying the chunk must free them exactly once and the scanner
|
||||||
|
* must never free them again. chunk_size = 1 forces the mid-directory
|
||||||
|
* conversion branch (chunk_data_size > chunk_size) for every file, and the
|
||||||
|
* chunk is destroyed immediately, catching a double free / use-after-free under
|
||||||
|
* ASan if ownership transfer regressed.
|
||||||
|
*
|
||||||
|
* The failure path (array_list_to_array() or chunk_create() returning NULL) is
|
||||||
|
* not reachable from a unit test: both allocate through protocol_alloc(), and
|
||||||
|
* each allocation they perform is no larger than the array_list allocations
|
||||||
|
* that already succeeded while building the list (array_list_to_array() copies
|
||||||
|
* exactly `size` pointers, which never exceeds the capacity just grown, and
|
||||||
|
* sizeof(Chunk) is far below the initial 100-entry item array). Binding a
|
||||||
|
* small --max-alloc session therefore always fails *before* this function, not
|
||||||
|
* inside it, so fault injection cannot isolate these paths. */
|
||||||
|
static void test_scanner_chunk_ownership() {
|
||||||
|
const char* dir = "test_scan_ownership";
|
||||||
|
const char* file1 = "test_scan_ownership/a.txt";
|
||||||
|
const char* file2 = "test_scan_ownership/b.txt";
|
||||||
|
const char* file3 = "test_scan_ownership/c.txt";
|
||||||
|
|
||||||
|
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||||
|
create_test_file(file1, "aaaa");
|
||||||
|
create_test_file(file2, "bbbb");
|
||||||
|
create_test_file(file3, "cccc");
|
||||||
|
|
||||||
|
ScannerOptions options = {0};
|
||||||
|
options.chunk_size = 1;
|
||||||
|
DirectoryScanner* scanner = directory_scanner_create_with_options(dir, &options);
|
||||||
|
EXPECT_NOT_NULL(scanner);
|
||||||
|
|
||||||
|
int chunks = 0;
|
||||||
|
int files = 0;
|
||||||
|
Chunk* chunk;
|
||||||
|
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
||||||
|
chunks++;
|
||||||
|
files += chunk->element_count;
|
||||||
|
EXPECT_EQ_INT(chunk->element_count, 1);
|
||||||
|
chunk_destroy(chunk);
|
||||||
|
EXPECT_FALSE(directory_scanner_failed(scanner));
|
||||||
|
}
|
||||||
|
EXPECT_EQ_INT(files, 3);
|
||||||
|
EXPECT_EQ_INT(chunks, 3);
|
||||||
|
EXPECT_FALSE(directory_scanner_failed(scanner));
|
||||||
|
|
||||||
|
directory_scanner_destroy(scanner);
|
||||||
|
unlink(file1);
|
||||||
|
unlink(file2);
|
||||||
|
unlink(file3);
|
||||||
|
rmdir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
void test_scanner() {
|
void test_scanner() {
|
||||||
test_scanner_single_file();
|
test_scanner_single_file();
|
||||||
test_scanner_multiple_files();
|
test_scanner_multiple_files();
|
||||||
@@ -1353,4 +1405,5 @@ void test_scanner() {
|
|||||||
test_dirs_files_from();
|
test_dirs_files_from();
|
||||||
test_files_from_relative_send_path();
|
test_files_from_relative_send_path();
|
||||||
test_scanner_captures_directory_times();
|
test_scanner_captures_directory_times();
|
||||||
|
test_scanner_chunk_ownership();
|
||||||
}
|
}
|
||||||
@@ -356,6 +356,69 @@ static void test_loopback_helpers() {
|
|||||||
close(listener);
|
close(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The daemon host ACL reads the numeric peer address through
|
||||||
|
* utils_fd_peer_ip. A real loopback TCP peer reports "127.0.0.1"; a pipe or an
|
||||||
|
* AF_UNIX socketpair has no INET peer and must return false with an empty
|
||||||
|
* buffer (the fail-closed "cannot tell" result). */
|
||||||
|
static void test_fd_peer_ip() {
|
||||||
|
char ip[INET6_ADDRSTRLEN];
|
||||||
|
EXPECT_FALSE(utils_fd_peer_ip(-1, ip, sizeof(ip)));
|
||||||
|
EXPECT_EQ_STR(ip, "");
|
||||||
|
EXPECT_FALSE(utils_fd_peer_ip(-1, NULL, 0));
|
||||||
|
|
||||||
|
int pipe_fds[2];
|
||||||
|
EXPECT_EQ_INT(pipe(pipe_fds), 0);
|
||||||
|
EXPECT_FALSE(utils_fd_peer_ip(pipe_fds[0], ip, sizeof(ip)));
|
||||||
|
EXPECT_EQ_STR(ip, "");
|
||||||
|
close(pipe_fds[0]);
|
||||||
|
close(pipe_fds[1]);
|
||||||
|
|
||||||
|
int pair_fds[2];
|
||||||
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, pair_fds), 0);
|
||||||
|
EXPECT_FALSE(utils_fd_peer_ip(pair_fds[0], ip, sizeof(ip)));
|
||||||
|
EXPECT_EQ_STR(ip, "");
|
||||||
|
close(pair_fds[0]);
|
||||||
|
close(pair_fds[1]);
|
||||||
|
|
||||||
|
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_ip(accepted, ip, sizeof(ip)));
|
||||||
|
EXPECT_EQ_STR(ip, "127.0.0.1");
|
||||||
|
|
||||||
|
/* utils_sockaddr_to_string includes the port for a real peer. */
|
||||||
|
struct sockaddr_storage peer;
|
||||||
|
socklen_t peer_len = sizeof(peer);
|
||||||
|
EXPECT_EQ_INT(getpeername(accepted, (struct sockaddr*)&peer, &peer_len), 0);
|
||||||
|
char peer_string[128];
|
||||||
|
EXPECT_TRUE(
|
||||||
|
utils_sockaddr_to_string((const struct sockaddr*)&peer, peer_string, sizeof(peer_string)));
|
||||||
|
EXPECT_TRUE(strncmp(peer_string, "127.0.0.1:", strlen("127.0.0.1:")) == 0);
|
||||||
|
close(accepted);
|
||||||
|
close(dialer);
|
||||||
|
close(listener);
|
||||||
|
|
||||||
|
/* A non-INET family formats to "unknown" at the call site, not a bogus IP. */
|
||||||
|
struct sockaddr sa_unix;
|
||||||
|
memset(&sa_unix, 0, sizeof(sa_unix));
|
||||||
|
sa_unix.sa_family = AF_UNIX;
|
||||||
|
EXPECT_FALSE(utils_sockaddr_to_string(&sa_unix, peer_string, sizeof(peer_string)));
|
||||||
|
EXPECT_EQ_STR(peer_string, "");
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -363,6 +426,7 @@ void test_shared_utils() {
|
|||||||
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();
|
test_loopback_helpers();
|
||||||
|
test_fd_peer_ip();
|
||||||
|
|
||||||
/* --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