Full V2 fetch handshake (ls-refs + fetch with done + sideband pack) runs between real git 2.43 and a Rust listener over the alkcall Connection -> BiStream -> tokio-util compat -> gix-packetline path. Verification: git ls-remote, git clone (fsck --strict clean), incremental git fetch, annotated-tag checkout, in-process duplex selftest. All over alkcall 0.8 core types; the flagged futures-io risk resolved with a two-line tokio-util compat adapter. Findings: docs/research/poc-1-findings.md (deliverable; POC source stays disposable at /workspace/alkgit-poc1 per the standalone POC mode). Also records in the research docs: - git-protocol.md: observed done-path shape (no acknowledgments section), git:// first-request framing, advertise-once rule - gitoxide.md: gix-packetline async stop-delimiter + reset() contract - alk-stack.md: POC-1 verification item marked resolved - pocs.md: POC-1 status -> proceed
113 lines
6.2 KiB
Markdown
113 lines
6.2 KiB
Markdown
# Research: alk stack fit for a git server
|
|
|
|
**Status**: initial pass complete
|
|
**Date**: 2026-09-19
|
|
**Sources**: local sibling crates at `/workspace/@alkdev/{alkcall,alkhttp,alktls,alkvault}`
|
|
(all MIT OR Apache-2.0; alkcall/alkhttp/alktls published on crates.io,
|
|
alkvault published at 0.1.0).
|
|
|
|
## TL;DR
|
|
|
|
The alk stack covers the parts of a self-hosted git server that were
|
|
historically the attack surface: the HTTP and SSH front doors, the wire
|
|
framing, TLS/ACME, and secret storage. alkgit contributes the git brain
|
|
(storage + smart protocol + repo/permission model) and the server binary that
|
|
assembles everything. The architectural match with alkcall is unusually good:
|
|
**alkcall's `BiStream` is exactly the byte-stream abstraction git's
|
|
packetline-based protocol wants underneath it** — the smart protocol is a
|
|
pair of unidirectional streams with request/response framing, which maps
|
|
cleanly onto `BiStream::write_half` / `read_half`.
|
|
|
|
## How the pieces map
|
|
|
|
| alkgit need | alk crate | mechanism |
|
|
|---|---|---|
|
|
| Authenticated transport with per-peer identity | alkcall | `Connection`, `Identity`, `IdentityProvider` |
|
|
| Internal/external operation split | alkcall | `Visibility::Internal` ops are never wire-callable (ADR-017/024) |
|
|
| Per-repo/per-op authorization | alkcall | `AccessControl::check(peer_identity)` + `required_scopes` |
|
|
| HTTP front door for git smart-http + admin API | alkhttp | HTTP/1.1 + HTTP/2 serving, adapters for call protocol |
|
|
| TLS + ACME for the HTTP endpoint | alktls | rustls server configs, cert resolvers, ACME state machine |
|
|
| Encrypted storage of tokens/keys at rest | alkvault | AES-256-GCM vault |
|
|
| Byte streams under pkt-line | alkcall | `BiStream` handed to handlers via `ProtocolHandler::connected` |
|
|
|
|
## The gitea lesson, restated as alkgit design constraints
|
|
|
|
The pwned-gitea incident (CVE-2026-59774) was reachable because an internal
|
|
API was exposed over the internet regardless of auth — the openapi-spec
|
|
pattern of "describe everything, enforce somewhere" is the structural
|
|
antipattern. alkcall already fixes the API-exposure half: only ops the caller
|
|
has privileges for are even visible, and internal ops are structurally
|
|
unreachable from the wire. alkgit must extend the same philosophy:
|
|
|
|
1. **No unauthenticated surface by default.** Even `info/refs` advertisement
|
|
requires an authenticated session unless a repo is explicitly public.
|
|
Anonymous-clone is a per-repo opt-in, not a global default.
|
|
2. **Admin API = internal ops.** Repo/user/permission management rides the
|
|
call protocol as `Visibility::Internal` ops over the admin interface
|
|
(or via alkhttp with auth); there is no admin endpoint that exists on the
|
|
same unauthenticated surface as git traffic.
|
|
3. **No plaintext secrets in the DB.** Credentials/tokens go through
|
|
alkvault; the metadata store holds references, not keys.
|
|
4. **Blast-radius thinking carries into the design**: single binary, no
|
|
plugin execution, no markdown renderer by default (a web UI, if it ever
|
|
exists, is a separate phase and separate threat model).
|
|
|
|
## Interfaces in alkcall terms
|
|
|
|
Git transport framing per interface:
|
|
|
|
- **SSH**: the alkcall channels protocol carries a channel whose stream is
|
|
the git pkt-line conversation. The client (`git clone ssh://...`) expects
|
|
an exec of `git-upload-pack '<repo>'`; with alkcall the "SSH channel-like"
|
|
layer provides the framing and identity, and we dispatch on the requested
|
|
command string. alkcall's channel model (channel 0 = call registry, data
|
|
channels = `BiStream`) maps to: one data channel per git session.
|
|
- **HTTP**: alkhttp hosts the smart-http endpoints (`GET
|
|
/<repo>/info/refs?service=git-upload-pack`, `POST /<repo>/git-upload-pack`,
|
|
`POST /<repo>/git-receive-pack`). Smart-http is stateless per request
|
|
(V0/V1) or one-shot (V2), which fits the http adapter model; the response
|
|
body is a pkt-line stream we generate.
|
|
|
|
Both interfaces converge on the same core: **a `BiStream`-like duplex
|
|
session + repo identity + caller identity** → `alkgit-transport` runs the
|
|
smart protocol and produces/consumes packs via `alkgit-core`. The interfaces
|
|
are thin adapters; all policy (auth, ACL, limits) lives before the transport
|
|
layer gets the stream. This is the same "ALPN as a service" shape as
|
|
alktty/alktunnels/alksocks: the git member of the family, one core, many
|
|
front doors, and nothing below the transport boundary knows or cares which
|
|
door produced the stream.
|
|
|
|
## What to verify before architecture commits (POC candidates)
|
|
|
|
1. **BiStream ↔ pkt-line fit**: run a real `git clone` handshake over an
|
|
alkcall `Connection` using `gix-packetline` async codec end-to-end.
|
|
**Verified (POC-1, 2026-09-20)**: real git 2.43 clones/fetches
|
|
fsck-clean over `Connection → BiStream → tokio-util compat →
|
|
gix-packetline`. See `poc-1-findings.md`.
|
|
2. **HTTP smart protocol shape**: confirm alkhttp's streaming response body
|
|
can carry a pack (chunked) and that request bodies stream in without
|
|
buffering the whole POST in memory (receive-pack can be gigabytes).
|
|
3. **alkcall backpressure/limits vs git sessions**: large clones are
|
|
long-lived, high-throughput, single-direction streams; confirm channels
|
|
buffer limits (ADR-040) don't fight pack streaming, or route git sessions
|
|
as raw duplex streams rather than chunk-framed channels if needed.
|
|
4. **alktls ACME**: config shape for the http endpoint; likely trivial.
|
|
|
|
## Composability boundary (what "ALPN as a service" implies here)
|
|
|
|
Same family pattern as alktty/alktunnels/alksocks (alknet decomposition):
|
|
one core that is front-door-blind, adapters that each speak one door. For
|
|
alkgit the boundary is concrete:
|
|
|
|
- `alkgit-core` + `alkgit-transport` depend on alkcall types only
|
|
(`BiStream`, identity/ACL types); they never depend on alkhttp or
|
|
alkgit-ssh, and carry no http/channel-specific types below the stream.
|
|
- The http and ssh crates are replaceable: a downstream app (gitea-like)
|
|
embeds the core + transport and brings its own front doors.
|
|
- Admin API = alkcall ops, usable as-is or replaced by a downstream app.
|
|
|
|
## Version pins in workspace manifests
|
|
|
|
- `alkcall 0.8` (published), `alkhttp 0.5` (published), `alktls 0.1`
|
|
(published), `alkvault 0.1` (published). The workspace root already pins
|
|
these; bump via workspace.dependencies when siblings release. |