diff --git a/README.md b/README.md index afb2dd5..c19ef26 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,28 @@ Minimal Firecracker launcher. - `--vcpu`: defaults to 2, max 16. - `--ram`: MiB, defaults to 1024, max 32768. - `--disk-size`: M/G suffixes supported; must be >= base `rootfs.ext4` size. Requires `resize2fs`. +- `DNS_SERVERS`: optional env var for resolv.conf (default: `1.1.1.1`). Requires `debugfs`. ## SSH ``` ssh -i "./id_ed25519" root@ ``` +## Internet Access +VMs do not get internet access by default. You must enable forwarding and NAT: +``` +./nat.sh up +``` +This enables `net.ipv4.ip_forward=1` and installs per-VM NAT rules for the VM's +guest IP and TAP device. To remove rules: +``` +./nat.sh down +``` +Check status with: +``` +./nat.sh status +``` + ## Shutdown ``` reboot diff --git a/nat.sh b/nat.sh new file mode 100755 index 0000000..8923eb8 --- /dev/null +++ b/nat.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +set -euo pipefail + +log() { + printf '[nat] %s\n' "$*" +} + +usage() { + cat <<'EOF' +Usage: ./nat.sh + +Manage per-VM NAT rules for internet access. +EOF +} + +get_prop() { + local info="$1" + local key="$2" + awk -F= -v k="$key" '$1==k {print $2}' "$info" +} + +find_vm_info() { + local query="$1" + local info match_count=0 match="" + + for info in state/vms/*/info; do + [[ -f "$info" ]] || continue + local id name + id="$(get_prop "$info" "id")" + name="$(get_prop "$info" "name")" + if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then + match="$info" + match_count=$((match_count + 1)) + fi + done + + if (( match_count == 0 )); then + log "no VM found for prefix: $query" + exit 1 + fi + if (( match_count > 1 )); then + log "multiple VMs found for prefix: $query" + exit 1 + fi + + printf '%s' "$match" +} + +default_uplink() { + ip route show default 2>/dev/null | awk '/default/ {print $5; exit}' +} + +ensure_iptables() { + if ! command -v iptables >/dev/null 2>&1; then + log "iptables not found" + exit 1 + fi +} + +ACTION="${1:-}" +QUERY="${2:-}" +if [[ -z "$ACTION" || -z "$QUERY" ]]; then + usage + exit 1 +fi + +INFO_FILE="$(find_vm_info "$QUERY")" +GUEST_IP="$(get_prop "$INFO_FILE" "guest_ip")" +TAP="$(get_prop "$INFO_FILE" "tap")" + +if [[ -z "$GUEST_IP" || -z "$TAP" ]]; then + log "missing guest_ip or tap in $INFO_FILE" + exit 1 +fi + +UPLINK="$(default_uplink)" +if [[ -z "$UPLINK" ]]; then + log "failed to detect uplink interface" + exit 1 +fi + +ensure_iptables + +nat_rule=(-t nat -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE) +fwd_out_rule=(-i "$TAP" -o "$UPLINK" -j ACCEPT) +fwd_in_rule=(-i "$UPLINK" -o "$TAP" -m state --state "RELATED,ESTABLISHED" -j ACCEPT) + +case "$ACTION" in + up) + sudo sysctl -w net.ipv4.ip_forward=1 >/dev/null + sudo iptables -t nat -C POSTROUTING "${nat_rule[@]}" 2>/dev/null || \ + sudo iptables -t nat -A POSTROUTING "${nat_rule[@]}" + sudo iptables -C FORWARD "${fwd_out_rule[@]}" 2>/dev/null || \ + sudo iptables -A FORWARD "${fwd_out_rule[@]}" + sudo iptables -C FORWARD "${fwd_in_rule[@]}" 2>/dev/null || \ + sudo iptables -A FORWARD "${fwd_in_rule[@]}" + log "NAT enabled for $GUEST_IP via $UPLINK" + ;; + down) + sudo iptables -t nat -C POSTROUTING "${nat_rule[@]}" 2>/dev/null && \ + sudo iptables -t nat -D POSTROUTING "${nat_rule[@]}" || true + sudo iptables -C FORWARD "${fwd_out_rule[@]}" 2>/dev/null && \ + sudo iptables -D FORWARD "${fwd_out_rule[@]}" || true + sudo iptables -C FORWARD "${fwd_in_rule[@]}" 2>/dev/null && \ + sudo iptables -D FORWARD "${fwd_in_rule[@]}" || true + log "NAT disabled for $GUEST_IP" + ;; + status) + sysctl net.ipv4.ip_forward | sed 's/^/[nat] /' + if sudo iptables -t nat -C POSTROUTING "${nat_rule[@]}" 2>/dev/null; then + log "nat: installed" + else + log "nat: missing" + fi + if sudo iptables -C FORWARD "${fwd_out_rule[@]}" 2>/dev/null; then + log "forward out: installed" + else + log "forward out: missing" + fi + if sudo iptables -C FORWARD "${fwd_in_rule[@]}" 2>/dev/null; then + log "forward in: installed" + else + log "forward in: missing" + fi + ;; + *) + usage + exit 1 + ;; +esac diff --git a/run.sh b/run.sh index 9730e89..f4b3799 100755 --- a/run.sh +++ b/run.sh @@ -42,6 +42,7 @@ MAX_VCPU=16 MIN_RAM=256 MAX_RAM=32768 MAX_DISK_BYTES=$((128 * 1024 * 1024 * 1024)) +DNS_SERVERS="${DNS_SERVERS:-1.1.1.1}" VCPU_COUNT="$DEFAULT_VCPU" RAM_MIB="$DEFAULT_RAM" @@ -192,7 +193,7 @@ sudo -v VM_STARTED=0 CLEANUP_ON_EXIT=0 KEEP_VM_DIR_ON_FAIL=1 -DISK_PATH="$ROOTFS" +DISK_PATH="$VM_DIR/rootfs.ext4" cleanup() { local exit_code=$? @@ -241,13 +242,13 @@ else log "setcap not available; firecracker may need root to open TAP" fi +cp --reflink=auto "$ROOTFS" "$DISK_PATH" + if [[ -n "$DISK_BYTES" ]]; then if ! command -v resize2fs >/dev/null 2>&1; then log "resize2fs required for --disk-size" exit 1 fi - DISK_PATH="$VM_DIR/rootfs.ext4" - cp --reflink=auto "$ROOTFS" "$DISK_PATH" BASE_BYTES="$(stat -c%s "$ROOTFS")" if (( DISK_BYTES < BASE_BYTES )); then log "disk-size must be >= base image size" @@ -260,6 +261,20 @@ if [[ -n "$DISK_BYTES" ]]; then fi fi +if ! command -v debugfs >/dev/null 2>&1; then + log "debugfs required to set resolv.conf" + exit 1 +fi +RESOLV_TMP="$VM_DIR/resolv.conf" +printf '' >"$RESOLV_TMP" +for ns in ${DNS_SERVERS//,/ }; do + printf 'nameserver %s\n' "$ns" >>"$RESOLV_TMP" +done +debugfs -w -R "write $RESOLV_TMP /etc/resolv.conf" "$DISK_PATH" >/dev/null 2>&1 || { + log "failed to write /etc/resolv.conf into rootfs" + exit 1 +} + # Host bridge if ! ip link show "$BR_DEV" >/dev/null 2>&1; then log "creating host bridge $BR_DEV ($BR_IP/$CIDR)" @@ -325,7 +340,7 @@ log "configuring machine" # Boot source log "configuring boot source" -KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0::eth0:off" +KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0::eth0:off hostname=${VM_NAME}" "${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \ -H "Content-Type: application/json" \