-
Notifications
You must be signed in to change notification settings - Fork 0
feat: support remote compilation over SSH tunnels #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| CLAUDE.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Overview | ||
|
|
||
| `HOMCC` is a work-from-home friendly `distcc` replacement: it distributes `C`/`C++` compilation (`gcc`/`clang`) to remote servers while guaranteeing the same result as a local build. Its distinguishing feature over `distcc` is aggressive optimization for thin network uplinks: dependencies are compressed and **server-side cached by SHA1 hash**, so a warmed-up cache only re-transmits missing dependencies. | ||
|
|
||
| The project ships two executables plus an optional GUI: | ||
| - `homcc` — the client (entry point `homcc.client.main:main`) | ||
| - `homccd` — the server daemon (entry point `homcc.server.main:main`) | ||
| - `homcc-monitor` — a PySide2 GUI that watches client state files (entry point `homcc.monitor.main:main`) | ||
|
|
||
| ## Commands | ||
|
|
||
| All linting/testing runs against `*.py homcc tests` from the repo root. | ||
|
|
||
| ```sh | ||
| # Install dev dependencies (requires liblzo2-dev liblzma-dev apt packages) | ||
| python -m pip install -r requirements.txt | ||
|
|
||
| # Run all tests with coverage | ||
| pytest -v -rfEs --cov=homcc | ||
|
|
||
| # Run a single test file / test | ||
| pytest -v tests/client/client_test.py | ||
| pytest -v tests/client/client_test.py::TestClass::test_name | ||
|
|
||
| # Tests that exercise sandboxed compilation (opt-in, need a configured environment) | ||
| pytest -v -rfEs --cov=homcc --runschroot=jammy | ||
| pytest -v -rfEs --cov=homcc --rundocker=jammy | ||
|
|
||
| # Lint + static typing (both run in CI and must pass) | ||
| pylint -v --rcfile=.pylintrc *.py homcc tests | ||
| mypy --pretty *.py homcc tests | ||
|
|
||
| # Format (line length 120, black skips string normalization) | ||
| black --check --color --diff --verbose *.py homcc tests | ||
| isort --check --color --diff --gitignore --verbose *.py homcc tests | ||
|
|
||
| # Build Debian packages (needs stdeb toolchain; run as root) | ||
| sudo make homcc # -> target/homcc.deb | ||
| sudo make homccd # -> target/homccd.deb | ||
| ``` | ||
|
|
||
| CI (`.github/workflows/`) runs three jobs: linters (pylint + mypy), format check (black + isort), and tests (pytest). Match all three locally before pushing. | ||
|
|
||
| ## Architecture | ||
|
|
||
| The codebase is three packages sharing a common protocol layer: | ||
|
|
||
| - **`homcc/common/`** — shared by client and server. This is the contract between them; changes here usually require touching both sides. | ||
| - **`homcc/client/`** — invoked once per compilation (as `CCACHE_PREFIX=homcc`), selects a remote host, drives the protocol, falls back to local compilation. | ||
| - **`homcc/server/`** — long-lived `socketserver`-based daemon that receives requests, resolves dependencies against its cache, and compiles inside a sandbox. | ||
| - **`homcc/monitor/`** — read-only GUI; independent of the compile path. | ||
|
|
||
| ### The wire protocol (`common/messages.py`) | ||
|
|
||
| All client/server communication is a stream of length-prefixed `Message` subclasses (JSON header + optional binary payload), serialized via `Message.to_bytes()` / `Message.from_bytes()`. The compilation handshake: | ||
|
|
||
| 1. Client sends `ArgumentMessage` (compiler args, cwd, target, sandbox profile, compression, and a `{path: sha1sum}` map of all dependencies). | ||
| 2. Server checks each hash against its `Cache`; for cache misses it sends `DependencyRequestMessage`, and the client replies with `DependencyReplyMessage` (compressed file bytes) until all dependencies are present. | ||
| 3. Server compiles and returns a `CompilationResultMessage` (object files + stdout/stderr/return code), or a `ConnectionRefusedMessage` if it is at capacity. | ||
|
|
||
| When adding a message type, register it in `MessageType` and `Message._parse_message_json`. | ||
|
|
||
| ### Client compilation flow (`client/compilation.py`) | ||
|
|
||
| `compile_remotely` is the orchestrator. Key invariants: | ||
| - **Local fallback is the norm, not an error**: on most failures the client compiles locally so builds never break (unless `--no-local-compilation` is set). Preprocessing (`_preprocess`) and final linking (`execute_linking`) always happen locally; only the compile step is distributed. | ||
| - **Recursion guard**: because homcc invokes a real compiler that might itself be homcc-wrapped, `main.py` sets the `_HOMCC_SAFEGUARD` env var and children detect it (`RECURSIVE_ERROR_MESSAGE`) to abort. | ||
| - **Host selection** (`client/client.py`): `RemoteHostSelector` picks hosts randomly weighted by their `LIMIT`. Concurrency is bounded by **SysV semaphores** (`sysv_ipc`) — `RemoteHostSemaphore`, `LocalHostCompilationSemaphore`, `LocalHostPreprocessingSemaphore`. Debug open semaphores with `ipcs -s`. | ||
| - Hosts come from `$HOMCC_HOSTS` or a `hosts` file. Parsing lives in `common/host.py` (`_parse_host`); formats are `HOST[:PORT][/LIMIT][,COMPRESSION]` for TCP and `@HOST[:PORT]` / `USER@HOST[:PORT]` (`[IPv6]` bracketed) for SSH. | ||
|
|
||
| ### Transports (`client/client.py`, `client/ssh.py`) | ||
|
|
||
| The protocol is transport-agnostic: `RemoteCompilationClient` (abstract, in `client/client.py`) holds all message framing/send/receive logic and only defers `_open_connection` to subclasses. `TCPClient` opens a direct connection to `host:port`; `SSHClient` (`client/ssh.py`) connects to `127.0.0.1:<forwarded_port>`. `create_remote_client` in `compilation.py` dispatches on `host.type`. When adding a transport, subclass `RemoteCompilationClient` and set `connection_target`. | ||
|
|
||
| SSH is a **tunnel to a running `homccd`**, not a per-job remote process: `SSHTunnel` establishes a multiplexed OpenSSH master (`ControlMaster`/`ControlPersist`) that local-forwards a port to the daemon's loopback port, so all jobs (TCP and SSH alike) hit the same daemon and its global connection limit, and per-job latency is ~a local TCP connect. A per-host `flock` under `$HOMCC_DIR/ssh/` serializes master setup across the many concurrent `homcc` processes a build spawns. `SSHError` subclasses `ConnectionError` so tunnel failures fall through to the next host / local fallback like any lost connection. **No server changes** are needed for SSH. | ||
|
|
||
| ### `Arguments` (`common/arguments.py`) | ||
|
|
||
| The central abstraction for a compiler command line. It parses/normalizes args, distinguishes source files, dependencies, output paths, and compiler capabilities. Both client (to build the request) and server (to reconstruct the command in the sandbox) depend on it, so its behavior must stay symmetric across the wire. | ||
|
|
||
| ### Server sandboxing (`server/environment.py`, `docker.py`, `schroot.py`) | ||
|
|
||
| Each request runs in a fresh `Environment` under a temp dir in `/tmp/`, with client paths remapped into a server-local `mapped_cwd`. Compilation executes through a `ShellEnvironment` strategy: `HostShellEnvironment` (bare), `DockerShellEnvironment` (`--docker-container`), or `SchrootShellEnvironment` (`--schroot-profile`). The `Cache` (`server/cache.py`) is a hash-addressed, LRU-evicted dependency store bounded by `--max-cache-size`. | ||
|
|
||
| ### State files & monitoring (`common/statefile.py`) | ||
|
|
||
| Each in-flight client compilation writes a binary `StateFile` (phases: STARTUP → CONNECT → PREPROCESSING → COMPILE, adapted from distcc's format for tooling compatibility) into `HOMCC_STATE_DIR`. The monitor GUI uses `watchdog` to observe this directory. This is purely observational and decoupled from the compile path. | ||
|
|
||
| ## Configuration & packaging notes | ||
|
|
||
| - `setup.py` is **generated** at package-build time by `make` copying `setup_client.py` or `setup_server.py` over it — do not hand-edit `setup.py`; edit the `setup_client.py` / `setup_server.py` sources. Version is read from `homcc/{client,server}/__init__.py` `__version__`. | ||
| - Config resolution order and CLI/env/file precedence live in the `parsing.py` modules (`client/parsing.py`, `server/parsing.py`, `common/parsing.py`). Client and server each have a config dataclass (`ClientConfig`, `ServerConfig`). | ||
| - Compression (`common/compression.py`) supports `lzo` (python-lzo, needs `liblzo2-dev`) and `lzma`; default is none. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,11 @@ Additionally, `HOMCC` provides sandboxed compiler execution for remote compilati | |
| - `HOST` format: | ||
| - `HOST`: TCP connection to specified `HOST` with default port `3126` | ||
| - `HOST:PORT`: TCP connection to specified `HOST` with specified `PORT` | ||
| - `@HOST` / `USER@HOST` format: | ||
| - Connect to `HOST` through an SSH tunnel instead of a plain TCP connection, optionally authenticating as `USER` | ||
| - `homcc` establishes a multiplexed SSH master connection to `HOST` and local-forwards a port to the `homccd` running there; the daemon only needs to listen on the remote loopback interface (e.g. `--listen=127.0.0.1`). Subsequent compilations reuse the master connection (OpenSSH `ControlMaster`/`ControlPersist`), so the per-compilation cost stays close to a plain TCP connection | ||
| - The daemon's port on the remote defaults to `3126` and can be overridden with `@HOST:PORT` (use `@[IPv6]:PORT` for IPv6 addresses) | ||
| - Authentication relies on your existing SSH setup (keys/agent); `homcc` does not manage SSH credentials itself | ||
| - `HOST/LIMIT` format: | ||
| - Define any of the above `HOST` formats with an additional `LIMIT` parameter that specifies the maximum connection limit to the corresponding `HOST` | ||
| - It is advised to always specify your `LIMIT`s as they will otherwise default to 2 and only enable minor levels of concurrency | ||
|
|
@@ -93,13 +98,17 @@ Additionally, `HOMCC` provides sandboxed compiler execution for remote compilati | |
| remotehost/12 | ||
| 192.168.0.1:3126/21 | ||
| [FC00::1]:3126/42,lzo | ||
| @buildhost/24,lzo | ||
| user@buildhost:3126/24 | ||
| </pre></sub></td> | ||
| <td><sub><pre> | ||
| # Comment | ||
| "localhost" host with default limit of 2 | ||
| Named "remotehost" TCP host with limit of 12 at default port 3126 | ||
| IPv4 "192.168.0.1" TCP host at port 3126 with limit of 21 | ||
| IPv6 "FC00::1" TCP host at port 3126 with limit of 42 and lzo compression | ||
| "buildhost" via SSH tunnel with limit of 24 and lzo compression | ||
| "buildhost" via SSH tunnel as user "user", daemon port 3126, limit of 24 | ||
| </pre></sub></td> | ||
| </tr> | ||
| </table> | ||
|
|
@@ -158,6 +167,9 @@ Additionally, `HOMCC` provides sandboxed compiler execution for remote compilati | |
| HOMCC_LOG_LEVEL | ||
| HOMCC_VERBOSE | ||
| HOMCC_NO_LOCAL_COMPILATION | ||
| HOMCC_SSH_EXECUTABLE | ||
| HOMCC_SSH_CONTROL_PERSIST | ||
| HOMCC_SSH_OPTIONS | ||
|
|
||
| # homccd | ||
| HOMCCD_LIMIT | ||
|
|
@@ -179,6 +191,9 @@ Additionally, `HOMCC` provides sandboxed compiler execution for remote compilati | |
| log_level=DEBUG | ||
| verbose=True | ||
| no_local_compilation=True | ||
| ssh_executable=ssh | ||
| ssh_control_persist=600 | ||
| ssh_options=-o BatchMode=yes | ||
|
|
||
| [homccd] | ||
| limit=64 | ||
|
|
@@ -199,6 +214,9 @@ Additionally, `HOMCC` provides sandboxed compiler execution for remote compilati | |
| Detail level for log messages: {DEBUG, INFO, WARNING, ERROR, CRITICAL} | ||
| Enable verbosity mode which implies detailed and colored logging | ||
| Enforce that even on recoverable failures no local compilation is executed | ||
| Executable used to establish SSH tunnels for '@HOST'/'USER@HOST' hosts | ||
| Seconds an idle multiplexed SSH master connection is kept alive for reuse | ||
| Additional options passed to the SSH executable, e.g. '-o' flags | ||
|
|
||
| # Server configuration | ||
| Maximum limit of concurrent compilations | ||
|
|
@@ -213,11 +231,11 @@ Additionally, `HOMCC` provides sandboxed compiler execution for remote compilati | |
|
|
||
| ## Deployment hints | ||
| Things to keep in mind when deploying `homccd`: | ||
| - `homcc` currently does not support any transport encryption such as TLS, so source files would get transmitted over the internet in plain text if not using a VPN. | ||
| - `homcc` does not support built-in transport encryption such as TLS: plain TCP hosts transmit source files unencrypted, so a VPN is required over untrusted networks. Alternatively, use an SSH host (`@HOST`/`USER@HOST`) to tunnel the connection through an encrypted, authenticated SSH channel to a `homccd` bound to the remote loopback interface. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can remove this now?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Left it — it's not just about TLS, the other two bullets (no per-client connection limit, no restriction on docker/chroot selection) still hold regardless of transport. Reworded it instead so it doesn't imply VPN is the only mitigation now that SSH tunneling is an option too: 9102cd4 |
||
| - `homccd` does not limit simultaneous connections of a single client. A malicious client could therefore block the service by always opening up connections until no server slots are available any more. | ||
| - `homccd` does not limit access to docker containers or chroot environments. A client can choose any docker container or chroot environment available on the server to execute the compilation in. | ||
|
|
||
| :exclamation: The key takeaway of the previous points is to **not expose** `homccd` publicly. You should make sure only internal users (e.g. developers) have access to the service, for example through using a VPN. | ||
| :exclamation: The key takeaway of the previous points is to **not expose** `homccd` publicly. Make sure only internal users (e.g. developers) have access to the service, for example through a VPN or by only accepting SSH-tunneled connections. | ||
|
|
||
| ## Development | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.