pub struct SshConfig {Show 17 fields
pub bind: SocketAddr,
pub server_id: String,
pub inactivity_timeout: Option<Duration>,
pub exec_grace: Option<Duration>,
pub auth_rejection_time: Duration,
pub max_auth_attempts: usize,
pub keepalive_interval: Option<Duration>,
pub max_concurrent_transfers: Option<usize>,
pub allow_anonymous: bool,
pub max_transfers_per_principal: Option<usize>,
pub max_anonymous_transfers: Option<usize>,
pub per_principal_limit_is_hard: bool,
pub memory_budget_bytes: Option<u64>,
pub admission_wait: Option<Duration>,
pub keyed_admission_wait: Option<Duration>,
pub session_end_grace: Duration,
pub session_end_grace_crowded: Duration,
}Expand description
How the SSH listener behaves.
The defaults are the appliance’s: bind everywhere on 2222, publickey only, and three separate ways for a connection that is going nowhere to be closed.
Fields§
§bind: SocketAddrWhere to listen. Unlike the gRPC control plane — which defaults to loopback because it is unauthenticated in v1 — this listener authenticates every connection with a public key, so binding it publicly is the intended configuration.
server_id: StringThe SSH-2.0-… identification string.
inactivity_timeout: Option<Duration>Close a connection that has sent nothing for this long. Covers the client that completes a handshake and then holds the socket open.
exec_grace: Option<Duration>How long an authenticated session may sit without asking for a git command before it is disconnected.
This is the leak the inactivity timeout does not catch: a client that authenticates, opens a session channel, and then answers keepalives forever is never idle. Sixty seconds is far longer than any real client takes between authenticating and sending its exec request.
auth_rejection_time: DurationConstant-time rejection delay, so a wrong key cannot be distinguished from a slow lookup.
max_auth_attempts: usizeHow many authentication attempts one connection may make.
keepalive_interval: Option<Duration>Send a keepalive if nothing has arrived for this long.
max_concurrent_transfers: Option<usize>How many git transfers may be in flight across the whole server at once.
None turns the limit off.
Before this existed the ceiling was tokio’s default of 512 blocking
threads, one per transfer, and going past it queued silently. This
ceiling is the same kind of thing, except that somebody chose the number
and both the client and the operator get told when it bites
(crate::AdmissionStats).
The default is deliberately below the blocking-thread pool it is
protecting: see gunnar_cli::server::serve::GIT_BLOCKING_THREADS, whose
own test asserts the relationship rather than leaving it to a comment.
allow_anonymous: boolWhether an unauthenticated caller may read public repositories
true by default.
Turning it off closes the anonymous door at the listener, before any
repository is resolved and before the ACL is consulted, so no
Visibility setting can reopen it. That is the switch for an appliance
that must be able to state “this box has never served a stranger”; it is
not what protects private repositories, which are protected by the
ACL and are unreachable anonymously either way.
The default is true because Visibility::Private is the default for
a repository: nothing is anonymously readable until someone deliberately
marks it public. See
SshConfig::without_anonymous_access.
max_transfers_per_principal: Option<usize>How many git transfers one principal is guaranteed at once, and its
share of a contended server. None turns the limit off.
The server-wide limit alone is exhaustible by a single client, which would then be denying service to everybody else; a fork-bomb of clones is exactly the stated threat. The limit is per principal, not per IP, because gunnar authenticates keys: a per-IP cap would throttle a whole office behind one NAT and miss a botnet entirely.
It is a floor, not a wall. Until 2026-08-03 it bound
unconditionally, and measured on a 32-core desktop that meant one
principal with 64 clients got 32 served and 32 refused against 224 idle slots: the
advertised 256 needed eight separate identities and no single-team
appliance could ever reach it. Now the number is what a principal always
gets and what it is held to once the server is contended; below that the
server-wide limit is the only ceiling. crate::admission has the rule
and the measurements.
max_anonymous_transfers: Option<usize>How many git reads may be served without a key at once.
None lets anonymous take an ordinary principal’s max-min share.
The companion to Self::allow_anonymous, and the reason it needs one:
that flag decides whether the door is open, and this decides how wide.
Without it, the open-by-default anonymous door inherits the per-principal share —
which is work-conserving on purpose, so on an idle appliance a stranger’s
ceiling is the entire server (crate::admission has the rule and why it
is right for a keyed identity and wrong for this one).
The default is DEFAULT_MAX_ANONYMOUS_TRANSFERS (5), which is under
2 % of the default server-wide 256 and one sixth of what a single keyed
identity is guaranteed. That asymmetry is the point: an operator must
never be made to wait behind traffic gunnar cannot name.
per_principal_limit_is_hard: boolMake Self::max_transfers_per_principal an unconditional ceiling
again, contention or not.
false by default, because the appliance case is one team behind a
handful of keys and a wall there only throttles the owner. Set it on a
host where an identity is a tenant and the cap is the thing being
sold: there, letting a tenant use an idle server is giving away what was
metered.
memory_budget_bytes: Option<u64>The memory budget the server is allowed to hold for git operations
in flight, in bytes (gate 2 of crate::governor).
A reservation gunnar owns, never a measurement of the machine. By the
OS-less law this crate may not read /proc, sysinfo or getrusage, so
it cannot size itself to the box and does not pretend to: this is the
number an operator sets, and the default
(DEFAULT_MEMORY_BUDGET_BYTES,
8 GiB) is the cgroup the bakeoff measurement was taken in, not a fit to
this host. Lower it on a small machine.
None only where Self::max_concurrent_transfers is also None: an
unlimited gate is unlimited in both dimensions.
admission_wait: Option<Duration>How long a transfer may wait for a slot before it is refused
(crate::governor::DEFAULT_ADMISSION_WAIT).
Some(Duration::ZERO) restores the pre-2026-08-30 gate exactly — refuse
on the spot — and is the arm the before/after table in
the published benchmark numbers were measured against. None takes
the default.
It is a bound, not a queue depth: nothing is enqueued, the waiting
session is one parked future, and the wait is counted
(crate::AdmissionStats::peak_wait_ms) and is the CoDel controller’s
input. See crate::governor::DEFAULT_ADMISSION_WAIT for why refusing
a client that will reconnect costs more than parking it.
keyed_admission_wait: Option<Duration>How long an authenticated caller may be queued for a slot — and,
after a restart, for the stores to open — before it is refused.
None: keyed callers get SshConfig::admission_wait like everybody.
See crate::governor::DEFAULT_KEYED_ADMISSION_WAIT.
session_end_grace: DurationHow long a transfer whose session ended is given to end by itself
before it is told to stop (crate::channel::SESSION_END_GRACE).
session_end_grace_crowded: DurationThe same grace when others wait for the transfer’s fan
(crate::channel::CROWDED_SESSION_END_GRACE): a worker a hung-up
client holds is one a queued client does not get (stinky row 19).
Implementations§
Source§impl SshConfig
impl SshConfig
Sourcepub fn ephemeral_loopback() -> Self
pub fn ephemeral_loopback() -> Self
A configuration bound to an ephemeral loopback port, for tests.
The timeouts are shortened so a test that waits for one finishes in under a second. It is a constructor, not a mode: nothing here changes what is authenticated or what is dispatched.
Sourcepub fn without_anonymous_access(self) -> Self
pub fn without_anonymous_access(self) -> Self
The same configuration with the anonymous read path closed.
After this, a caller with no key cannot open a session at all, not even
to a repository marked Visibility::Public.