Semantic search over a Markdown knowledge base, served over MCP.
GrooveSeek は誰も認証しない。--i-know 無しで非 loopback な bind を求められたときに
出す拒否文自体がそう書いている
(grooveseek/src/transport/mod.rs の non_loopback_bind_refusal) —
そのポートに到達できる者は、知識ベース全体を読める。したがって到達性がアクセス制御の
すべてであり、v1.10.0 まで到達性とは TCP ポートのことだった。loopback のポートは
そのホスト上のすべてのアカウントに開いているので、1 台に複数の daemon を
(文書の束ごとに 1 つ、それぞれ 1 人の呼び出し元のために) 立てても、
どのポートが誰のものかという取り決め以上に強いものでは分かれていなかった。
ファイルシステム上の socket は、この問いをカーネルが既に答えている場所へ移す。
所有者と mode は connect(2) の時点で、最初の 1 バイトが届く前に、
他のあらゆるファイルを守っているのと同じコードによって検査される。
必要なのは、daemon が起動する前に、正しい所有者と mode でそれを作る誰かである。
この記録が答えるのは「AF_UNIX で serve するかどうか」ではない。
誰がその socket を作るのか、そして約束された socket を受け取れなかったとき
serve は何をするのかである。
sd_listen_fds(3) が受け渡しの手順と、それを安全にする検査を既に定めている。
独自に発明すれば、間違えられる箇所が 2 つになるListenStream= は 1 つなので、
「ちょうど 1 本」はそこでは何の代償にもならず、
すべての拒否が「読めた値」を名指しできる状態を保てる
(grooveseek/src/transport/systemd_fd.rs の check_listen_env、
LISTEN_FDS を見る分岐).socket unit が既に bind した descriptor を、明示 opt-in で受け取る。
受け取れなければ起動を拒否する。 — 採った案。
socket activation を環境変数から自動検出する (LISTEN_FDS が居れば使う)。
却下。1 つのコマンドラインが 2 つの異なる意味を持つことになり、しかも
どちらになるかを決める変数は継承される。activation された service から起動した shell、
自分の descriptor を渡し込む supervisor、他者のために変数を設定した親 —
どれも同じ形で serve に届く。sd_listen_fds(3) が最初に置いている検査は、
まさに環境変数が descriptor より遠くまで伝わるから存在する
(grooveseek/src/transport/systemd_fd.rs の check_listen_env、
LISTEN_PID を見る分岐)。自動検出にすると、その検査を通り抜けることが
「opt-in された経路」ではなく「通常の経路」になる。
groove 自身が path に bind する (--unix-socket <path>)。却下。
動きはするが、socket を作る主体が 2 つになる。所有者と mode は、groove と
unit ファイルと周囲の umask のうち先に到達したものが決めることになり、
しかも配置ごとに違う主体が決める。本設計では socket を作るのは service manager の仕事であり、
そこに置いたままにしておくことが「誰がここへの到達可否を決めたか」に答えを 1 つだけ残す。
unit と groove の間に systemd-socket-proxyd を挟む (systemd の man の
namespace の例が採っている形)。proxy の 3 つの性質を根拠に却下。いずれも一次情報:
SO_KEEPALIVE も無い
(https://github.com/systemd/systemd/issues/23320)。MCP の Streamable HTTP は
接続を張り続けるので、半死の接続が --connections-max= の枠を食うsystemd-socket-proxyd(8) の原文が
“will not forward SCM_RIGHTS, SCM_CREDENTIALS, SCM_SECURITY, SO_PEERCRED,
SO_PEERPIDFD, SO_PEERSEC, SO_PEERGROUPS and similar” と書いている4 つ目の懸念には裏付ける文書が無いので、推定としてここに残す: proxy は上流への接続を持つ前に接続を受け付けるので、呼び出し元の死活確認は 「接続できた」と答えられてから待たされ、失敗にはならないと考えられる。 これは実測していない。する必要も無かった — 上の 3 つで足りている。
いずれにせよ daemon ごとにプロセスが 1 つ前に挟まる。descriptor を直接 受け取る形なら 0 である。
SO_PEERCRED で接続元の uid を照合する。却下 — そしてこの道が開いているのは
選択肢 4 のおかげである。proxy を通さないことが、資格情報が届く状態を残している。
axum の Connected trait は sealed ではなく
(axum-0.8.9/src/extract/connect_info.rs:80-83)、axum 自身が UnixListener 向けの
実装をコンパイル試験として持っている (axum-0.8.9/src/serve/mod.rs:503-513)。
IncomingStream::io() (axum-0.8.9/src/serve/mod.rs:436-439) から
UnixStream が取れ、その peer_cred() が答えを返す。
それでも採らない。 socket の mode が、同じ問いをより早い段階でカーネルに
立てさせている。アプリケーション層でもう一度問えば、1 つの条件が
socket の mode とアプリケーションのコードという、食い違いうる別々の場所へ
分かれる。加えて Connected::connect_info は
io::Result ではなく Self を返す
(axum-0.8.9/src/extract/connect_info.rs:82) ので、
peer_cred() が失敗したときにそれを申告する先が無い。既定値は
fail-open か、理由の言えない拒否のどちらかにしかならない。
覆る条件: 1 つの socket を複数の principal に開ける必要が出たとき (想定する呼び出し元とは別に監視エージェントを足す、外部の read-only 利用者を作る等)。 そのとき mode はグループへ広げられ、uid の区別をここで持つ必要が出る。
groove serve は渡された socket で accept し、自分では socket を作らない。
--systemd-socket、または [transport.http].systemd_socket = true。
どちらも無ければ LISTEN_FDS は誰も読まず、要求しない daemon は
v1.11.0 より前のすべてのリリースと同じように振る舞うserve は終了する。descriptor の検査は
sd_listen_fds(3) が定める順序で行う: まず LISTEN_PID を自プロセスの pid と照合する
(親から継承した変数は、そうしなければ他人の descriptorを掴ませる)、次に
LISTEN_FDS がちょうど 1 であること、そして SO_ACCEPTCONN より先に SO_TYPE を見る
(ListenDatagram= の unit を「listening でない」ではなく型の誤りとして報告するため)
(grooveseek/src/transport/systemd_fd.rs の check_listen_env と
check_listening_stream)--bind / --port / [transport.http].bind のいずれも、
これと並んで立つことを拒否する
(grooveseek/src/transport/mod.rs の resolve_systemd_listen)。
待ち受けアドレスが 2 つあるのは設定ではないし、黙って一方を勝たせれば
運用者は何も応答しないアドレスを読み続けることになるLISTEN_FDS を渡す service manager (systemd、または同じ
protocol を話すもの) があること。他の OS 向けビルドはフラグもキーも拒否する
(grooveseek/src/transport/mod.rs の systemd_socket_supported)。
なぜ当初より狭いかは次項cfg(unix) の機能として出した — Windows に LISTEN_FDS
protocol が無く、他の Unix にはあり、macOS は単に LISTEN_PID が無いと分かるだけ、
という読みだった。CI の macOS leg がそれを否定した: macOS は
getsockopt(SO_ACCEPTCONN) を実装しておらず ENOPROTOOPT を返すので、
service manager が正しく渡した socket を含め、どの descriptor でも検査が失敗する
(2026-09-18、CI の macOS leg が報告した)。取れる道は 2 つだった —
答えられない platform では検査を飛ばすか、その platform を名乗るのをやめるか。
飛ばす方は採らなかった: この module で listening な socket と接続済みの socket を
分けているのは SO_ACCEPTCONN だけであり、上の決定は拒否のひとつひとつを
要の位置に置いているうえ、手元で誰も走らせられない target でだけ欠ける検査は、
気づかれずに腐る。そこで target の側を狭めた。
他の OS は「そこでも検査が成り立つ」と確かめてから足せる。
その判断を誤った時の代償は、未検証の descriptor で serve する daemon ではなく、
起動時の拒否になった/mcp の既定値も、
bind の場合とまったく同じようにそのアドレスから導かれる。Unix socket は
アドレスを一切持たない listener になる
(grooveseek/src/transport/systemd_fd.rs の adopt)。
唯一これに従わないのが admin の Host allow-list で、
allowed_admin_hosts に足されるのはこのプロセス自身が bind したアドレスだけである
(grooveseek/src/server.rs の run_server。HttpListen::Tcp で分解している)shutdown(2) を呼ばず、
path を unlink もしない。service manager が自分の descriptor の複製を持ち続けており、
systemd.socket(5) は service が
“must not unlink the socket from a file system” と書いているgetsockopt / getsockname / fcntl を、この crate が
cfg(unix) で既に持っている libc から呼ぶ。libsystemd や listenfd は足さないgrooveseek/src/transport/http.rs の open_listener)。それを読む判断は、peer 規則・
Host の既定・Origin の既定・起動時の行であり、いずれも
open_listener が返す同じ Option<SocketAddr> から来る
(run_http の match bound と、そこから admin router へ渡す peer:。
どちらも grooveseek/src/transport/http.rs)。だから
見落としやすい場合 (unit が TCP の descriptor を渡してきた場合) を、
一方では一通りに、その隣では別の通りに扱う、ということが起きないUnixListener は
ConnectInfo<SocketAddr> を持たないので、これが置き換えた bool は
「有効」と読めているのに、守っているはずの条件が黙って素通りしていた。
PeerRule::UnixLocal が Unix 側で、その意味は
「socket の所有者と mode が既に決めた」であって、
「誰だか分からない」ではない
(grooveseek/src/transport/http.rs の PeerRule と admin_peer_rule)PeerRule の 3 値が
観測上すべて同じ答えを返す。検査が読む extension がそもそも付かないからである。
Unix listener に対する挙動試験が守るのは Host / Origin の配線であり、
admin_peer_rule が listener ごとに何を返すかは単体試験が守る。
run_http が admin 経路にその戻り値を渡していること自体を守るのは、
レビューだけであるOrigin の既定は port 無しの loopback の綴りになる。名指す port が無いからである。
port を持たない allow-list の entry は、そのホストの全 port に一致する
(grooveseek/src/transport/http.rs の NormalizedAuthority::matches) ので、
localhost / 127.0.0.1 / [::1] を名乗る Origin は
どの port を載せていても通り、それ以外の Origin は拒否される。
リストを空にしないのは意図的で、空は「Origin を検証しない」の綴りだからであるunsafe が transport 層に入る。ただし systemd_fd.rs の中だけである。
試験を除いたブロックの内訳は、descriptor を読む getsockopt と
getsockname、フラグを 2 つ立てる fcntl、getsockname の書き込み先である
sockaddr_storage を埋める mem::zeroed、そして唯一何かを生み出す
OwnedFd::from_raw_fd である。
grep -n "unsafe {" grooveseek/src/transport/systemd_fd.rs で一覧できる —
この 5 つより後のヒットは #[cfg(test)] の中にある。
所有権が生まれるのは take_listener の中だけで、しかも
所有せずにできる検査を先に済ませたあとである。adopt は OwnedFd を
受け取るので、その中に unsafe は現れないsystemd_socket を書いた groove.toml は
それらのリリースをそもそも起動させない。groove を先に上げ、それから unit を変えるsystemd.socket(5) の SocketMode= の既定は 0666 なので、
ListenStream= しか書かれていない unit が返すのは、
上の背景で挙げた loopback ポートとまったく同じ到達性である。
例外は、運用者に設定するものが 1 つも無い場合である: abstract socket —
名前が @ で始まる ListenStream= — はファイルシステム上の実体を持たないので、
正しく設定すべき所有者も mode も存在しない。groove は、ホスト上の全アカウントに
admin 経路を local として開くのではなく、起動時に拒否するsd_listen_fds(3) — 受け渡しの protocol と検査の順序。systemd.socket(5) —
Accept=no、socket を unlink しないこと、および SocketMode= の既定 0666。
systemd-socket-proxyd(8) — proxy が転送しないものHost / Origin を GrooveSeek 自身が答える理由grooveseek/src/transport/systemd_fd.rs (受け渡し)、
grooveseek/src/transport/mod.rs (HttpListen / resolve_systemd_listen /
systemd_socket_supported)、grooveseek/src/transport/http.rs
(open_listener / PeerRule / admin_peer_rule /
effective_allowed_hosts_unix / effective_allowed_origins_unix)