Reach the GitHub broker over a mounted socket instead of the network (#213)
Computers on evo could not reach the broker over TCP: the git shim reported that evOo's GitHub connection was unreachable, so cloning private repositories still failed. Container-to-host traffic depends on the host firewall and on which Docker network the computer joined. - On a Linux host the supervisor also serves the broker on a unix socket in a host directory (SANDBOX_GITHUB_BROKER_SOCKET_DIR, default ~/.rakazo/github-broker), and mounts that directory read-only into each new computer once the socket listens. - The shim uses the socket when it is present and falls back to the network otherwise; connections time out after 5 seconds and the unreachable message names the curl exit code. Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f136451e22
commit
7452b8c6d2
@ -4,7 +4,7 @@ set -euo pipefail
|
||||
endpoint="$1"
|
||||
payload="$2"
|
||||
post() {
|
||||
curl -fsS -X POST "$1/$endpoint" \
|
||||
curl -fsS --connect-timeout 5 -X POST "$1/$endpoint" "${@:2}" \
|
||||
-H "Authorization: Bearer ${RAKAZO_GITHUB_BROKER_RUN_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary "$payload"
|
||||
@ -26,11 +26,19 @@ sys.exit(1)
|
||||
' "$RAKAZO_GITHUB_BROKER_URL"
|
||||
}
|
||||
unreachable() {
|
||||
echo "evOo could not reach its GitHub connection from this computer. Tell the user GitHub access is broken on the evOo server; do not ask for a token." >&2
|
||||
echo "evOo could not reach its GitHub connection from this computer (curl exit $status). Tell the user GitHub access is broken on the evOo server; do not ask for a token." >&2
|
||||
exit 1
|
||||
}
|
||||
status=0
|
||||
response="$(post "$RAKAZO_GITHUB_BROKER_URL" 2>/dev/null)" || status=$?
|
||||
socket=/run/rakazo-github-broker/broker.sock
|
||||
if [[ -S "$socket" ]]; then
|
||||
response="$(post http://broker/internal/github-broker --unix-socket "$socket" 2>/dev/null)" || status=$?
|
||||
fi
|
||||
# 7: nothing listening on the socket either; then try the network.
|
||||
if [[ ! -S "$socket" || "$status" == 7 ]]; then
|
||||
status=0
|
||||
response="$(post "$RAKAZO_GITHUB_BROKER_URL" 2>/dev/null)" || status=$?
|
||||
fi
|
||||
# 6: unresolved host, 7: connection refused, 28: timeout. Anything else is the broker answering.
|
||||
if [[ "$status" == 6 || "$status" == 7 || "$status" == 28 ]]; then
|
||||
fallback="$(gateway_url)" || unreachable
|
||||
|
||||
@ -107,6 +107,30 @@ describe("graphical computer spec", () => {
|
||||
expect(options.HostConfig.NetworkMode).toBe("rakazo_default");
|
||||
});
|
||||
|
||||
it("mounts the GitHub broker socket read-only when the supervisor serves one", () => {
|
||||
const base = {
|
||||
name: "rakazo-bot-abc",
|
||||
image: COMPUTER_IMAGE,
|
||||
botId: "abc",
|
||||
spaceId: "ws",
|
||||
githubBrokerSocketDir: "/srv/rakazo/github-broker",
|
||||
};
|
||||
expect(
|
||||
containerCreateOptions({ ...base, homePath: "/var/rakazo/homes/abc" }).HostConfig.Binds,
|
||||
).toContain("/srv/rakazo/github-broker:/run/rakazo-github-broker:ro");
|
||||
const projectMounts = containerCreateOptions({
|
||||
...base,
|
||||
homePath: "/var/rakazo/homes/abc",
|
||||
projectWorkspacePath: "/srv/projects/evoo",
|
||||
}).HostConfig.Mounts;
|
||||
expect(projectMounts).toContainEqual({
|
||||
Type: "bind",
|
||||
Source: "/srv/rakazo/github-broker",
|
||||
Target: "/run/rakazo-github-broker",
|
||||
ReadOnly: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("still publishes host ports when NetworkMode is a per-bot isolated network", () => {
|
||||
const networkMode = computerNetworkNameFor("bot_isolation");
|
||||
const options = containerCreateOptions({
|
||||
|
||||
@ -245,8 +245,13 @@ export interface ComputerCreateInput {
|
||||
controlToken?: string;
|
||||
networkMode?: string;
|
||||
publishControlPort?: boolean;
|
||||
/** Host directory holding the GitHub broker socket, mounted read-only into the computer. */
|
||||
githubBrokerSocketDir?: string;
|
||||
}
|
||||
|
||||
/** Where computers find the GitHub broker socket; the git shim prefers it over the network. */
|
||||
export const GITHUB_BROKER_SOCKET_MOUNT = "/run/rakazo-github-broker";
|
||||
|
||||
interface PointerInput {
|
||||
kind: "pointer";
|
||||
x: number;
|
||||
@ -281,6 +286,14 @@ function computerVolumeMounts(input: ComputerCreateInput): Docker.MountSettings[
|
||||
ReadOnly: false,
|
||||
});
|
||||
}
|
||||
if (input.githubBrokerSocketDir) {
|
||||
mounts.push({
|
||||
Type: "bind",
|
||||
Source: input.githubBrokerSocketDir,
|
||||
Target: GITHUB_BROKER_SOCKET_MOUNT,
|
||||
ReadOnly: true,
|
||||
});
|
||||
}
|
||||
return mounts;
|
||||
}
|
||||
|
||||
@ -321,6 +334,9 @@ export function containerCreateOptions(input: ComputerCreateInput) {
|
||||
...(input.projectWorkspacePath
|
||||
? [`${input.projectWorkspacePath}:/home/rakazo/project`]
|
||||
: []),
|
||||
...(input.githubBrokerSocketDir
|
||||
? [`${input.githubBrokerSocketDir}:${GITHUB_BROKER_SOCKET_MOUNT}:ro`]
|
||||
: []),
|
||||
],
|
||||
Mounts: undefined,
|
||||
}),
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { chmodSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { createAdaptorServer, serve } from "@hono/node-server";
|
||||
import {
|
||||
boundedSandboxCommandTimeoutMs,
|
||||
readBoundedJsonResponse,
|
||||
@ -152,6 +153,31 @@ export function needsBridgeBrokerListener(
|
||||
);
|
||||
}
|
||||
|
||||
// A socket bind-mounted into each computer reaches the broker without crossing the host firewall.
|
||||
const githubBrokerSocketDir = path.resolve(
|
||||
process.env.SANDBOX_GITHUB_BROKER_SOCKET_DIR ??
|
||||
path.join(os.homedir(), ".rakazo", "github-broker"),
|
||||
);
|
||||
// Set once the socket listens, so computers only get the mount when there is something behind it.
|
||||
let githubBrokerSocketReady = false;
|
||||
|
||||
function listenGithubBrokerSocket(logger: ReturnType<typeof createRootLogger>) {
|
||||
mkdirSync(githubBrokerSocketDir, { recursive: true, mode: 0o755 });
|
||||
const socketPath = path.join(githubBrokerSocketDir, "broker.sock");
|
||||
rmSync(socketPath, { force: true });
|
||||
const server = createAdaptorServer({ fetch: brokerApp.fetch });
|
||||
server.on("error", (error) => {
|
||||
logger.warn("github broker socket unavailable", { "error.message": error.message });
|
||||
});
|
||||
server.listen(socketPath, () => {
|
||||
// Computers may run as a different uid; the run token is what authorizes a request.
|
||||
chmodSync(socketPath, 0o666);
|
||||
githubBrokerSocketReady = true;
|
||||
logger.info("github broker listening", { "server.socket": socketPath });
|
||||
});
|
||||
return server;
|
||||
}
|
||||
|
||||
async function bridgeGatewayAddress(): Promise<string | undefined> {
|
||||
const network = await docker.getNetwork("bridge").inspect();
|
||||
return network.IPAM?.Config?.find((entry: { Gateway?: string }) => entry.Gateway)?.Gateway;
|
||||
@ -293,6 +319,7 @@ app.post("/computers", async (c) => {
|
||||
networkMode,
|
||||
controlToken: randomUUID(),
|
||||
publishControlPort: controlViaLoopback,
|
||||
...(githubBrokerSocketReady ? { githubBrokerSocketDir } : {}),
|
||||
}),
|
||||
);
|
||||
await container.start();
|
||||
@ -875,6 +902,9 @@ function startSupervisor() {
|
||||
logger.info("supervisor listening", { "http.host": hostname, "http.port": port });
|
||||
});
|
||||
let brokerServer: ReturnType<typeof serve> | undefined;
|
||||
const brokerSocketServer = needsBridgeBrokerListener(hostname)
|
||||
? listenGithubBrokerSocket(logger)
|
||||
: undefined;
|
||||
if (needsBridgeBrokerListener(hostname)) {
|
||||
void bridgeGatewayAddress()
|
||||
.then((gateway) => {
|
||||
@ -894,6 +924,7 @@ function startSupervisor() {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
if (brokerServer) await closeListeningServer(brokerServer);
|
||||
if (brokerSocketServer) await closeListeningServer(brokerSocketServer);
|
||||
await closeListeningServer(server);
|
||||
await logger.flush({ timeoutMs: 2_000 });
|
||||
process.exit(0);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user