refactor(geode): drop unused KtorRelay knobs (maxFrameBytes, maxAdminBodyBytes)
Ktor's WebSocket layer is unbounded by default, so [limits].max_ws_frame_bytes was a strfry-parity tuning knob nobody actually used — drop the whole LimitsSection from StaticConfig and the matching constructor param from KtorRelay. The negentropy-large-corpus plan note is updated accordingly. maxAdminBodyBytes is a real defense-in-depth cap (NIP-98 forces us to read the body for the payload sha256 before we can authenticate, so an unbounded read is a pre-auth DoS vector). Keep the cap, but stop exposing it as an operator knob — it was never plumbed through to StaticConfig and 1 MiB is ~1000× any plausible NIP-86 RPC payload. Hardcoded at the Nip86HttpRoute construction site with the rationale inline. Nip86HttpRoute.maxBodyBytes stays so tests can drive the 413 boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,13 +66,6 @@ require_auth = false
|
||||
# the future. Enforced by RejectFutureEventsPolicy.
|
||||
# reject_future_seconds = 1800
|
||||
|
||||
[limits]
|
||||
# Maximum WebSocket frame size. Frames larger than this are dropped at
|
||||
# the WS layer. (max_ws_message_bytes maps to the same setting since
|
||||
# Ktor's WebSockets plugin only exposes per-frame caps.)
|
||||
# max_ws_message_bytes = 1048576
|
||||
# max_ws_frame_bytes = 1048576
|
||||
|
||||
[authorization]
|
||||
# Allow / deny lists. Allow is a permissive ceiling; deny still
|
||||
# removes specific entries inside it. Enforced by Pubkey/KindAllowDenyPolicy.
|
||||
|
||||
@@ -126,12 +126,13 @@ for unlimited). 500_000 is well above the floor. Pure config change in
|
||||
`NegSessionRegistry.open`; expose via `[negentropy].frame_size_limit`
|
||||
for operators who tune it down to fit smaller WS frame budgets.
|
||||
|
||||
Note: `LimitsSection.max_ws_frame_bytes`
|
||||
(`geode/src/main/kotlin/com/vitorpamplona/geode/config/StaticConfig.kt:148`)
|
||||
applies to the WebSocket layer. After hex-encoding a 500_000-byte
|
||||
negentropy payload doubles to ~1_000_000 bytes on the wire; ensure
|
||||
`max_ws_frame_bytes` is at least 2 MB (or unlimited) in default
|
||||
config so the response isn't truncated by the WS layer.
|
||||
Note: Ktor's WebSocket layer does not impose a default frame cap, so a
|
||||
~1 MB hex-encoded negentropy payload is delivered intact. The
|
||||
`[limits].max_ws_frame_bytes` operator knob was removed once Ktor's
|
||||
unbounded default was confirmed to be sufficient — see the
|
||||
`refactor(geode): drop [limits] section` commit. If a reverse proxy
|
||||
is in front of the relay, the proxy's frame cap is the only thing
|
||||
that can still truncate.
|
||||
|
||||
### D — concurrent NEG-OPEN cap, shared with REQ
|
||||
|
||||
|
||||
@@ -71,13 +71,6 @@ class KtorRelay(
|
||||
/** Pass 0 to let the OS pick a free port. Read [url] after [start] to learn it. */
|
||||
val port: Int = 0,
|
||||
val path: String = "/",
|
||||
/**
|
||||
* Per-frame size cap; mirrors `[limits].max_ws_frame_bytes` in the
|
||||
* config. Frames larger than this are rejected at the WebSocket
|
||||
* layer, which is the only layer that sees the raw bytes. `null`
|
||||
* uses Ktor's default (~1 MiB).
|
||||
*/
|
||||
val maxFrameBytes: Long? = null,
|
||||
/**
|
||||
* Pubkeys allowed to call NIP-86 admin RPCs. Empty (the default)
|
||||
* disables the admin endpoint entirely — POSTs return 403.
|
||||
@@ -100,15 +93,6 @@ class KtorRelay(
|
||||
* `Host` and bind their signature to any URL.
|
||||
*/
|
||||
val publicUrl: String? = null,
|
||||
/**
|
||||
* Maximum body size accepted on the NIP-86 POST endpoint, in
|
||||
* bytes. Bounded *before* auth verification because we read the
|
||||
* body to compute its sha256 for NIP-98's payload binding —
|
||||
* unbounded reads would let an unauthenticated attacker stream
|
||||
* gigabytes and OOM the relay. 1 MiB easily fits any plausible
|
||||
* RPC payload.
|
||||
*/
|
||||
val maxAdminBodyBytes: Int = 1 shl 20,
|
||||
/**
|
||||
* Ktor CIO acceptor-thread count. `null` keeps Ktor's default.
|
||||
* Lift on machines with many cores when targeting 10k+
|
||||
@@ -135,7 +119,13 @@ class KtorRelay(
|
||||
server = nip86Server,
|
||||
verifier = Nip98AuthVerifier(),
|
||||
allowList = adminPubkeys.mapTo(HashSet()) { it.lowercase() },
|
||||
maxBodyBytes = maxAdminBodyBytes,
|
||||
// 1 MiB. Bounded *before* NIP-98 auth verification — we
|
||||
// have to read the body to compute its sha256 for the
|
||||
// payload binding, so an unbounded read would let an
|
||||
// unauthenticated attacker stream gigabytes and OOM the
|
||||
// relay. NIP-86 RPC payloads are a few hundred bytes;
|
||||
// 1 MiB is ~1000× any plausible request.
|
||||
maxBodyBytes = 1 shl 20,
|
||||
signedUrlFor = { call ->
|
||||
publicUrl ?: ("http://" + (call.request.header(HttpHeaders.Host) ?: "$host:$resolvedPort") + path)
|
||||
},
|
||||
@@ -192,9 +182,7 @@ class KtorRelay(
|
||||
rootConfig =
|
||||
serverConfig {
|
||||
module {
|
||||
install(WebSockets) {
|
||||
maxFrameBytes?.let { maxFrameSize = it }
|
||||
}
|
||||
install(WebSockets)
|
||||
routing {
|
||||
// NIP-11: GET on the relay URL with Accept:
|
||||
// application/nostr+json returns the relay info doc.
|
||||
|
||||
@@ -49,8 +49,8 @@ import java.io.File
|
||||
*
|
||||
* Every section is enforced: `[info]` populates the NIP-11 doc,
|
||||
* `[network]` controls the bind, `[database]` chooses the SQLite path,
|
||||
* `[options]` toggles AUTH/verify/future-skew, `[limits]` plugs into
|
||||
* the relay's policy stack, and `[authorization]` seeds the runtime
|
||||
* `[options]` toggles AUTH/verify/future-skew, and `[authorization]`
|
||||
* seeds the runtime
|
||||
* [com.vitorpamplona.quartz.nip86RelayManagement.server.BanStore] on
|
||||
* first boot (see [com.vitorpamplona.geode.config.RuntimeConfig]).
|
||||
*
|
||||
@@ -140,18 +140,12 @@ fun main(args: Array<String>) {
|
||||
parallelVerify = parallelVerify,
|
||||
negentropySettings = negentropySettings,
|
||||
)
|
||||
// Frame cap honors max_ws_frame_bytes when set; max_ws_message_bytes
|
||||
// is treated as the same cap (Ktor's WebSockets plugin only exposes
|
||||
// a single per-frame limit; multi-frame messages remain unbounded).
|
||||
val frameLimit =
|
||||
(config.limits.max_ws_frame_bytes ?: config.limits.max_ws_message_bytes)?.toLong()
|
||||
val server =
|
||||
KtorRelay(
|
||||
relay,
|
||||
host = host,
|
||||
port = port,
|
||||
path = path,
|
||||
maxFrameBytes = frameLimit,
|
||||
adminPubkeys = config.admin.pubkeys.toSet(),
|
||||
publicUrl = config.admin.public_url,
|
||||
connectionGroupSize = config.network.connection_group_size,
|
||||
|
||||
@@ -44,7 +44,6 @@ data class StaticConfig(
|
||||
val network: NetworkSection = NetworkSection(),
|
||||
val database: DatabaseSection = DatabaseSection(),
|
||||
val options: OptionsSection = OptionsSection(),
|
||||
val limits: LimitsSection = LimitsSection(),
|
||||
val authorization: AuthorizationSection = AuthorizationSection(),
|
||||
val admin: AdminSection = AdminSection(),
|
||||
val negentropy: NegentropySection = NegentropySection(),
|
||||
@@ -160,11 +159,6 @@ data class StaticConfig(
|
||||
val parallel_verify: Boolean = true,
|
||||
)
|
||||
|
||||
data class LimitsSection(
|
||||
val max_ws_message_bytes: Int? = null,
|
||||
val max_ws_frame_bytes: Int? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* NIP-77 negentropy tuning. Defaults track strfry
|
||||
* (`hoytech/strfry`) so a Geode relay accepts the same workload
|
||||
@@ -173,9 +167,10 @@ data class StaticConfig(
|
||||
*
|
||||
* - [frame_size_limit] mirrors strfry's hard-coded
|
||||
* `Negentropy ne(storage, 500'000)` in `RelayNegentropy.cpp`.
|
||||
* Hex-encoded that's ~1 MB on the wire per NEG-MSG; ensure
|
||||
* `[limits].max_ws_frame_bytes` (when set) is at least double
|
||||
* this or NEG-MSGs get truncated by the WS layer.
|
||||
* Hex-encoded that's ~1 MB on the wire per NEG-MSG. Ktor's
|
||||
* WebSocket layer does not impose a default frame cap, so this
|
||||
* payload is delivered intact unless a reverse proxy is
|
||||
* misconfigured.
|
||||
* - [max_sync_events] mirrors strfry's
|
||||
* `relay__negentropy__maxSyncEvents`. NEG-OPEN whose snapshot
|
||||
* exceeds this returns
|
||||
|
||||
@@ -70,9 +70,6 @@ class StaticConfigTest {
|
||||
require_auth = true
|
||||
reject_future_seconds = 1800
|
||||
|
||||
[limits]
|
||||
max_ws_frame_bytes = 1048576
|
||||
|
||||
[authorization]
|
||||
pubkey_blacklist = ["aaaa", "bbbb"]
|
||||
kind_blacklist = [4, 1059]
|
||||
@@ -95,8 +92,6 @@ class StaticConfigTest {
|
||||
assertEquals(true, c.options.require_auth)
|
||||
assertEquals(1800, c.options.reject_future_seconds)
|
||||
|
||||
assertEquals(1_048_576, c.limits.max_ws_frame_bytes)
|
||||
|
||||
assertEquals(listOf("aaaa", "bbbb"), c.authorization.pubkey_blacklist)
|
||||
assertEquals(listOf(4, 1059), c.authorization.kind_blacklist)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user