initial commit: SPEC.md, implementation spec
This commit is contained in:
@@ -0,0 +1,252 @@
|
|||||||
|
# ecr - implementation specification
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
```
|
||||||
|
ecr <distro>[:<version>] [-a <arch>] [options] [-- command [args...]]
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Interface
|
||||||
|
|
||||||
|
### Positional Arguments
|
||||||
|
|
||||||
|
- `<distro>` (required): Distribution name
|
||||||
|
- `<version>` (optional): Distribution version/codename
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `-a, --arch <arch>` | host arch | Target architecture |
|
||||||
|
| `--bind [path]` | cwd | Directory to overlay-mount |
|
||||||
|
| `--bind-rw [path]` | none | Read-write bind mount at `/root/<basename>` (bypasses overlay) |
|
||||||
|
| `--no-cache` | false | Download fresh tarball, ignore cache |
|
||||||
|
| `-h, --help` | - | Show help |
|
||||||
|
| `-V, --version` | - | Show version |
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
### Cache Directory
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.cache/ecr/
|
||||||
|
├── ubuntu-noble-amd64.tar.gz
|
||||||
|
├── debian-bookworm-arm64.tar.gz
|
||||||
|
├── arch-latest-riscv64.tar.gz
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
No metadata files. Tarballs are downloaded once and never redownloaded. Users can delete files manually or use `--no-cache` to fetch fresh.
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
|
||||||
|
`~/.config/ecr.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
cache_dir: ~/.cache/ecr
|
||||||
|
```
|
||||||
|
|
||||||
|
## Distro Sources
|
||||||
|
|
||||||
|
| Distro | Version Format | URL Pattern |
|
||||||
|
|--------|----------------|-------------|
|
||||||
|
| Ubuntu | noble, jammy, mantic or 26.04, 25.10, 22.04, latest, lts | https://cdimage.ubuntu.com/ubuntu-base/bionic/daily/current/bionic-base-amd64.tar.gz |
|
||||||
|
| Debian | bookworm, bullseye, sid, latest | https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.tar.xz |
|
||||||
|
| Arch | latest | https://geo.mirror.pkgbuild.com/iso/latest/archlinux-bootstrap-x86_64.tar.zst |
|
||||||
|
| Alpine | 3.20, 3.19, latest, edge | https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.23.0-x86_64.tar.gz |
|
||||||
|
| Fedora | 40, 41, rawhide, latest | Extract from `registry.fedoraproject.org/fedora:<version>` |
|
||||||
|
|
||||||
|
### Architecture Mapping
|
||||||
|
|
||||||
|
| ecr | Ubuntu | Debian | Arch | Alpine | Fedora |
|
||||||
|
|-----|--------|--------|------|--------|--------|
|
||||||
|
| amd64 | amd64 | amd64 | x86_64 | x86_64 | x86_64 |
|
||||||
|
| arm64 | arm64 | arm64 | aarch64 | aarch64 | aarch64 |
|
||||||
|
| armhf | armhf | armhf | - | armv7 | - |
|
||||||
|
| riscv64 | riscv64 | riscv64 | riscv64 | riscv64 | riscv64 |
|
||||||
|
| ppc64el | ppc64el | ppc64el | - | ppc64le | ppc64le |
|
||||||
|
| s390x | s390x | s390x | - | s390x | s390x |
|
||||||
|
|
||||||
|
### Fedora Container Extraction
|
||||||
|
|
||||||
|
For Fedora, download the container image layer and extract:
|
||||||
|
|
||||||
|
1. Query manifest: `GET https://registry.fedoraproject.org/v2/fedora/manifests/<version>`
|
||||||
|
2. Parse manifest, get layer digest
|
||||||
|
3. Download layer blob
|
||||||
|
4. Extract tarball
|
||||||
|
|
||||||
|
## Execution Flow
|
||||||
|
|
||||||
|
1. Parse CLI arguments and config file
|
||||||
|
2. Resolve distro/version/arch to tarball URL
|
||||||
|
3. Check cache for existing tarball
|
||||||
|
4. If not cached, download tarball
|
||||||
|
5. Create temp directory for extraction
|
||||||
|
6. Extract tarball to temp directory
|
||||||
|
7. Create namespaces: user, pid, mount, uts
|
||||||
|
8. Set up mounts: /proc, /sys (ro), /dev, /dev/pts
|
||||||
|
9. Write /etc/resolv.conf with DNS servers
|
||||||
|
10. Set up overlay mount for workspace directory
|
||||||
|
11. Set environment variables
|
||||||
|
12. Exec shell or command in chroot
|
||||||
|
13. On exit, clean up temp directory
|
||||||
|
|
||||||
|
## Namespace Setup
|
||||||
|
|
||||||
|
### Namespaces (Always Created)
|
||||||
|
|
||||||
|
- **user**: Map current user to root (UID 0) inside
|
||||||
|
- **pid**: Isolated process tree
|
||||||
|
- **mount**: Private mounts for chroot setup
|
||||||
|
- **uts**: Hostname set to `ecr-<distro>-<random>`
|
||||||
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
Host network namespace (no isolation).
|
||||||
|
|
||||||
|
### User Namespace Mapping
|
||||||
|
|
||||||
|
```
|
||||||
|
uid_map: 0 <current_uid> 1
|
||||||
|
gid_map: 0 <current_gid> 1
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes the user appear as root inside the chroot while remaining unprivileged on the host.
|
||||||
|
|
||||||
|
### Mounts Inside Chroot
|
||||||
|
|
||||||
|
| Path | Type | Options |
|
||||||
|
|------|------|---------|
|
||||||
|
| /proc | proc | defaults |
|
||||||
|
| /sys | sysfs | ro,nosuid,nodev,noexec |
|
||||||
|
| /dev | devtmpfs | nosuid |
|
||||||
|
| /dev/pts | devpts | nosuid,noexec |
|
||||||
|
| /root/<basename> | overlay | lowerdir=<bind_path>, upperdir=<temp>, workdir=<temp> |
|
||||||
|
| /etc/resolv.conf | file | written with DNS |
|
||||||
|
|
||||||
|
## QEMU Integration
|
||||||
|
|
||||||
|
### Foreign Architecture Detection
|
||||||
|
|
||||||
|
If `--arch` differs from host architecture, QEMU is required.
|
||||||
|
|
||||||
|
### binfmt_misc Check
|
||||||
|
|
||||||
|
Before entering chroot, verify binfmt_misc is registered for target architecture by checking `/proc/sys/fs/binfmt_misc/qemu-<arch>`.
|
||||||
|
|
||||||
|
If not registered, error with message:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: binfmt_misc not registered for riscv64
|
||||||
|
|
||||||
|
Install QEMU user emulation:
|
||||||
|
Ubuntu/Debian: sudo apt install qemu-user-static
|
||||||
|
Arch: sudo pacman -S qemu-user-static-binfmt
|
||||||
|
Alpine: sudo apk add qemu-user-static
|
||||||
|
```
|
||||||
|
|
||||||
|
### QEMU Binary
|
||||||
|
|
||||||
|
No action required. Modern qemu-user-static packages register binfmt_misc with the `F` (fix binary) flag, loading the interpreter into kernel memory. The kernel handles foreign binary execution transparently.
|
||||||
|
|
||||||
|
## File Handling
|
||||||
|
|
||||||
|
### Overlay Mount (Default)
|
||||||
|
|
||||||
|
By default, the current working directory is mounted as an overlay filesystem at `/root/<basename>` inside the chroot, where `<basename>` is the name of the current directory.
|
||||||
|
|
||||||
|
Overlay configuration:
|
||||||
|
- `lowerdir`: the source directory (read-only)
|
||||||
|
- `upperdir`: temp directory for modifications
|
||||||
|
- `workdir`: temp directory required by overlayfs
|
||||||
|
|
||||||
|
Changes made inside the chroot are written to upperdir and discarded on exit. The host directory is never modified.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
$ cd ~/projects/myapp
|
||||||
|
$ ecr ubuntu:noble -- make build
|
||||||
|
# ~/projects/myapp mounted at /root/myapp
|
||||||
|
# Build artifacts written to overlay, discarded on exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read-Write Bind Mount
|
||||||
|
|
||||||
|
`--bind-rw <path>` bypasses overlay and creates a true read-write bind mount at `/mnt/<basename>`. This modifies the host filesystem directly. Use with caution.
|
||||||
|
|
||||||
|
### No Mount
|
||||||
|
|
||||||
|
`--no-bind` skips mounting any directory.
|
||||||
|
|
||||||
|
## DNS
|
||||||
|
|
||||||
|
Default DNS server is 1.1.1.1. Configured via `/etc/resolv.conf` in chroot:
|
||||||
|
|
||||||
|
```
|
||||||
|
nameserver 1.1.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Override with `--dns` flag or config file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Default environment inside chroot:
|
||||||
|
|
||||||
|
- HOME=/root
|
||||||
|
- USER=root
|
||||||
|
- SHELL=/bin/bash (or /bin/sh if bash unavailable)
|
||||||
|
- TERM=<from host>
|
||||||
|
- PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
|
Host environment is not inherited.
|
||||||
|
|
||||||
|
## Signal Handling
|
||||||
|
|
||||||
|
Forward SIGINT, SIGTERM, SIGHUP, SIGQUIT to child process. Wait for child to exit before cleanup.
|
||||||
|
|
||||||
|
## Security Requirements
|
||||||
|
|
||||||
|
### User Namespace Required
|
||||||
|
|
||||||
|
`ecr` requires unprivileged user namespaces. If unavailable (sysctl `kernel.unprivileged_userns_clone=0` or AppArmor restrictions), error with:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: User namespaces not available
|
||||||
|
|
||||||
|
Enable with:
|
||||||
|
sysctl -w kernel.unprivileged_userns_clone=1
|
||||||
|
|
||||||
|
Or check AppArmor profile restrictions.
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Root Fallback
|
||||||
|
|
||||||
|
There is no fallback to running as real root. The tool is designed for unprivileged use.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Language
|
||||||
|
|
||||||
|
Rust.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- `nix`: Unix syscall bindings (clone, unshare, mount, chroot, namespaces)
|
||||||
|
- `serde` + `serde_yaml`: config parsing
|
||||||
|
- `reqwest`: HTTP downloads
|
||||||
|
- `tar`: tarball extraction
|
||||||
|
- `xz2`: xz decompression
|
||||||
|
- `zstd`: zstd decompression
|
||||||
|
- `flate2`: gzip decompression
|
||||||
|
- `clap`: CLI parsing
|
||||||
|
- `signal-hook`: signal handling
|
||||||
|
- `tempfile`: temp directories
|
||||||
Reference in New Issue
Block a user