Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: CI

on:
push:
branches:
- '**'
tags:
- 'v*'
pull_request:

permissions:
contents: read

jobs:
lint-and-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Bash syntax checks
run: |
bash -n \
cmd/boxctl \
lib/*.sh \
lib/firewall/*.sh \
lib/supervisor/*.sh \
tests/integration/test_phase2.sh \
tests/integration/test_real_kernel.sh \
tests/integration/test_arch_package_smoke.sh \
tests/fixtures/mockbin/ip \
tests/fixtures/mockbin/iptables \
packaging/scripts/systemd-lifecycle.sh \
packaging/arch/box4linux.install

- name: ShellCheck (if available)
run: |
if command -v shellcheck >/dev/null 2>&1; then
# SC1091: dynamic source paths are expected in this repo layout.
# SC2034: shared globals/constants are intentionally defined in common libs.
shellcheck \
-e SC1091,SC2034 \
cmd/boxctl \
lib/*.sh \
lib/firewall/*.sh \
lib/supervisor/*.sh \
tests/integration/test_phase2.sh \
tests/integration/test_real_kernel.sh \
tests/integration/test_arch_package_smoke.sh \
packaging/scripts/systemd-lifecycle.sh
shellcheck -e SC1091,SC2034 -s sh packaging/arch/box4linux.install
else
echo "shellcheck not available; skipping"
fi

- name: Mock integration tests
run: ./tests/integration/test_phase2.sh

- name: Real-kernel integration tests (skip-capable)
run: sudo ./tests/integration/test_real_kernel.sh

build-arch-package:
runs-on: ubuntu-latest
needs:
- lint-and-tests
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build Arch package in container
run: |
docker run --rm \
-v "$PWD":/work \
-w /work \
archlinux:base-devel \
bash -lc '
set -euo pipefail
# Avoid relying on distro default unprivileged accounts (for example `nobody`)
# because some base images can mark them as expired.
useradd -m -U builder
chown -R builder:builder /work
su builder -s /bin/bash -c "cd /work/packaging/arch && makepkg --nodeps --noconfirm -f"
'

- name: Capture package path
id: pkg
run: |
pkg_path="$(ls -1 packaging/arch/*.pkg.tar.* | head -n 1)"
echo "package_path=${pkg_path}" >> "${GITHUB_OUTPUT}"
echo "Built package: ${pkg_path}"

- name: Upload Arch package artifact
uses: actions/upload-artifact@v4
with:
name: box4linux-arch-pkg
path: ${{ steps.pkg.outputs.package_path }}

smoke-package:
runs-on: ubuntu-latest
needs:
- build-arch-package
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Download Arch package artifact
uses: actions/download-artifact@v4
with:
name: box4linux-arch-pkg
path: ./dist

- name: Package smoke test
run: |
pkg_path="$(ls -1 ./dist/*.pkg.tar.* | head -n 1)"
./tests/integration/test_arch_package_smoke.sh "${pkg_path}"

release:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
needs:
- smoke-package
permissions:
contents: write
steps:
- name: Download Arch package artifact
uses: actions/download-artifact@v4
with:
name: box4linux-arch-pkg
path: ./dist

- name: Publish release assets
uses: softprops/action-gh-release@v2
with:
files: ./dist/*.pkg.tar.*
generate_release_notes: true
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# OS / editor
.DS_Store
Thumbs.db
.idea/
.vscode/
*.swp
*.swo
*~

# Box references
box-reference/

# Local Linux dev runtime state
.box-dev/

# Arch packaging build artifacts
packaging/arch/*.pkg.tar.*
packaging/arch/pkg/
packaging/arch/src/
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# box4linux workspace

Linux-native control plane lives in:
- `cmd/boxctl`
- `lib/common.sh`
- `lib/config.sh`
- `lib/supervisor/`
- `lib/firewall/`
- `systemd/`
- `tests/integration/`
- `packaging/arch/`

Android reference artifacts are kept untouched in `box-reference/`.

## Quick run (dev)

1. Use repo-local fallback config at `etc/box/box.toml` (auto-used when `/etc/box/box.toml` is missing).
- If `BOX_CONFIG_FILE` is explicitly set, it must exist; commands fail fast instead of falling back.
2. Run status commands:
- `./cmd/boxctl service status`
- `./cmd/boxctl service status --json`
- `./cmd/boxctl firewall status`
- `./cmd/boxctl firewall status --json`
3. Run privileged actions as root:
- `sudo ./cmd/boxctl service start|stop|restart`
- `sudo ./cmd/boxctl firewall enable|disable|renew`
- `./cmd/boxctl firewall dry-run`
4. Run integration checks:
- `./tests/integration/test_phase2.sh`
- `sudo ./tests/integration/test_real_kernel.sh`

## Arch Package Build/Install

Build package from repo root:
- `cd packaging/arch && makepkg --noconfirm -f`

Install package:
- `sudo pacman -U ./box4linux-*.pkg.tar.zst`

Installed layout:
- `/usr/bin/boxctl`
- `/usr/lib/box4linux/cmd/boxctl`
- `/usr/lib/box4linux/lib/...`
- `/etc/box/box.toml`
- `/usr/lib/systemd/system/box.service`
- `/usr/lib/systemd/system/box-firewall.service`
- `/usr/share/doc/box4linux/`

Config upgrade behavior:
- Package marks `/etc/box/box.toml` as backup config.
- Local edits are preserved across upgrades.
- New template versions land as `.pacnew` when needed.

## Service Lifecycle (Packaged Install)

Use helper script from package docs:
- `sudo /usr/share/doc/box4linux/systemd-lifecycle.sh enable`
- `sudo /usr/share/doc/box4linux/systemd-lifecycle.sh status`
- `sudo /usr/share/doc/box4linux/systemd-lifecycle.sh disable`

Manual equivalent:
- `sudo systemctl daemon-reload`
- `sudo systemctl enable --now box.service box-firewall.service`
- `sudo systemctl disable --now box-firewall.service box.service`

## Packaged Operational Quickstart

1. Verify command and units:
- `boxctl service status --json`
- `boxctl firewall status --json`
2. Preview firewall operations:
- `boxctl firewall dry-run`
3. Start runtime:
- `sudo systemctl start box.service`
4. Renew firewall policy safely:
- `sudo systemctl reload box-firewall.service`

## CI/Release Flow

Workflow file: `.github/workflows/ci.yml`

On push/PR:
- `bash -n` checks on shell scripts
- `shellcheck` when available
- mock integration: `./tests/integration/test_phase2.sh`
- privileged integration: `sudo ./tests/integration/test_real_kernel.sh` (suite prints `SKIP` when capabilities/tooling are unavailable)
- Arch package build in Arch container
- package smoke test: `./tests/integration/test_arch_package_smoke.sh <pkg>`

On tags (`v*`):
- built package artifact is published to GitHub Releases

## Phase 3 Notes

- Supported cores: `mihomo`, `sing-box`
- Runtime overlays rendered under `/run/box/rendered` (or dev fallback)
- Firewall backends:
- `iptables` (mature path)
- `nftables` (MVP parity)
- Supported modes on both backends: `tun`, `tproxy`, `redirect`, `mixed`, `enhance`
- DNS strategies: `tproxy`, `redirect`, `disable`
- Coexistence modes:
- `preserve_tailnet` (default): apply tailscale and MagicDNS bypasses
- `strict_box`: skip tailscale/MagicDNS bypass insertion
- Route convergence: renew/reapply prunes stale BOX fwmark rules and enforces one current `route_pref` rule
- Idempotent + lock-protected: `enable|renew|disable`
- `BOX_TRACE_COMMANDS=1` logs external command executions with component/action context
- `boxctl firewall status --json` exposes stable diagnostics (backend, capabilities, coexist fields, errors)

## Backend Capability Notes

- `iptables`:
- `cap_ipv4=true`
- `cap_ipv6=false` (full ip6tables graph pending)
- `nftables`:
- `cap_ipv4=true`
- `cap_ipv6=false` (full IPv6 interception/hijack graph pending)

## Rollback/Uninstall

Safe uninstall (keeps config backups/data unless manually removed):
- `sudo pacman -R box4linux`

Optional manual purge of local state:
- `sudo rm -rf /etc/box /var/lib/box /run/box /var/log/box`

## Remaining TODO

- Full UID/GID/interface/MAC policy graph in firewall.
- Full IPv6 interception/hijack parity.
- API-based reload hooks for `mihomo` and `sing-box`.
Comment on lines +15 to +131
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix heading spacing to satisfy markdownlint MD022.

Line 14, Line 27, Line 33, and Line 50 headings should be followed by a blank line.

📝 Proposed markdown fix
 ## Quick run (dev)
+
 1. Use repo-local fallback config at `etc/box/box.toml` (auto-used when `/etc/box/box.toml` is missing).
 2. Run status commands:
@@
 ## Systemd units
+
 - `systemd/box.service`
 - `systemd/box-firewall.service`
@@
 ## Phase 2 notes
+
 - Supported cores: `mihomo`, `sing-box`
 - Core overlay mutators render runtime configs under `/run/box/rendered` (or dev fallback).
@@
 ## Remaining TODO
+
 - Full UID/GID/interface/MAC policy graph in firewall (currently placeholder stage).
 - `nftables` backend.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Quick run (dev)
1. Use repo-local fallback config at `etc/box/box.toml` (auto-used when `/etc/box/box.toml` is missing).
2. Run status commands:
- `./cmd/boxctl service status`
- `./cmd/boxctl service status --json`
- `./cmd/boxctl firewall status`
- `./cmd/boxctl firewall status --json`
3. Run privileged actions as root:
- `sudo ./cmd/boxctl service start|stop|restart`
- `sudo ./cmd/boxctl firewall enable|disable|renew`
4. Run integration checks:
- `./tests/integration/test_phase2.sh`
## Systemd units
- `systemd/box.service`
- `systemd/box-firewall.service`
Copy/symlink these to your systemd unit path and ensure `boxctl` is installed as `/usr/bin/boxctl`.
## Phase 2 notes
- Supported cores: `mihomo`, `sing-box`
- Core overlay mutators render runtime configs under `/run/box/rendered` (or dev fallback).
- Firewall backend (`iptables`) supports staged apply + rollback for `tun`, `tproxy`, `redirect`, `mixed`, `enhance`.
- DNS strategy handling: `tproxy`, `redirect`, `disable`.
- Tailscale coexistence defaults to `dns_coexist_mode=preserve_tailnet`.
- Coexistence mode semantics:
- `preserve_tailnet`: apply tailscale bypass (`tailscale0`, `100.64.0.0/10`) and MagicDNS resolver exclusion (`100.100.100.100:53`).
- `strict_box`: do not add tailscale bypass/MagicDNS exclusion rules; still never delete non-BOX routes/rules.
- Tailscale safeguards include:
- bypass `tailscale0`
- bypass `100.64.0.0/10` and preserve `fd7a:115c:a1e0::/48` by not touching ip6tables in this backend
- bypass `100.100.100.100:53` (MagicDNS resolver)
- preserve table `52` / fwmark `0x80000/0xff0000` ownership
- Route convergence: renew/reapply prunes stale BOX fwmark rules for the same fwmark+table (any old pref) and installs exactly one rule with current `route_pref`.
- `enable|renew|disable` paths are idempotent and lock-protected.
## Remaining TODO
- Full UID/GID/interface/MAC policy graph in firewall (currently placeholder stage).
- `nftables` backend.
- API-based reload hooks for `mihomo` and `sing-box`.
## Quick run (dev)
1. Use repo-local fallback config at `etc/box/box.toml` (auto-used when `/etc/box/box.toml` is missing).
2. Run status commands:
- `./cmd/boxctl service status`
- `./cmd/boxctl service status --json`
- `./cmd/boxctl firewall status`
- `./cmd/boxctl firewall status --json`
3. Run privileged actions as root:
- `sudo ./cmd/boxctl service start|stop|restart`
- `sudo ./cmd/boxctl firewall enable|disable|renew`
4. Run integration checks:
- `./tests/integration/test_phase2.sh`
## Systemd units
- `systemd/box.service`
- `systemd/box-firewall.service`
Copy/symlink these to your systemd unit path and ensure `boxctl` is installed as `/usr/bin/boxctl`.
## Phase 2 notes
- Supported cores: `mihomo`, `sing-box`
- Core overlay mutators render runtime configs under `/run/box/rendered` (or dev fallback).
- Firewall backend (`iptables`) supports staged apply + rollback for `tun`, `tproxy`, `redirect`, `mixed`, `enhance`.
- DNS strategy handling: `tproxy`, `redirect`, `disable`.
- Tailscale coexistence defaults to `dns_coexist_mode=preserve_tailnet`.
- Coexistence mode semantics:
- `preserve_tailnet`: apply tailscale bypass (`tailscale0`, `100.64.0.0/10`) and MagicDNS resolver exclusion (`100.100.100.100:53`).
- `strict_box`: do not add tailscale bypass/MagicDNS exclusion rules; still never delete non-BOX routes/rules.
- Tailscale safeguards include:
- bypass `tailscale0`
- bypass `100.64.0.0/10` and preserve `fd7a:115c:a1e0::/48` by not touching ip6tables in this backend
- bypass `100.100.100.100:53` (MagicDNS resolver)
- preserve table `52` / fwmark `0x80000/0xff0000` ownership
- Route convergence: renew/reapply prunes stale BOX fwmark rules for the same fwmark+table (any old pref) and installs exactly one rule with current `route_pref`.
- `enable|renew|disable` paths are idempotent and lock-protected.
## Remaining TODO
- Full UID/GID/interface/MAC policy graph in firewall (currently placeholder stage).
- `nftables` backend.
- API-based reload hooks for `mihomo` and `sing-box`.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 14-14: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 27-27: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 33-33: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 50-50: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 14 - 53, The Markdown headings "Quick run (dev)",
"Systemd units", "Phase 2 notes", and "Remaining TODO" each lack the required
blank line after the heading; update README.md by inserting a single blank line
immediately after those heading lines so that the content following each heading
is separated (ensure the lines containing those exact headings are followed by
an empty line).

- Broader kernel-capability probing across distro variants.
81 changes: 81 additions & 0 deletions cmd/boxctl
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash

# shellcheck shell=bash

set -euo pipefail

CMD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR_CANDIDATES=(
"${BOX_LIB_DIR:-}"
"${CMD_DIR}/../lib"
"/usr/lib/box4linux/lib"
)
LIB_DIR=""

find_lib_dir() {
local candidate
for candidate in "${LIB_DIR_CANDIDATES[@]}"; do
[[ -n "${candidate}" ]] || continue
if [[ -f "${candidate}/common.sh" ]]; then
LIB_DIR="${candidate}"
export BOX_LIB_DIR="${LIB_DIR}"
return 0
fi
done
return 1
}

if ! find_lib_dir; then
printf 'failed to locate box4linux lib directory; checked:\n' >&2
local_candidate_list="$(printf ' %s\n' "${LIB_DIR_CANDIDATES[@]}")"
printf '%s' "${local_candidate_list}" >&2
exit 1
fi

source "${LIB_DIR}/common.sh"
source "${LIB_DIR}/config.sh"
source "${LIB_DIR}/supervisor/supervisor.sh"
source "${LIB_DIR}/firewall/firewall.sh"

usage() {
cat <<'EOF'
Usage:
boxctl service <start|stop|restart|status> [--json]
boxctl firewall <enable|disable|renew|status|dry-run> [--json]
EOF
}

main() {
local component="${1:-}"
local action="${2:-}"
shift 2 || true

for arg in "$@"; do
if [[ "${arg}" == "--json" ]]; then
BOX_OUTPUT_FORMAT="json"
export BOX_OUTPUT_FORMAT
fi
done

enable_command_trace "${component:-boxctl}" "${action:-none}"
trap 'disable_command_trace' EXIT

case "${component}" in
service)
supervisor_cmd "${action}"
;;
firewall)
firewall_cmd "${action}"
;;
-h|--help|help|"")
usage
;;
*)
printf 'unknown component: %s\n' "${component}" >&2
usage >&2
return 2
;;
esac
}

main "$@"
21 changes: 21 additions & 0 deletions docs/linux-port/04-component-firewall-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ If unsupported, apply controlled downgrade with explicit logs.
- Do not hardcode interface names like `wlan0`.
- Make route table id configurable to avoid collisions.

## Tailscale Coexistence Requirements
For hosts that run Tailscale alongside Box, firewall apply/cleanup must preserve Tailscale routing and DNS behavior.

Hard requirements:

- Never flush/delete non-BOX chains or global policy rules.
- Never touch Tailscale policy-routing entries (commonly table `52`, fwmark rules like `0x80000/0xff0000`, or rule priorities around `5210..5270`).
- Add explicit bypass for Tailscale interface traffic:
- `-i tailscale0 -j RETURN` and `-o tailscale0 -j RETURN` in relevant chains.
- Add destination bypass CIDRs for tailnet traffic:
- IPv4 `100.64.0.0/10`
- IPv6 `fd7a:115c:a1e0::/48`
- Exclude Tailscale DNS endpoint from DNS hijack:
- `100.100.100.100:53`
- Keep Box rule/table/pref IDs configurable and in a dedicated namespace to avoid collisions with existing local policy routing (for example `2022`, `2024`, `52` already in use on some hosts).

DNS guidance:

- When transparent DNS interception is enabled, provide `dns_exclude_servers` and `dns_exclude_domains` settings.
- Default excludes should include Tailscale resolver and tailnet domains (`*.ts.net` and local MagicDNS suffix).

## Required Tests
- each mode on IPv4-only and dual-stack
- idempotent enable/renew/disable loops
Expand Down
Loading
Loading