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"
|
endpoint="$1"
|
||||||
payload="$2"
|
payload="$2"
|
||||||
post() {
|
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 "Authorization: Bearer ${RAKAZO_GITHUB_BROKER_RUN_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
--data-binary "$payload"
|
--data-binary "$payload"
|
||||||
@ -26,11 +26,19 @@ sys.exit(1)
|
|||||||
' "$RAKAZO_GITHUB_BROKER_URL"
|
' "$RAKAZO_GITHUB_BROKER_URL"
|
||||||
}
|
}
|
||||||
unreachable() {
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
status=0
|
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.
|
# 6: unresolved host, 7: connection refused, 28: timeout. Anything else is the broker answering.
|
||||||
if [[ "$status" == 6 || "$status" == 7 || "$status" == 28 ]]; then
|
if [[ "$status" == 6 || "$status" == 7 || "$status" == 28 ]]; then
|
||||||
fallback="$(gateway_url)" || unreachable
|
fallback="$(gateway_url)" || unreachable
|
||||||
|
|||||||
@ -107,6 +107,30 @@ describe("graphical computer spec", () => {
|
|||||||
expect(options.HostConfig.NetworkMode).toBe("rakazo_default");
|
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", () => {
|
it("still publishes host ports when NetworkMode is a per-bot isolated network", () => {
|
||||||
const networkMode = computerNetworkNameFor("bot_isolation");
|
const networkMode = computerNetworkNameFor("bot_isolation");
|
||||||
const options = containerCreateOptions({
|
const options = containerCreateOptions({
|
||||||
|
|||||||
@ -245,8 +245,13 @@ export interface ComputerCreateInput {
|
|||||||
controlToken?: string;
|
controlToken?: string;
|
||||||
networkMode?: string;
|
networkMode?: string;
|
||||||
publishControlPort?: boolean;
|
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 {
|
interface PointerInput {
|
||||||
kind: "pointer";
|
kind: "pointer";
|
||||||
x: number;
|
x: number;
|
||||||
@ -281,6 +286,14 @@ function computerVolumeMounts(input: ComputerCreateInput): Docker.MountSettings[
|
|||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (input.githubBrokerSocketDir) {
|
||||||
|
mounts.push({
|
||||||
|
Type: "bind",
|
||||||
|
Source: input.githubBrokerSocketDir,
|
||||||
|
Target: GITHUB_BROKER_SOCKET_MOUNT,
|
||||||
|
ReadOnly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
return mounts;
|
return mounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,6 +334,9 @@ export function containerCreateOptions(input: ComputerCreateInput) {
|
|||||||
...(input.projectWorkspacePath
|
...(input.projectWorkspacePath
|
||||||
? [`${input.projectWorkspacePath}:/home/rakazo/project`]
|
? [`${input.projectWorkspacePath}:/home/rakazo/project`]
|
||||||
: []),
|
: []),
|
||||||
|
...(input.githubBrokerSocketDir
|
||||||
|
? [`${input.githubBrokerSocketDir}:${GITHUB_BROKER_SOCKET_MOUNT}:ro`]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
Mounts: undefined,
|
Mounts: undefined,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
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 { mkdir } from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { serve } from "@hono/node-server";
|
import { createAdaptorServer, serve } from "@hono/node-server";
|
||||||
import {
|
import {
|
||||||
boundedSandboxCommandTimeoutMs,
|
boundedSandboxCommandTimeoutMs,
|
||||||
readBoundedJsonResponse,
|
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> {
|
async function bridgeGatewayAddress(): Promise<string | undefined> {
|
||||||
const network = await docker.getNetwork("bridge").inspect();
|
const network = await docker.getNetwork("bridge").inspect();
|
||||||
return network.IPAM?.Config?.find((entry: { Gateway?: string }) => entry.Gateway)?.Gateway;
|
return network.IPAM?.Config?.find((entry: { Gateway?: string }) => entry.Gateway)?.Gateway;
|
||||||
@ -293,6 +319,7 @@ app.post("/computers", async (c) => {
|
|||||||
networkMode,
|
networkMode,
|
||||||
controlToken: randomUUID(),
|
controlToken: randomUUID(),
|
||||||
publishControlPort: controlViaLoopback,
|
publishControlPort: controlViaLoopback,
|
||||||
|
...(githubBrokerSocketReady ? { githubBrokerSocketDir } : {}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await container.start();
|
await container.start();
|
||||||
@ -875,6 +902,9 @@ function startSupervisor() {
|
|||||||
logger.info("supervisor listening", { "http.host": hostname, "http.port": port });
|
logger.info("supervisor listening", { "http.host": hostname, "http.port": port });
|
||||||
});
|
});
|
||||||
let brokerServer: ReturnType<typeof serve> | undefined;
|
let brokerServer: ReturnType<typeof serve> | undefined;
|
||||||
|
const brokerSocketServer = needsBridgeBrokerListener(hostname)
|
||||||
|
? listenGithubBrokerSocket(logger)
|
||||||
|
: undefined;
|
||||||
if (needsBridgeBrokerListener(hostname)) {
|
if (needsBridgeBrokerListener(hostname)) {
|
||||||
void bridgeGatewayAddress()
|
void bridgeGatewayAddress()
|
||||||
.then((gateway) => {
|
.then((gateway) => {
|
||||||
@ -894,6 +924,7 @@ function startSupervisor() {
|
|||||||
if (stopping) return;
|
if (stopping) return;
|
||||||
stopping = true;
|
stopping = true;
|
||||||
if (brokerServer) await closeListeningServer(brokerServer);
|
if (brokerServer) await closeListeningServer(brokerServer);
|
||||||
|
if (brokerSocketServer) await closeListeningServer(brokerSocketServer);
|
||||||
await closeListeningServer(server);
|
await closeListeningServer(server);
|
||||||
await logger.flush({ timeoutMs: 2_000 });
|
await logger.flush({ timeoutMs: 2_000 });
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user