mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 16:43:12 +00:00
Merge branch 'master' into next
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
name: FreeBSD Package
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- maint
|
||||
- next
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
determine-versioning:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
freebsd_package_version: ${{ steps.freebsd_version.outputs.freebsd_package_version }}
|
||||
freebsd_pkg_file_version: ${{ steps.freebsd_version.outputs.freebsd_pkg_file_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Derive FreeBSD package version
|
||||
id: freebsd_version
|
||||
shell: bash
|
||||
run: |
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
|
||||
BASE_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
else
|
||||
BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|[^A-Za-z0-9]|.|g; s/\.\{2,\}/./g; s/^\.//; s/\.$//')
|
||||
HEIGHT=$(git rev-list --count HEAD)
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
if [[ -z "$BRANCH" ]]; then
|
||||
BRANCH="ref"
|
||||
fi
|
||||
VERSION="${BASE_VERSION}+${BRANCH}.${HEIGHT}.${HASH}"
|
||||
fi
|
||||
|
||||
# build-pkg.sh maps '-' and '+' to '.' (neither is allowed in a
|
||||
# pkg version); derive the same mapping here so later steps can
|
||||
# assert the exact artifact filename.
|
||||
PKG_FILE_VERSION=$(printf '%s' "$VERSION" | tr -- '+-' '..')
|
||||
|
||||
echo "freebsd_package_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "freebsd_pkg_file_version=${PKG_FILE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build FreeBSD package (x86_64)
|
||||
# No GitHub-hosted FreeBSD runners exist; build inside a KVM-accelerated
|
||||
# FreeBSD VM on the Linux runner. The release must track the .pkg ABI
|
||||
# major (FreeBSD:15:amd64) — pkg on other majors refuses the package.
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine-versioning
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set SOURCE_DATE_EPOCH from git
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and smoke-install in FreeBSD VM
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
env:
|
||||
FREEBSD_PACKAGE_VERSION: ${{ needs.determine-versioning.outputs.freebsd_package_version }}
|
||||
with:
|
||||
release: "15.1"
|
||||
usesh: true
|
||||
sync: rsync
|
||||
copyback: true
|
||||
mem: 6144
|
||||
envs: "SOURCE_DATE_EPOCH CARGO_TERM_COLOR FREEBSD_PACKAGE_VERSION"
|
||||
prepare: |
|
||||
pkg install -y curl
|
||||
run: |
|
||||
set -e
|
||||
|
||||
# rustup rather than the ports rust: rust-toolchain.toml pins
|
||||
# the toolchain, and rustup honors the pin on first cargo use.
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --default-toolchain none --profile minimal
|
||||
. "$HOME/.cargo/env"
|
||||
|
||||
cargo build --release
|
||||
|
||||
# The only place the FreeBSD cfg arms' unit tests ever run in
|
||||
# CI — the main CI matrix is Linux-only, and a release build
|
||||
# compiles no #[cfg(test)] code (AF-prefix strip round-trips,
|
||||
# platform module, config path gates).
|
||||
cargo test
|
||||
|
||||
packaging/freebsd/build-pkg.sh \
|
||||
--version "$FREEBSD_PACKAGE_VERSION" \
|
||||
--no-build
|
||||
|
||||
# Smoke-install the package in the VM: files land where the
|
||||
# rc.d scripts and DNS integration expect them, and the
|
||||
# binaries link against this release's base libraries.
|
||||
PKG=$(ls deploy/fips-*-freebsd-*.pkg)
|
||||
pkg add "$PKG"
|
||||
for bin in fips fipsctl fipstop; do
|
||||
test -x "/usr/local/bin/$bin" || { echo "FAIL: missing /usr/local/bin/$bin"; exit 1; }
|
||||
if ldd "/usr/local/bin/$bin" | grep "not found"; then
|
||||
echo "FAIL: unresolved shared libraries in $bin"; exit 1
|
||||
fi
|
||||
done
|
||||
test -x /usr/local/etc/rc.d/fips
|
||||
test -x /usr/local/etc/rc.d/fips_dns
|
||||
test -f /usr/local/etc/fips/fips.yaml.sample
|
||||
test -f /usr/local/etc/fips/hosts.sample
|
||||
# The manifest post-install script must have copied the
|
||||
# samples into place (install-if-absent semantics).
|
||||
test -f /usr/local/etc/fips/fips.yaml
|
||||
test -f /usr/local/etc/fips/hosts
|
||||
# fips.yaml may hold a node private key (nsec:); it must not
|
||||
# be world-readable — Debian and macOS both install it 0600.
|
||||
for f in /usr/local/etc/fips/fips.yaml /usr/local/etc/fips/fips.yaml.sample; do
|
||||
mode=$(stat -f %Lp "$f")
|
||||
if [ "$mode" != "600" ]; then
|
||||
echo "FAIL: $f mode is $mode, expected 600"; exit 1
|
||||
fi
|
||||
done
|
||||
# post-install must create the control-socket access group.
|
||||
pw groupshow fips >/dev/null || { echo "FAIL: fips group missing"; exit 1; }
|
||||
test -x /usr/local/libexec/fips/fips-dns-setup
|
||||
pkg info fips
|
||||
echo "==> pkg smoke-install PASSED"
|
||||
|
||||
# SHA-256 sidecar computed inside the VM; the host verifies the
|
||||
# bytes again after the rsync copyback, so corruption across
|
||||
# the VM handoff is detected before upload.
|
||||
( cd deploy && sha256 -q "$(basename "$PKG")" \
|
||||
| { read -r h; printf '%s %s\n' "$h" "$(basename "$PKG")"; } \
|
||||
> "$(basename "$PKG").sha256" )
|
||||
|
||||
# The whole workspace is rsynced back to the host; drop the
|
||||
# build tree so the copyback moves megabytes, not gigabytes.
|
||||
rm -rf target
|
||||
|
||||
- name: Resolve FreeBSD asset path
|
||||
id: freebsd-assets
|
||||
shell: bash
|
||||
run: |
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
set -euo pipefail
|
||||
|
||||
# build-pkg.sh names the package from the derived version and the
|
||||
# pkg ABI arch; assert the exact name so a naming regression fails
|
||||
# here instead of colliding on the release page.
|
||||
EXPECTED="deploy/fips-${{ needs.determine-versioning.outputs.freebsd_pkg_file_version }}-freebsd-amd64.pkg"
|
||||
if [[ ! -f "$EXPECTED" ]]; then
|
||||
echo "Expected package $EXPECTED was not produced" >&2
|
||||
echo "deploy/ contains:" >&2
|
||||
ls -la deploy >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "pkg=$EXPECTED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify .pkg integrity across the VM handoff
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG="${{ steps.freebsd-assets.outputs.pkg }}"
|
||||
sidecar="${PKG}.sha256"
|
||||
if [[ ! -f "$sidecar" ]]; then
|
||||
echo "FAIL: missing SHA-256 sidecar for $(basename "$PKG")" >&2
|
||||
exit 1
|
||||
fi
|
||||
expected=$(awk '{print $1}' "$sidecar")
|
||||
actual=$(sha256sum "$PKG" | awk '{print $1}')
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
echo "FAIL: $(basename "$PKG") SHA-256 mismatch across the VM copyback" >&2
|
||||
echo " expected (FreeBSD VM): $expected" >&2
|
||||
echo " actual (host): $actual" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: $(basename "$PKG") matches the in-VM SHA-256 ($actual)"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fips_${{ needs.determine-versioning.outputs.freebsd_package_version }}_x86_64_freebsd
|
||||
path: |
|
||||
${{ steps.freebsd-assets.outputs.pkg }}
|
||||
${{ steps.freebsd-assets.outputs.pkg }}.sha256
|
||||
retention-days: 30
|
||||
|
||||
- name: Build summary
|
||||
run: |
|
||||
echo "Build Summary for freebsd/x86_64:"
|
||||
echo " Package: ${{ steps.freebsd-assets.outputs.pkg }}"
|
||||
|
||||
release:
|
||||
name: Publish FreeBSD assets to GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download FreeBSD artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Validate .pkg bytes before publishing
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd dist
|
||||
|
||||
pkgs=$(find . -maxdepth 1 -type f -name '*.pkg' | LC_ALL=C sort)
|
||||
if [[ -z "$pkgs" ]]; then
|
||||
echo "FAIL: no .pkg artifacts were downloaded" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fail=0
|
||||
while IFS= read -r pkg; do
|
||||
base=$(basename "$pkg")
|
||||
sidecar="${pkg}.sha256"
|
||||
if [[ ! -f "$sidecar" ]]; then
|
||||
echo "FAIL: missing SHA-256 sidecar for $base" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
expected=$(awk '{print $1}' "$sidecar")
|
||||
actual=$(sha256sum "$pkg" | awk '{print $1}')
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
echo "FAIL: $base SHA-256 mismatch on the bytes about to be published" >&2
|
||||
echo " expected (FreeBSD VM): $expected" >&2
|
||||
echo " actual (downloaded): $actual" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
echo "PASS: $base matches the in-VM SHA-256 ($actual)"
|
||||
done <<<"$pkgs"
|
||||
|
||||
if [[ "$fail" -ne 0 ]]; then
|
||||
echo "==> pre-publish .pkg verification FAILED; not publishing" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> pre-publish .pkg verification PASSED"
|
||||
|
||||
- name: Generate FreeBSD release checksums
|
||||
run: |
|
||||
cd dist
|
||||
find . -maxdepth 1 -type f -name '*.pkg' -printf '%P\n' \
|
||||
| LC_ALL=C sort \
|
||||
| xargs sha256sum \
|
||||
> checksums-freebsd.txt
|
||||
|
||||
- name: Wait for tag release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
for attempt in $(seq 1 20); do
|
||||
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Release ${GITHUB_REF_NAME} not available yet; waiting..."
|
||||
sleep 15
|
||||
done
|
||||
|
||||
echo "Timed out waiting for release ${GITHUB_REF_NAME}" >&2
|
||||
exit 1
|
||||
|
||||
- name: Upload FreeBSD assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release upload "${GITHUB_REF_NAME}" \
|
||||
dist/*.pkg \
|
||||
dist/checksums-freebsd.txt \
|
||||
--clobber \
|
||||
--repo "${GITHUB_REPOSITORY}"
|
||||
@@ -113,6 +113,23 @@ with v0.4.x or earlier peers.
|
||||
- The receive-path `RejectReason` classification (shipped in 0.4.0) is
|
||||
additionally wired into the Noise XX handshake cluster
|
||||
(msg1/msg2/msg3) and the rekey-initiator outbound sites on `next`.
|
||||
|
||||
- FreeBSD support for the daemon, `fipsctl`, and `fipstop`: native TUN
|
||||
datapath (TUNSIFHEAD address-family framing, kernel-assigned `tunN`
|
||||
device name as with `utun` on macOS), clean service teardown,
|
||||
`/usr/local/etc/fips` config search path, and `/var/run/fips`
|
||||
control-socket default (both shared with macOS). The `hosts`,
|
||||
`peers.allow` / `peers.deny` and `fipsctl keygen` defaults follow the
|
||||
same `/usr/local/etc/fips` layout as macOS — see the corresponding
|
||||
entry under Fixed, which describes that move and its startup warning.
|
||||
`fips-gateway` remains Linux-only. Native `.pkg` packaging under
|
||||
`packaging/freebsd/`
|
||||
(`make freebsd`) with rc.d services, a `fips` control-socket group,
|
||||
service stop/restart across `pkg upgrade`, and `.fips` DNS integration
|
||||
for `local_unbound`/`unbound`/`dnsmasq`. mDNS LAN discovery works via
|
||||
`mdns-sd` 0.20 (`socket-pktinfo` 0.4.1, the first release that builds
|
||||
on FreeBSD). Daemon logs now disable ANSI color when stdout is not a
|
||||
terminal (all platforms).
|
||||
- An optional tick-body profiler behind the new `profiling` Cargo feature,
|
||||
**off by default**. When enabled, `fipsctl profile tick on [--dir PATH]` /
|
||||
`off` / `status` starts and stops a capture at runtime with no restart. Each
|
||||
|
||||
Generated
+18
-92
@@ -1124,9 +1124,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1687,9 +1687,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -1819,9 +1819,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mdns-sd"
|
||||
version = "0.19.2"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18148fee27e99e76dbf6e137f27727113d31f766e578d1b93a93c3615fca7081"
|
||||
checksum = "86dbb9f00c8c367f75ed3a775d3eb31d0375a72f58275ef64a1bc53c255a2ce2"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"flume",
|
||||
@@ -3113,13 +3113,13 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket-pktinfo"
|
||||
version = "0.3.2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927136cc2ae6a1b0e66ac6b1210902b75c3f726db004a73bc18686dcd0dcd22f"
|
||||
checksum = "612942246d0cc239cfd83af1dfd39be47f649208a3524e5e9da651910128e0ac"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"socket2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4037,16 +4037,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4064,31 +4055,14 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4097,96 +4071,48 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "wintun"
|
||||
version = "0.5.1"
|
||||
|
||||
+3
-1
@@ -41,7 +41,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "net", "time", "process", "io-util"] }
|
||||
futures = "0.3"
|
||||
simple-dns = "0.11.2"
|
||||
mdns-sd = "0.19"
|
||||
# 0.20 picks up socket-pktinfo 0.4.1, the first release that builds on
|
||||
# FreeBSD (IP_RECVDSTADDR/IP_RECVIF instead of Linux-style IP_PKTINFO).
|
||||
mdns-sd = "0.20"
|
||||
socket2 = { version = "0.6.2", features = ["all"] }
|
||||
tokio-socks = "0.5"
|
||||
portable-atomic = { version = "1", features = ["std"] }
|
||||
|
||||
@@ -112,19 +112,19 @@ tutorial progression starting at
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Requires Rust 1.94.1+ (edition 2024). Linux, macOS, and Windows run as
|
||||
standalone daemons; Android is supported as an embedded library (the host
|
||||
app owns the TUN, e.g. a `VpnService`). Transport availability varies by
|
||||
platform.
|
||||
Requires Rust 1.94.1+ (edition 2024). Linux, macOS, FreeBSD, and Windows
|
||||
run as standalone daemons; Android is supported as an embedded library
|
||||
(the host app owns the TUN, e.g. a `VpnService`). Transport availability
|
||||
varies by platform.
|
||||
|
||||
| Transport | Linux | macOS | Windows | Android | OpenWrt |
|
||||
|-----------|:-----:|:-----:|:-------:|:-------:|:-------:|
|
||||
| UDP | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| TCP | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Ethernet | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| Tor | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Nym | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| BLE | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Transport | Linux | macOS | FreeBSD | Windows | Android | OpenWrt |
|
||||
|-----------|:-----:|:-----:|:-------:|:-------:|:-------:|:-------:|
|
||||
| UDP | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| TCP | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Ethernet | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Tor | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Nym | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| BLE | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
On Linux, a source build requires `libclang` — the LAN gateway's
|
||||
nftables bindings are generated by `bindgen` at build time, which
|
||||
@@ -254,7 +254,7 @@ full list of v0.6.0 wire-format changes in flight.
|
||||
- Reproducible builds with toolchain pinning and
|
||||
`SOURCE_DATE_EPOCH`.
|
||||
- Linux (Debian, systemd tarball, OpenWrt, AUR), macOS (`.pkg`),
|
||||
and Windows (ZIP, service) packaging.
|
||||
FreeBSD (`.pkg`), and Windows (ZIP, service) packaging.
|
||||
- Docker-based integration and chaos testing.
|
||||
|
||||
### Near-term priorities
|
||||
|
||||
+5
-1
@@ -10,6 +10,7 @@
|
||||
# make apk Build an OpenWrt .apk package (apk-tools, mandatory on OpenWrt 25+)
|
||||
# make aur Build fips-git AUR package and validate with namcap
|
||||
# make pkg Build a macOS .pkg installer
|
||||
# make freebsd Build a FreeBSD .pkg package (on FreeBSD; use gmake)
|
||||
# make zip Build a Windows .zip package
|
||||
# make all Build deb and tarball (default)
|
||||
# make clean Remove deploy/ directory
|
||||
@@ -18,7 +19,7 @@ SHELL := /bin/bash
|
||||
PACKAGING_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
PROJECT_ROOT := $(abspath $(PACKAGING_DIR)/..)
|
||||
|
||||
.PHONY: all deb tarball ipk apk aur pkg zip clean
|
||||
.PHONY: all deb tarball ipk apk aur pkg freebsd zip clean
|
||||
|
||||
all: deb tarball
|
||||
|
||||
@@ -40,6 +41,9 @@ aur:
|
||||
pkg:
|
||||
@bash $(PACKAGING_DIR)/macos/build-pkg.sh
|
||||
|
||||
freebsd:
|
||||
@sh $(PACKAGING_DIR)/freebsd/build-pkg.sh
|
||||
|
||||
zip:
|
||||
@powershell -File $(PACKAGING_DIR)/windows/build-zip.ps1
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ make ipk # OpenWrt .ipk (opkg, OpenWrt 24.x and earlier)
|
||||
make apk # OpenWrt .apk (apk-tools, mandatory on OpenWrt 25+)
|
||||
make aur # Arch Linux AUR package (fips-git, local build + namcap)
|
||||
make pkg # macOS .pkg installer
|
||||
make freebsd # FreeBSD .pkg package (on FreeBSD; use gmake)
|
||||
make zip # Windows .zip package
|
||||
make all # deb + tarball (default)
|
||||
```
|
||||
@@ -45,6 +46,7 @@ packaging/
|
||||
aur/ Arch Linux AUR packaging (PKGBUILD, supporting files)
|
||||
common/ Shared assets (default config, hosts file)
|
||||
debian/ Debian/Ubuntu .deb packaging via cargo-deb
|
||||
freebsd/ FreeBSD .pkg packaging via pkg-create(8)
|
||||
macos/ macOS .pkg installer via pkgbuild
|
||||
systemd/ Generic Linux systemd tarball packaging
|
||||
openwrt-ipk/ OpenWrt .ipk packaging via cargo-zigbuild (opkg)
|
||||
@@ -158,6 +160,30 @@ sudo installer -pkg deploy/fips-<version>-macos-<arch>.pkg -target /
|
||||
sudo packaging/macos/uninstall.sh
|
||||
```
|
||||
|
||||
### FreeBSD (`.pkg`)
|
||||
|
||||
Built natively on a FreeBSD host with `pkg create`. Ships `fips`,
|
||||
`fipsctl`, and `fipstop` (`fips-gateway` is excluded — its NAT backend
|
||||
is nftables, Linux-only), rc.d services, and `.fips` DNS integration
|
||||
for `local_unbound`, `unbound`, or `dnsmasq`. Config installs
|
||||
sample-style under `/usr/local/etc/fips/` (edits survive upgrades).
|
||||
|
||||
```sh
|
||||
# Build (on FreeBSD; this Makefile needs GNU make — pkg install gmake)
|
||||
gmake freebsd
|
||||
# or directly, no gmake needed:
|
||||
./packaging/freebsd/build-pkg.sh
|
||||
|
||||
# Install
|
||||
pkg add ./deploy/fips-<version>-freebsd-<arch>.pkg
|
||||
sysrc fips_enable=YES fips_dns_enable=YES
|
||||
service fips start
|
||||
service fips_dns start
|
||||
```
|
||||
|
||||
See [freebsd/README.md](freebsd/README.md) for host resolver setup and
|
||||
field-tested caveats.
|
||||
|
||||
### Windows (`.zip`)
|
||||
|
||||
A ZIP archive containing binaries, default config, and PowerShell
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# FIPS FreeBSD packaging
|
||||
|
||||
Builds a native FreeBSD `.pkg` shipping `fips`, `fipsctl`, `fipstop`,
|
||||
rc.d services, and `.fips` DNS integration. `fips-gateway` is excluded
|
||||
(its NAT backend is nftables, Linux-only).
|
||||
|
||||
Platform notes: the Ethernet and BLE transports are not available on
|
||||
FreeBSD (UDP, TCP, Tor, and Nym are). The UDP datapath deliberately
|
||||
uses the portable single-packet receive loop — FreeBSD's `recvmmsg(2)`
|
||||
is a libc loop over `recvmsg`, not a kernel batch, so the Linux/macOS
|
||||
batched arm would gain nothing — and the connected-UDP fast path is
|
||||
not compiled pending FreeBSD-specific `SO_REUSEPORT` validation.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
./packaging/freebsd/build-pkg.sh # cargo build --release + pkg create
|
||||
./packaging/freebsd/build-pkg.sh --no-build # package existing release binaries
|
||||
```
|
||||
|
||||
Output: `deploy/fips-<version>-freebsd-<arch>.pkg` (e.g.
|
||||
`fips-0.5.0.dev-freebsd-amd64.pkg` — pkg versions cannot contain `-`).
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
pkg add ./deploy/fips-0.5.0.dev-freebsd-amd64.pkg
|
||||
cp /usr/local/etc/fips/fips.yaml.sample /usr/local/etc/fips/fips.yaml # then edit
|
||||
sysrc fips_enable=YES fips_dns_enable=YES
|
||||
service fips start
|
||||
service fips_dns start
|
||||
fipsctl show status
|
||||
```
|
||||
|
||||
Config installs sample-style (`@sample` semantics via manifest
|
||||
scripts), so an edited `fips.yaml` survives upgrade/removal.
|
||||
`fips.yaml` is installed `0600` — it may hold the node's private key
|
||||
(`nsec:`). The daemon runs under daemon(8) with pidfile
|
||||
`/var/run/fips/fips.pid` and logs to `/var/log/fips.log` (rc.conf
|
||||
knobs: `fips_config`, `fips_flags`, `fips_logfile`).
|
||||
|
||||
The package creates a `fips` group; members can run `fipsctl` and
|
||||
`fipstop` without root (`pw groupmod fips -m <user>`, then re-login).
|
||||
On `pkg upgrade` the services are stopped before the binaries are
|
||||
replaced and started again afterwards if enabled; on `pkg delete` they
|
||||
are stopped and the `.fips` resolver drop-in is removed.
|
||||
|
||||
## .fips DNS integration
|
||||
|
||||
The daemon answers `.fips` queries on `[::1]:5354`. `fips_dns` points
|
||||
the system resolver's `fips.` zone there; backends tried in order:
|
||||
base `local_unbound` (drop-in `/var/unbound/conf.d/fips.conf`), pkg
|
||||
`unbound`, pkg `dnsmasq`. The unbound drop-in must (and does) set:
|
||||
|
||||
- `do-not-query-localhost: no` — unbound's default silently refuses
|
||||
loopback forwarders, SERVFAILing every `.fips` query.
|
||||
- `do-ip6: yes` — the daemon binds `::1` only.
|
||||
- `domain-insecure: "fips."` — the zone is unsigned.
|
||||
|
||||
### One-time host resolver setup (NOT automated)
|
||||
|
||||
The package configures the `fips.` zone only; making the local
|
||||
resolver the *system* resolver is an operator decision. On a typical
|
||||
box:
|
||||
|
||||
```sh
|
||||
sysrc local_unbound_enable=YES
|
||||
local-unbound-setup 1.1.1.1 9.9.9.9 # explicit upstreams — see below
|
||||
service local_unbound restart
|
||||
```
|
||||
|
||||
Field-tested caveats (`fips-dns-setup` detects and warns about each):
|
||||
|
||||
- `/etc/resolv.conf` must list a loopback nameserver (ideally only
|
||||
`127.0.0.1`), or nothing ever queries unbound and `.fips` cannot
|
||||
resolve.
|
||||
- **Do not use a home-router DNS proxy as unbound's upstream.** Many
|
||||
CPE forwarders are EDNS-broken; unbound always sends EDNS, so every
|
||||
public query SERVFAILs. Forward to real resolvers (ISP or public).
|
||||
- Remove `options edns0` from `/etc/resolv.conf` if present, and set
|
||||
`resolv_conf_options=""` in `/etc/resolvconf.conf` so resolvconf(8)
|
||||
does not re-add it — against an EDNS-broken router it breaks libc
|
||||
resolution outright.
|
||||
- Never run `local-unbound-setup` with no arguments while resolv.conf
|
||||
already points at 127.0.0.1 — it snapshots that as upstream and
|
||||
forwards unbound to itself.
|
||||
|
||||
## Debugging
|
||||
|
||||
```sh
|
||||
drill -p 5354 <npub>.fips @::1 AAAA # daemon directly (bypasses unbound)
|
||||
drill <npub>.fips AAAA # full chain; SERVER: must be 127.0.0.1
|
||||
cat /var/run/fips/dns-backend # which backend fips_dns configured
|
||||
```
|
||||
|
||||
The TUN interface gets a kernel-assigned name (`tun0`, `tun1`, ...),
|
||||
like `utun` on macOS, and is destroyed automatically when the daemon
|
||||
exits. `ifconfig <name>` prints `Opened by PID <n>` for the process
|
||||
holding a tun device.
|
||||
Executable
+169
@@ -0,0 +1,169 @@
|
||||
#!/bin/sh
|
||||
# Build a FreeBSD .pkg package for FIPS using pkg-create(8).
|
||||
#
|
||||
# Usage: packaging/freebsd/build-pkg.sh [--version <version>] [--no-build]
|
||||
#
|
||||
# Prerequisites: the pinned Rust toolchain, pkg(8).
|
||||
# Output: deploy/fips-<version>-freebsd-<arch>.pkg
|
||||
#
|
||||
# Ships fips, fipsctl, and fipstop. fips-gateway is excluded: its NAT
|
||||
# backend is nftables (Linux-only) and the binary is a stub elsewhere.
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
NO_BUILD=0
|
||||
VERSION=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--no-build) NO_BUILD=1 ;;
|
||||
--version) VERSION="${2:?--version requires an argument}"; shift ;;
|
||||
*) echo "usage: $0 [--version <version>] [--no-build]" >&2; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Default to the Cargo.toml version; CI passes a derived version that
|
||||
# appends +<branch>.<height>.<hash> on branch builds. Either way, map
|
||||
# '-' and '+' to '.' — '-' is the pkg name/version separator and
|
||||
# neither is allowed inside a pkg version (0.5.0-dev -> 0.5.0.dev).
|
||||
[ -n "$VERSION" ] \
|
||||
|| VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "${PROJECT_ROOT}/Cargo.toml" | head -1)"
|
||||
[ -n "$VERSION" ] || { echo "error: could not read version from Cargo.toml" >&2; exit 1; }
|
||||
VERSION="$(printf '%s' "$VERSION" | tr -- '+-' '..')"
|
||||
|
||||
ABI="$(pkg config abi 2>/dev/null || echo "FreeBSD:15:amd64")"
|
||||
ARCH="${ABI##*:}"
|
||||
|
||||
if [ "$NO_BUILD" -eq 0 ]; then
|
||||
echo "==> cargo build --release"
|
||||
(cd "$PROJECT_ROOT" && cargo build --release)
|
||||
fi
|
||||
|
||||
for bin in fips fipsctl fipstop; do
|
||||
[ -x "${PROJECT_ROOT}/target/release/${bin}" ] \
|
||||
|| { echo "error: target/release/${bin} missing (run without --no-build)" >&2; exit 1; }
|
||||
done
|
||||
|
||||
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/fips-pkg.XXXXXX")"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
|
||||
echo "==> staging into ${STAGE}"
|
||||
install -d "${STAGE}/usr/local/bin" \
|
||||
"${STAGE}/usr/local/etc/fips" \
|
||||
"${STAGE}/usr/local/etc/rc.d" \
|
||||
"${STAGE}/usr/local/libexec/fips"
|
||||
|
||||
install -m 0755 "${PROJECT_ROOT}/target/release/fips" \
|
||||
"${PROJECT_ROOT}/target/release/fipsctl" \
|
||||
"${PROJECT_ROOT}/target/release/fipstop" \
|
||||
"${STAGE}/usr/local/bin/"
|
||||
|
||||
# Config ships sample-style: copied into place on install if absent,
|
||||
# removed on deinstall only if unmodified (see the manifest scripts).
|
||||
# fips.yaml may hold a node private key (nsec:), so it is never
|
||||
# world-readable — 0600 like the Debian and macOS packages.
|
||||
install -m 0600 "${PROJECT_ROOT}/packaging/common/fips.yaml" \
|
||||
"${STAGE}/usr/local/etc/fips/fips.yaml.sample"
|
||||
install -m 0644 "${PROJECT_ROOT}/packaging/common/hosts" \
|
||||
"${STAGE}/usr/local/etc/fips/hosts.sample"
|
||||
|
||||
install -m 0755 "${SCRIPT_DIR}/fips.rc" "${STAGE}/usr/local/etc/rc.d/fips"
|
||||
install -m 0755 "${SCRIPT_DIR}/fips-dns.rc" "${STAGE}/usr/local/etc/rc.d/fips_dns"
|
||||
|
||||
install -m 0755 "${SCRIPT_DIR}/fips-dns-setup" \
|
||||
"${SCRIPT_DIR}/fips-dns-teardown" \
|
||||
"${STAGE}/usr/local/libexec/fips/"
|
||||
|
||||
DESC="$(cat "${SCRIPT_DIR}/pkg-descr")"
|
||||
|
||||
# The config files get @sample semantics — copied into place on install
|
||||
# if absent, removed on deinstall only if unmodified — but spelled out as
|
||||
# manifest scripts: the @sample plist keyword lives in the ports tree
|
||||
# (/usr/ports/Keywords/sample.ucl), which a plain pkg-create host (e.g.
|
||||
# a CI VM) does not have.
|
||||
cat > "${STAGE}/+MANIFEST" <<EOF
|
||||
name: "fips"
|
||||
version: "${VERSION}"
|
||||
origin: "net/fips"
|
||||
comment: "Self-organizing encrypted mesh network on Nostr identities"
|
||||
desc: <<EOD
|
||||
${DESC}
|
||||
EOD
|
||||
maintainer: "jcorgan@corganlabs.com"
|
||||
www: "https://fips.network"
|
||||
abi: "${ABI}"
|
||||
prefix: "/usr/local"
|
||||
licenselogic: "single"
|
||||
licenses: ["MIT"]
|
||||
categories: ["net"]
|
||||
scripts: {
|
||||
post-install: <<EOD
|
||||
# Control-socket access group: the rc script creates /var/run/fips as
|
||||
# root:fips 0750, so members can use fipsctl/fipstop without root.
|
||||
pw groupshow fips >/dev/null 2>&1 || pw groupadd fips
|
||||
# Install-if-absent config. fips.yaml may hold a node private key
|
||||
# (nsec:), so it is 0600; FreeBSD has no "root" group, wheel is gid 0.
|
||||
[ -f /usr/local/etc/fips/fips.yaml ] || install -m 0600 -o root -g wheel \\
|
||||
/usr/local/etc/fips/fips.yaml.sample /usr/local/etc/fips/fips.yaml
|
||||
[ -f /usr/local/etc/fips/hosts ] || install -m 0644 -o root -g wheel \\
|
||||
/usr/local/etc/fips/hosts.sample /usr/local/etc/fips/hosts
|
||||
# pkg upgrade runs the old package's pre-deinstall (which stops the
|
||||
# services); bring them back up on the new binaries if enabled.
|
||||
if [ "\${PKG_UPGRADE:-}" = "true" ]; then
|
||||
if service fips enabled >/dev/null 2>&1; then
|
||||
service fips start >/dev/null 2>&1 || true
|
||||
fi
|
||||
if service fips_dns enabled >/dev/null 2>&1; then
|
||||
service fips_dns start >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
EOD
|
||||
pre-deinstall: <<EOD
|
||||
# Stop the services so the daemon binary is never replaced (upgrade) or
|
||||
# removed (deinstall) under a running process. fips_dns stop also tears
|
||||
# down the .fips resolver drop-in; on upgrade the new package's
|
||||
# post-install re-establishes it.
|
||||
service fips_dns onestop >/dev/null 2>&1 || true
|
||||
service fips onestop >/dev/null 2>&1 || true
|
||||
if [ "\${PKG_UPGRADE:-}" != "true" ]; then
|
||||
# Removal: clear the resolver drop-in even if the service was never
|
||||
# started through rc.
|
||||
/usr/local/libexec/fips/fips-dns-teardown 2>/dev/null || true
|
||||
for f in fips.yaml hosts; do
|
||||
s="/usr/local/etc/fips/\${f}.sample"
|
||||
t="/usr/local/etc/fips/\${f}"
|
||||
if [ -f "\$t" ] && cmp -s "\$t" "\$s"; then rm -f "\$t"; fi
|
||||
done
|
||||
fi
|
||||
EOD
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "${STAGE}/pkg-plist" <<'EOF'
|
||||
bin/fips
|
||||
bin/fipsctl
|
||||
bin/fipstop
|
||||
etc/fips/fips.yaml.sample
|
||||
etc/fips/hosts.sample
|
||||
etc/rc.d/fips
|
||||
etc/rc.d/fips_dns
|
||||
libexec/fips/fips-dns-setup
|
||||
libexec/fips/fips-dns-teardown
|
||||
@dir etc/fips
|
||||
EOF
|
||||
|
||||
mkdir -p "${PROJECT_ROOT}/deploy"
|
||||
echo "==> pkg create"
|
||||
pkg create -M "${STAGE}/+MANIFEST" -p "${STAGE}/pkg-plist" \
|
||||
-r "$STAGE" -o "${PROJECT_ROOT}/deploy"
|
||||
|
||||
# pkg create always names the file <name>-<version>.pkg; add the OS and
|
||||
# arch so release assets stay distinct from the macOS .pkg files.
|
||||
OUT="${PROJECT_ROOT}/deploy/fips-${VERSION}-freebsd-${ARCH}.pkg"
|
||||
mv "${PROJECT_ROOT}/deploy/fips-${VERSION}.pkg" "$OUT"
|
||||
|
||||
echo "==> built:"
|
||||
ls -l "$OUT"
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/bin/sh
|
||||
# fips-dns-setup — Configure DNS routing for the .fips domain (FreeBSD).
|
||||
#
|
||||
# Detects the system's DNS resolver and configures it to forward .fips
|
||||
# queries to the FIPS DNS responder on [::1]:5354 (the daemon's default
|
||||
# IPv6 loopback bind).
|
||||
#
|
||||
# Backends (tried in order):
|
||||
# 1. local_unbound (base system) — drop-in /var/unbound/conf.d/fips.conf
|
||||
# 2. unbound (pkg) — drop-in /usr/local/etc/unbound/conf.d/
|
||||
# 3. dnsmasq (pkg) — drop-in include, if a conf-dir is used
|
||||
# 4. Warning with manual instructions
|
||||
#
|
||||
# Notes baked in from field debugging:
|
||||
# - The daemon binds ::1 ONLY, so the resolver must forward over IPv6;
|
||||
# local-unbound-setup often writes `do-ip6: no`, which fails silently.
|
||||
# The drop-in forces `do-ip6: yes`.
|
||||
# - `domain-insecure: "fips."` is required or DNSSEC validation rejects
|
||||
# the unsigned zone.
|
||||
# - A configured, running unbound is useless if /etc/resolv.conf does
|
||||
# not point at it — warn loudly if it doesn't.
|
||||
|
||||
set -eu
|
||||
|
||||
FIPS_DNS_PORT="5354"
|
||||
FIPS_DNS_LOOPBACK_V6="::1"
|
||||
|
||||
LOCAL_UNBOUND_DROPIN_DIR="/var/unbound/conf.d"
|
||||
LOCAL_UNBOUND_DROPIN="${LOCAL_UNBOUND_DROPIN_DIR}/fips.conf"
|
||||
PKG_UNBOUND_DROPIN_DIR="/usr/local/etc/unbound/conf.d"
|
||||
PKG_UNBOUND_DROPIN="${PKG_UNBOUND_DROPIN_DIR}/fips.conf"
|
||||
DNSMASQ_DROPIN_DIR="/usr/local/etc/dnsmasq.d"
|
||||
DNSMASQ_DROPIN="${DNSMASQ_DROPIN_DIR}/fips.conf"
|
||||
|
||||
# Record which backend was configured, for teardown.
|
||||
STATE_DIR="/var/run/fips"
|
||||
STATE_FILE="${STATE_DIR}/dns-backend"
|
||||
|
||||
log() { echo "fips-dns: $*"; }
|
||||
|
||||
save_backend() {
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "$1" > "$STATE_FILE"
|
||||
}
|
||||
|
||||
service_enabled_or_running() {
|
||||
service "$1" enabled >/dev/null 2>&1 || service "$1" onestatus >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Wait for the daemon's DNS responder to be listening (up to 30s). The
|
||||
# TUN interface name is kernel-assigned (tunN), so the responder socket
|
||||
# is the reliable readiness signal.
|
||||
wait_for_daemon() {
|
||||
i=0
|
||||
while [ "$i" -lt 30 ]; do
|
||||
sockstat -6 -l -p "$FIPS_DNS_PORT" 2>/dev/null | grep -q ":$FIPS_DNS_PORT" && return 0
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
log "ERROR: nothing listening on [${FIPS_DNS_LOOPBACK_V6}]:$FIPS_DNS_PORT after 30s (is the fips service running?)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# The forward-zone drop-in shared by both unbound backends.
|
||||
unbound_snippet() {
|
||||
cat <<EOF
|
||||
# Managed by fips_dns (fips-dns-setup). Routes .fips queries to the
|
||||
# FIPS daemon's local DNS responder.
|
||||
server:
|
||||
# The .fips zone is unsigned; skip DNSSEC validation for it.
|
||||
domain-insecure: "fips."
|
||||
# The daemon binds [::1] only — forwarding must go over IPv6.
|
||||
# local-unbound-setup writes do-ip6: no by default, which would
|
||||
# make this forward-zone fail silently.
|
||||
do-ip6: yes
|
||||
# unbound refuses to query loopback forwarders by default, which
|
||||
# silently SERVFAILs every .fips query instead of asking the
|
||||
# daemon on [::1]:5354.
|
||||
do-not-query-localhost: no
|
||||
|
||||
forward-zone:
|
||||
name: "fips."
|
||||
forward-addr: ${FIPS_DNS_LOOPBACK_V6}@${FIPS_DNS_PORT}
|
||||
forward-first: no
|
||||
EOF
|
||||
}
|
||||
|
||||
# A resolver can be perfectly configured and still unused: if
|
||||
# /etc/resolv.conf has no loopback nameserver, applications never query
|
||||
# it. This was the actual root cause of ".fips doesn't resolve" in the
|
||||
# field, so diagnose it here.
|
||||
warn_if_not_system_resolver() {
|
||||
if ! grep -Eq '^[[:space:]]*nameserver[[:space:]]+(127\.0\.0\.1|::1)' /etc/resolv.conf 2>/dev/null; then
|
||||
log "WARNING: /etc/resolv.conf has no 127.0.0.1/::1 nameserver —"
|
||||
log "WARNING: the local resolver is configured but is NOT the system resolver,"
|
||||
log "WARNING: so .fips names will not resolve. Fix (base local_unbound):"
|
||||
log "WARNING: service local_unbound enable && local-unbound-setup <upstream-ip ...>"
|
||||
log "WARNING: (rewrites resolv.conf to 127.0.0.1 and keeps this drop-in)."
|
||||
fi
|
||||
|
||||
# Field finding: `options edns0` in resolv.conf (written by
|
||||
# local-unbound-setup via resolv_conf_options in /etc/resolvconf.conf)
|
||||
# has broken public resolution on some setups.
|
||||
if grep -Eq '^[[:space:]]*options.*\bedns0\b' /etc/resolv.conf 2>/dev/null; then
|
||||
log "NOTE: /etc/resolv.conf sets 'options edns0'. If public DNS resolution"
|
||||
log "NOTE: fails, remove that line and make it permanent by setting"
|
||||
log "NOTE: resolv_conf_options=\"\""
|
||||
log "NOTE: in /etc/resolvconf.conf (else resolvconf(8) re-adds it)."
|
||||
fi
|
||||
}
|
||||
|
||||
# local-unbound-setup snapshots the nameservers it finds in
|
||||
# /etc/resolv.conf into /var/unbound/forward.conf. If it is (re-)run
|
||||
# AFTER resolv.conf already points at 127.0.0.1, unbound ends up
|
||||
# forwarding every public query to itself — and with unbound's
|
||||
# do-not-query-localhost default it refuses the loop, so all public
|
||||
# resolution dies with SERVFAIL. Detect and explain.
|
||||
warn_if_forward_loop() {
|
||||
fwd="/var/unbound/forward.conf"
|
||||
[ -f "$fwd" ] || return 0
|
||||
if grep -Eq '^[[:space:]]*forward-addr:[[:space:]]*(127\.0\.0\.1|::1)([[:space:]]|$)' "$fwd"; then
|
||||
log "WARNING: ${fwd} forwards public queries to localhost — unbound is"
|
||||
log "WARNING: forwarding to itself, which breaks ALL public resolution."
|
||||
log "WARNING: (Cause: local-unbound-setup was run while resolv.conf already"
|
||||
log "WARNING: pointed at 127.0.0.1.) Fix by re-running it with explicit"
|
||||
log "WARNING: upstream resolvers, e.g. your router or ISP resolver:"
|
||||
log "WARNING: local-unbound-setup 192.168.1.1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Backend 1: base-system local_unbound
|
||||
try_local_unbound() {
|
||||
service_enabled_or_running local_unbound || return 1
|
||||
[ -d /var/unbound ] || return 1
|
||||
|
||||
log "Configuring via local_unbound (${LOCAL_UNBOUND_DROPIN})"
|
||||
mkdir -p "$LOCAL_UNBOUND_DROPIN_DIR"
|
||||
unbound_snippet > "$LOCAL_UNBOUND_DROPIN"
|
||||
|
||||
if command -v local-unbound-checkconf >/dev/null 2>&1 \
|
||||
&& ! local-unbound-checkconf >/dev/null 2>&1; then
|
||||
log "ERROR: local-unbound-checkconf rejected the config; removing drop-in"
|
||||
rm -f "$LOCAL_UNBOUND_DROPIN"
|
||||
return 1
|
||||
fi
|
||||
|
||||
service local_unbound reload >/dev/null 2>&1 \
|
||||
|| service local_unbound onerestart >/dev/null 2>&1 \
|
||||
|| log "WARNING: local_unbound reload failed (config written, may need manual restart)"
|
||||
save_backend "local_unbound"
|
||||
warn_if_not_system_resolver
|
||||
warn_if_forward_loop
|
||||
return 0
|
||||
}
|
||||
|
||||
# Backend 2: pkg unbound
|
||||
try_pkg_unbound() {
|
||||
service_enabled_or_running unbound || return 1
|
||||
[ -d /usr/local/etc/unbound ] || return 1
|
||||
|
||||
log "Configuring via unbound (${PKG_UNBOUND_DROPIN})"
|
||||
mkdir -p "$PKG_UNBOUND_DROPIN_DIR"
|
||||
unbound_snippet > "$PKG_UNBOUND_DROPIN"
|
||||
|
||||
if ! grep -Erqs '^[[:space:]]*include(-toplevel)?:.*conf\.d' /usr/local/etc/unbound/unbound.conf; then
|
||||
log "NOTE: ensure unbound.conf includes the drop-in directory, e.g.:"
|
||||
log "NOTE: include-toplevel: \"${PKG_UNBOUND_DROPIN_DIR}/*.conf\""
|
||||
fi
|
||||
|
||||
service unbound reload >/dev/null 2>&1 \
|
||||
|| service unbound onerestart >/dev/null 2>&1 \
|
||||
|| log "WARNING: unbound reload failed (config written, may need manual restart)"
|
||||
save_backend "pkg-unbound"
|
||||
warn_if_not_system_resolver
|
||||
return 0
|
||||
}
|
||||
|
||||
# Backend 3: pkg dnsmasq
|
||||
#
|
||||
# dnsmasq's `server=/<domain>/<addr>#<port>` accepts a bare IPv6 literal.
|
||||
try_dnsmasq() {
|
||||
service_enabled_or_running dnsmasq || return 1
|
||||
|
||||
log "Configuring via dnsmasq (${DNSMASQ_DROPIN})"
|
||||
mkdir -p "$DNSMASQ_DROPIN_DIR"
|
||||
cat > "$DNSMASQ_DROPIN" <<EOF
|
||||
# FIPS .fips domain forwarding (managed by fips_dns)
|
||||
server=/fips/${FIPS_DNS_LOOPBACK_V6}#${FIPS_DNS_PORT}
|
||||
EOF
|
||||
|
||||
if ! grep -Eqs "^[[:space:]]*conf-dir=.*dnsmasq\.d" /usr/local/etc/dnsmasq.conf; then
|
||||
log "NOTE: ensure dnsmasq.conf reads the drop-in directory, e.g.:"
|
||||
log "NOTE: conf-dir=${DNSMASQ_DROPIN_DIR}/,*.conf"
|
||||
fi
|
||||
|
||||
service dnsmasq reload >/dev/null 2>&1 \
|
||||
|| service dnsmasq onerestart >/dev/null 2>&1 \
|
||||
|| log "WARNING: dnsmasq reload failed (config written, may need manual restart)"
|
||||
save_backend "dnsmasq"
|
||||
warn_if_not_system_resolver
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
wait_for_daemon || exit 1
|
||||
|
||||
try_local_unbound && exit 0
|
||||
try_pkg_unbound && exit 0
|
||||
try_dnsmasq && exit 0
|
||||
|
||||
log "WARNING: No supported DNS resolver detected."
|
||||
log "To resolve .fips domains, forward the fips. zone to"
|
||||
log "[${FIPS_DNS_LOOPBACK_V6}]:${FIPS_DNS_PORT} (the daemon's default bind)."
|
||||
log ""
|
||||
log "Easiest path on FreeBSD (base local_unbound):"
|
||||
log " sysrc local_unbound_enable=YES"
|
||||
log " local-unbound-setup"
|
||||
log " service fips_dns restart"
|
||||
save_backend "none"
|
||||
exit 0
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
# fips-dns-teardown — Remove .fips DNS routing configured by fips-dns-setup.
|
||||
|
||||
set -eu
|
||||
|
||||
STATE_FILE="/var/run/fips/dns-backend"
|
||||
|
||||
LOCAL_UNBOUND_DROPIN="/var/unbound/conf.d/fips.conf"
|
||||
PKG_UNBOUND_DROPIN="/usr/local/etc/unbound/conf.d/fips.conf"
|
||||
DNSMASQ_DROPIN="/usr/local/etc/dnsmasq.d/fips.conf"
|
||||
|
||||
log() { echo "fips-dns: $*"; }
|
||||
|
||||
backend=""
|
||||
[ -f "$STATE_FILE" ] && backend="$(cat "$STATE_FILE")"
|
||||
|
||||
case "$backend" in
|
||||
local_unbound)
|
||||
log "Removing local_unbound drop-in"
|
||||
rm -f "$LOCAL_UNBOUND_DROPIN"
|
||||
service local_unbound reload >/dev/null 2>&1 \
|
||||
|| service local_unbound onerestart >/dev/null 2>&1 \
|
||||
|| log "WARNING: local_unbound reload failed"
|
||||
;;
|
||||
pkg-unbound)
|
||||
log "Removing unbound drop-in"
|
||||
rm -f "$PKG_UNBOUND_DROPIN"
|
||||
service unbound reload >/dev/null 2>&1 \
|
||||
|| service unbound onerestart >/dev/null 2>&1 \
|
||||
|| log "WARNING: unbound reload failed"
|
||||
;;
|
||||
dnsmasq)
|
||||
log "Removing dnsmasq drop-in"
|
||||
rm -f "$DNSMASQ_DROPIN"
|
||||
service dnsmasq reload >/dev/null 2>&1 \
|
||||
|| service dnsmasq onerestart >/dev/null 2>&1 \
|
||||
|| log "WARNING: dnsmasq reload failed"
|
||||
;;
|
||||
none)
|
||||
log "No backend was configured; nothing to remove"
|
||||
;;
|
||||
*)
|
||||
# Unknown or missing state — clean up any drop-in we may own.
|
||||
log "No recorded backend; removing any FIPS drop-ins"
|
||||
rm -f "$LOCAL_UNBOUND_DROPIN" "$PKG_UNBOUND_DROPIN" "$DNSMASQ_DROPIN"
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -f "$STATE_FILE"
|
||||
exit 0
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
|
||||
# PROVIDE: fips_dns
|
||||
# REQUIRE: fips
|
||||
# KEYWORD: shutdown
|
||||
|
||||
# One-shot service that points the system resolver's `fips.` zone at the
|
||||
# FIPS daemon's built-in DNS responder on [::1]:5354, so `<name>.fips`
|
||||
# hostnames resolve system-wide. Set fips_dns_enable=YES in rc.conf.
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="fips_dns"
|
||||
rcvar="fips_dns_enable"
|
||||
desc="Route .fips DNS queries to the FIPS daemon"
|
||||
|
||||
load_rc_config $name
|
||||
|
||||
: ${fips_dns_enable:="NO"}
|
||||
|
||||
start_cmd="fips_dns_start"
|
||||
stop_cmd="fips_dns_stop"
|
||||
|
||||
fips_dns_start() {
|
||||
/usr/local/libexec/fips/fips-dns-setup
|
||||
}
|
||||
|
||||
fips_dns_stop() {
|
||||
/usr/local/libexec/fips/fips-dns-teardown
|
||||
}
|
||||
|
||||
run_rc_command "$1"
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
|
||||
# PROVIDE: fips
|
||||
# REQUIRE: NETWORKING FILESYSTEMS
|
||||
# KEYWORD: shutdown
|
||||
|
||||
# rc.conf knobs:
|
||||
# fips_enable (bool): Set YES to run the FIPS daemon. Default NO.
|
||||
# fips_config (path): Config file. Default /usr/local/etc/fips/fips.yaml.
|
||||
# fips_flags (str): Extra arguments passed to the fips daemon.
|
||||
# fips_logfile (path): Daemon stdout/stderr log. Default /var/log/fips.log.
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="fips"
|
||||
rcvar="fips_enable"
|
||||
desc="FIPS mesh networking daemon"
|
||||
|
||||
load_rc_config $name
|
||||
|
||||
: ${fips_enable:="NO"}
|
||||
: ${fips_config:="/usr/local/etc/fips/fips.yaml"}
|
||||
: ${fips_logfile:="/var/log/fips.log"}
|
||||
|
||||
runtime_dir="/var/run/fips"
|
||||
pidfile="${runtime_dir}/fips.pid"
|
||||
procname="/usr/local/bin/fips"
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-p ${pidfile} -t fips -o ${fips_logfile} ${procname} --config ${fips_config} ${fips_flags}"
|
||||
start_precmd="fips_precmd"
|
||||
|
||||
# The daemon resolves its control socket to /var/run/fips when the
|
||||
# directory exists, so it must be there before the daemon starts.
|
||||
# root:fips 0750 (matching the Debian tmpfiles entry) lets members of
|
||||
# the fips group use fipsctl/fipstop without root; the group is created
|
||||
# by the package post-install. Fall back to 0755 for source builds
|
||||
# where the group does not exist.
|
||||
fips_precmd() {
|
||||
if pw groupshow fips >/dev/null 2>&1; then
|
||||
install -d -m 0750 -o root -g fips "$runtime_dir"
|
||||
else
|
||||
install -d -m 0755 "$runtime_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
run_rc_command "$1"
|
||||
@@ -0,0 +1,14 @@
|
||||
FIPS (Free Internetworking Peering System) is a self-organizing
|
||||
encrypted mesh network built on Nostr identities, operating over
|
||||
arbitrary transports (UDP, TCP, Ethernet, Tor) with no central
|
||||
infrastructure.
|
||||
|
||||
A TUN interface maps each remote peer to an fd00::/8 address,
|
||||
so unmodified IPv6 applications work over the mesh, with a built-in
|
||||
DNS responder for <npub>.fips names. Peer discovery and NAT traversal
|
||||
are Nostr-mediated; traffic is encrypted hop-by-hop (Noise IK) and
|
||||
end-to-end (Noise XK) with periodic rekey.
|
||||
|
||||
This package ships the fips daemon, the fipsctl control CLI, the
|
||||
fipstop live-status TUI, and rc.d services for the daemon (fips) and
|
||||
for system-wide .fips DNS resolution (fips_dns).
|
||||
+9
-3
@@ -99,7 +99,13 @@ async fn run_daemon(
|
||||
_ => filter,
|
||||
};
|
||||
|
||||
fmt().with_env_filter(filter).with_target(true).init();
|
||||
// ANSI color only when stdout is a terminal — under a supervisor
|
||||
// (daemon(8), systemd) escape codes would litter the log file.
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stdout()))
|
||||
.init();
|
||||
|
||||
info!("FIPS {} starting", version::short_version());
|
||||
|
||||
@@ -111,9 +117,9 @@ async fn run_daemon(
|
||||
}
|
||||
}
|
||||
|
||||
// The hosts/ACL defaults on macOS moved from /etc/fips to
|
||||
// The hosts/ACL defaults on these platforms moved from /etc/fips to
|
||||
// /usr/local/etc/fips; flag files stranded at the old location.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
fips::node::warn_on_legacy_config_paths();
|
||||
|
||||
// Identity provisioning: config nsec > key file > generate ephemeral
|
||||
|
||||
+16
-5
@@ -425,6 +425,17 @@ fn main() {
|
||||
let key_path = dir.join("fips.key");
|
||||
let pub_path = dir.join("fips.pub");
|
||||
|
||||
// The default key directory on macOS/FreeBSD moved from /etc/fips
|
||||
// to /usr/local/etc/fips; point at keys stranded at the old path.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
if std::path::Path::new("/etc/fips/fips.key").exists() && !key_path.exists() {
|
||||
eprintln!("note: /etc/fips/fips.key exists but the default key directory");
|
||||
eprintln!(
|
||||
" is now {}; that key is no longer used by default.",
|
||||
dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
if key_path.exists() && !force {
|
||||
eprintln!("error: key file already exists: {}", key_path.display());
|
||||
eprintln!("Use --force to overwrite.");
|
||||
@@ -655,15 +666,15 @@ fn sparkline(values: &[f64], min: f64, max: f64) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// macOS packaging ships config under /usr/local/etc/fips/.
|
||||
#[cfg(target_os = "macos")]
|
||||
// macOS and FreeBSD packaging ship config under /usr/local/etc/fips/.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
#[test]
|
||||
fn test_default_key_dir_follows_macos_packaging_layout() {
|
||||
fn test_default_key_dir_follows_packaging_layout() {
|
||||
assert_eq!(default_key_dir(), PathBuf::from("/usr/local/etc/fips"));
|
||||
}
|
||||
|
||||
// Non-macOS Unix keeps the historic /etc/fips/ location.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
// Other Unix keeps the historic /etc/fips/ location.
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "freebsd"))))]
|
||||
#[test]
|
||||
fn test_default_key_dir_keeps_etc_fips_layout() {
|
||||
assert_eq!(default_key_dir(), PathBuf::from("/etc/fips"));
|
||||
|
||||
+40
-22
@@ -49,12 +49,13 @@ pub use transport::{
|
||||
const CONFIG_FILENAME: &str = "fips.yaml";
|
||||
|
||||
/// System-wide config directory, following the platform's packaging layout
|
||||
/// (`/usr/local/etc/fips` on macOS, `/etc/fips` otherwise). The daemon
|
||||
/// derives identity key paths from the config file's location, so anything
|
||||
/// that reads or writes config-adjacent files should use this one constant.
|
||||
#[cfg(target_os = "macos")]
|
||||
/// (`/usr/local/etc/fips` on macOS and FreeBSD, `/etc/fips` otherwise). The
|
||||
/// daemon derives identity key paths from the config file's location, so
|
||||
/// anything that reads or writes config-adjacent files should use this one
|
||||
/// constant.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub const SYSTEM_CONFIG_DIR: &str = "/usr/local/etc/fips";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
pub const SYSTEM_CONFIG_DIR: &str = "/etc/fips";
|
||||
|
||||
/// Default key filename, placed alongside the config file.
|
||||
@@ -167,6 +168,13 @@ pub(crate) fn resolve_default_socket(filename: &str) -> String {
|
||||
return format!("/run/fips/{filename}");
|
||||
}
|
||||
|
||||
// 1b. /var/run/fips — macOS and FreeBSD have no /run; the FreeBSD
|
||||
// rc.d script creates this directory at service start.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
if Path::new("/var/run/fips").is_dir() {
|
||||
return format!("/var/run/fips/{filename}");
|
||||
}
|
||||
|
||||
// 2. $XDG_RUNTIME_DIR/fips/ — only if the variable points at an existing
|
||||
// directory.
|
||||
if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
@@ -624,12 +632,12 @@ impl Config {
|
||||
// keep working after an upgrade.
|
||||
paths.push(PathBuf::from("/etc/fips").join(CONFIG_FILENAME));
|
||||
|
||||
// macOS packaging installs config under /usr/local/etc/fips
|
||||
// (Homebrew-style prefix); probe it after /etc/fips so the
|
||||
// packaged file wins over a stale /etc/fips leftover. Read from
|
||||
// SYSTEM_CONFIG_DIR rather than a second literal, so this path and
|
||||
// the directory `fipsctl keygen` writes into cannot drift apart.
|
||||
#[cfg(target_os = "macos")]
|
||||
// macOS and FreeBSD packaging install config under /usr/local/etc/fips;
|
||||
// probe it after /etc/fips so the packaged file wins over a stale
|
||||
// /etc/fips leftover. Read from SYSTEM_CONFIG_DIR rather than a second
|
||||
// literal, so this path and the directory `fipsctl keygen` writes into
|
||||
// cannot drift apart.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
paths.push(PathBuf::from(SYSTEM_CONFIG_DIR).join(CONFIG_FILENAME));
|
||||
|
||||
// User config directory
|
||||
@@ -1159,8 +1167,8 @@ node:
|
||||
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml"))
|
||||
);
|
||||
|
||||
// macOS should also include /usr/local/etc/fips
|
||||
#[cfg(target_os = "macos")]
|
||||
// macOS and FreeBSD should also include /usr/local/etc/fips
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
assert!(
|
||||
paths
|
||||
.iter()
|
||||
@@ -2096,13 +2104,18 @@ node:
|
||||
|
||||
// If /run/fips happens to be writable in the test environment (CI
|
||||
// running as root, for instance), the resolver legitimately picks
|
||||
// /run/fips and skips XDG entirely. Accept either outcome but
|
||||
// demand that one of the two canonical prefixes is chosen — never
|
||||
// /tmp when XDG was valid.
|
||||
// /run/fips and skips XDG entirely — likewise /var/run/fips on
|
||||
// macOS/FreeBSD. Accept either outcome but demand that one of the
|
||||
// canonical prefixes is chosen — never /tmp when XDG was valid.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
let canonical_var_run = path.starts_with("/var/run/fips/");
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
let canonical_var_run = false;
|
||||
assert!(
|
||||
path.starts_with("/run/fips/")
|
||||
|| canonical_var_run
|
||||
|| path.starts_with(&format!("{}/fips/", temp_dir.path().display())),
|
||||
"expected /run/fips or XDG path, got: {path}"
|
||||
"expected /run/fips, /var/run/fips, or XDG path, got: {path}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2132,12 +2145,17 @@ node:
|
||||
}
|
||||
}
|
||||
|
||||
// Accept either /run/fips/ (test running as root with that dir
|
||||
// writable) or /tmp/fips-... (the dev-machine fallback). Never
|
||||
// accept the bogus XDG dir leaking through.
|
||||
// Accept /run/fips/ (test running as root with that dir
|
||||
// writable), /var/run/fips/ on macOS/FreeBSD, or /tmp/fips-...
|
||||
// (the dev-machine fallback). Never accept the bogus XDG dir
|
||||
// leaking through.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
let canonical_var_run = path.starts_with("/var/run/fips/");
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
let canonical_var_run = false;
|
||||
assert!(
|
||||
path.starts_with("/run/fips/") || path == "/tmp/fips-gateway.sock",
|
||||
"expected /run/fips or /tmp fallback, got: {path}"
|
||||
path.starts_with("/run/fips/") || canonical_var_run || path == "/tmp/fips-gateway.sock",
|
||||
"expected /run/fips, /var/run/fips, or /tmp fallback, got: {path}"
|
||||
);
|
||||
assert!(
|
||||
!path.starts_with(bogus),
|
||||
|
||||
+28
-26
@@ -24,33 +24,34 @@ use tracing::{debug, info, warn};
|
||||
|
||||
/// Default path for the peer allow list.
|
||||
///
|
||||
/// On macOS the install layout (see `packaging/macos/`) ships config under
|
||||
/// `/usr/local/etc/fips/` rather than `/etc/fips/`; the default follows the
|
||||
/// platform's packaging so the daemon reads the file the operator was told
|
||||
/// to edit. Linux and other Unix keep the historic `/etc/fips/` location.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT_PEERS_ALLOW_PATH: &str = "/usr/local/etc/fips/peers.allow";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
/// On macOS (`packaging/macos/`) and FreeBSD (`packaging/freebsd/`) the
|
||||
/// install layout ships config under `/usr/local/etc/fips/` rather than
|
||||
/// `/etc/fips/`; the default follows the platform's packaging so the daemon
|
||||
/// reads the file the operator was told to edit. Linux and other Unix keep
|
||||
/// the historic `/etc/fips/` location.
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
pub const DEFAULT_PEERS_ALLOW_PATH: &str = "/etc/fips/peers.allow";
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub const DEFAULT_PEERS_ALLOW_PATH: &str = "/usr/local/etc/fips/peers.allow";
|
||||
|
||||
/// Default path for the peer deny list.
|
||||
///
|
||||
/// See [`DEFAULT_PEERS_ALLOW_PATH`] for the macOS `/usr/local/etc/fips/`
|
||||
/// See [`DEFAULT_PEERS_ALLOW_PATH`] for the `/usr/local/etc/fips/`
|
||||
/// rationale.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT_PEERS_DENY_PATH: &str = "/usr/local/etc/fips/peers.deny";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
pub const DEFAULT_PEERS_DENY_PATH: &str = "/etc/fips/peers.deny";
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub const DEFAULT_PEERS_DENY_PATH: &str = "/usr/local/etc/fips/peers.deny";
|
||||
|
||||
/// Warn about config files stranded at the pre-move default location.
|
||||
///
|
||||
/// The macOS defaults for `hosts`, `peers.allow` and `peers.deny` moved
|
||||
/// from `/etc/fips` to `/usr/local/etc/fips`, the directory the macOS
|
||||
/// packaging actually populates. The old location is no longer read by
|
||||
/// the default path constants, and a `peers.deny` silently left behind
|
||||
/// there would fail open (a missing deny list is not an error), so surface
|
||||
/// the situation loudly once at startup.
|
||||
#[cfg(target_os = "macos")]
|
||||
/// The macOS/FreeBSD defaults for `hosts`, `peers.allow` and `peers.deny`
|
||||
/// moved from `/etc/fips` to `/usr/local/etc/fips`, the directory both
|
||||
/// installers actually populate. The old location is no longer read, and
|
||||
/// a `peers.deny` silently left behind there would fail open (a missing
|
||||
/// deny list is not an error), so surface the situation loudly once at
|
||||
/// startup.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub fn warn_on_legacy_config_paths() {
|
||||
for (current, name) in [
|
||||
(crate::upper::hosts::DEFAULT_HOSTS_PATH, "hosts"),
|
||||
@@ -538,21 +539,22 @@ mod tests {
|
||||
acl
|
||||
}
|
||||
|
||||
// Guard against the macOS path regression: the install layout
|
||||
// (`packaging/macos/`) ships config under `/usr/local/etc/fips/`, so the
|
||||
// default ACL paths must follow it, or `peers.allow`/`peers.deny` are
|
||||
// silently unread on macOS (see the `NotFound` no-op in `load_file`).
|
||||
#[cfg(target_os = "macos")]
|
||||
// Guard against the path regression: the macOS and FreeBSD install
|
||||
// layouts (`packaging/macos/`, `packaging/freebsd/`) ship config under
|
||||
// `/usr/local/etc/fips/`, so the default ACL paths must follow it, or
|
||||
// `peers.allow`/`peers.deny` are silently unread there (see the
|
||||
// `NotFound` no-op in `load_file`).
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
#[test]
|
||||
fn test_default_acl_paths_follow_macos_packaging_layout() {
|
||||
fn test_default_acl_paths_follow_packaging_layout() {
|
||||
assert_eq!(DEFAULT_PEERS_ALLOW_PATH, "/usr/local/etc/fips/peers.allow");
|
||||
assert_eq!(DEFAULT_PEERS_DENY_PATH, "/usr/local/etc/fips/peers.deny");
|
||||
}
|
||||
|
||||
// Non-macOS Unix/Linux keeps the historic `/etc/fips/` location; this
|
||||
// Other Unix/Linux keeps the historic `/etc/fips/` location; this
|
||||
// runs on the Linux CI matrix and pins the value so a future refactor
|
||||
// can't silently drift it.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "freebsd"))))]
|
||||
#[test]
|
||||
fn test_default_acl_paths_keep_etc_fips_layout() {
|
||||
assert_eq!(DEFAULT_PEERS_ALLOW_PATH, "/etc/fips/peers.allow");
|
||||
|
||||
+17
-11
@@ -1857,10 +1857,13 @@ impl Node {
|
||||
info!("effective MTU: {} bytes", effective_mtu);
|
||||
debug!(" max TCP MSS: {} bytes", max_mss);
|
||||
|
||||
// On macOS, create a shutdown pipe. Writing to it unblocks the
|
||||
// reader thread's select() loop without closing the TUN fd
|
||||
// (which would cause a double-close when TunDevice drops).
|
||||
#[cfg(target_os = "macos")]
|
||||
// On macOS and FreeBSD, create a shutdown pipe. Writing to it
|
||||
// unblocks the reader thread's select() loop without closing
|
||||
// the TUN fd (which would cause a double-close when TunDevice
|
||||
// drops). Linux instead unblocks the reader by deleting the
|
||||
// interface; on macOS/FreeBSD downing the interface does not
|
||||
// wake a blocked read.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
let (shutdown_read_fd, shutdown_write_fd) = {
|
||||
let mut fds = [0i32; 2];
|
||||
if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
|
||||
@@ -1906,7 +1909,7 @@ impl Node {
|
||||
let transport_mtu = self.transport_mtu();
|
||||
let path_mtu_lookup = self.path_mtu_lookup.clone();
|
||||
let reader_child_tx = self.child_exit_tx.clone();
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
let reader_handle = thread::spawn(move || {
|
||||
run_tun_reader(
|
||||
device,
|
||||
@@ -1922,7 +1925,7 @@ impl Node {
|
||||
let _ = tx.blocking_send(Child::Tun);
|
||||
}
|
||||
});
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
let reader_handle = thread::spawn(move || {
|
||||
run_tun_reader(
|
||||
device,
|
||||
@@ -1944,7 +1947,7 @@ impl Node {
|
||||
self.supervisor.tun_outbound_rx = Some(outbound_rx);
|
||||
self.supervisor.tun_reader_handle = Some(reader_handle);
|
||||
self.supervisor.tun_writer_handle = Some(writer_handle);
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
{
|
||||
self.supervisor.tun_shutdown_fd = Some(shutdown_write_fd);
|
||||
}
|
||||
@@ -2335,14 +2338,17 @@ impl Node {
|
||||
// Drop the tun_tx to signal the writer to stop
|
||||
self.supervisor.tun_tx.take();
|
||||
|
||||
// Delete the interface (on Linux, causes reader to get EFAULT)
|
||||
// Delete the interface (on Linux, causes reader to get
|
||||
// EFAULT; on macOS/FreeBSD this downs it — the kernel
|
||||
// destroys the device once the reader closes the fd).
|
||||
if let Err(e) = shutdown_tun_interface(&name).await {
|
||||
warn!(name = %name, error = %e, "Failed to shutdown TUN interface");
|
||||
}
|
||||
|
||||
// On macOS, signal the reader thread to exit by writing to the
|
||||
// shutdown pipe. The reader's select() will wake up and break.
|
||||
#[cfg(target_os = "macos")]
|
||||
// On macOS and FreeBSD, signal the reader thread to exit by
|
||||
// writing to the shutdown pipe. The reader's select() will
|
||||
// wake up and break.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
if let Some(fd) = self.supervisor.tun_shutdown_fd.take() {
|
||||
unsafe {
|
||||
libc::write(fd, b"x".as_ptr() as *const libc::c_void, 1);
|
||||
|
||||
@@ -682,9 +682,10 @@ pub(crate) struct Supervisor {
|
||||
pub(in crate::node) tun_reader_handle: Option<JoinHandle<()>>,
|
||||
/// TUN writer thread handle.
|
||||
pub(in crate::node) tun_writer_handle: Option<JoinHandle<()>>,
|
||||
/// Shutdown pipe: writing to this fd unblocks the TUN reader thread on macOS.
|
||||
/// On Linux, deleting the interface via netlink serves the same purpose.
|
||||
#[cfg(target_os = "macos")]
|
||||
/// Shutdown pipe: writing to this fd unblocks the TUN reader thread on
|
||||
/// macOS and FreeBSD. On Linux, deleting the interface via netlink
|
||||
/// serves the same purpose.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub(in crate::node) tun_shutdown_fd: Option<std::os::unix::io::RawFd>,
|
||||
|
||||
/// Receiver for resolved identities from the DNS responder.
|
||||
@@ -732,7 +733,7 @@ impl Supervisor {
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
|
||||
+11
-1
@@ -5,7 +5,7 @@
|
||||
//! Bloom filters, coordinate caches, transports, links, and peers.
|
||||
|
||||
pub(crate) mod acl;
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub use acl::warn_on_legacy_config_paths;
|
||||
mod bloom;
|
||||
pub(crate) mod context;
|
||||
@@ -961,6 +961,16 @@ impl Node {
|
||||
transports.push(TransportHandle::Ethernet(eth));
|
||||
}
|
||||
}
|
||||
// `EthernetConfig` always parses, so on platforms without the
|
||||
// transport a configured `ethernet:` block would otherwise be
|
||||
// dropped silently and the node would report healthy without it.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
for (name, _) in self.config().transports.ethernet.iter() {
|
||||
tracing::warn!(
|
||||
instance = name.unwrap_or("default"),
|
||||
"Ethernet transport is not supported on this platform; ignoring configured instance"
|
||||
);
|
||||
}
|
||||
|
||||
// Create TCP transport instances
|
||||
let tcp_instances: Vec<_> = self
|
||||
|
||||
+16
-16
@@ -18,15 +18,15 @@ use tracing::{debug, info, warn};
|
||||
|
||||
/// Default path for the FIPS hosts file.
|
||||
///
|
||||
/// On macOS the install layout (see `packaging/macos/`) ships config under
|
||||
/// `/usr/local/etc/fips/`, not `/etc/fips/`; the default follows the
|
||||
/// platform's packaging so aliases edited by the operator are actually
|
||||
/// loaded. Linux and other Unix keep the historic `/etc/fips/` location;
|
||||
/// Windows uses `%ProgramData%`.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT_HOSTS_PATH: &str = "/usr/local/etc/fips/hosts";
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
/// On macOS (`packaging/macos/`) and FreeBSD (`packaging/freebsd/`) the
|
||||
/// install layout ships config under `/usr/local/etc/fips/`, not
|
||||
/// `/etc/fips/`; the default follows the platform's packaging so aliases
|
||||
/// edited by the operator are actually loaded. Linux and other Unix keep
|
||||
/// the historic `/etc/fips/` location; Windows uses `%ProgramData%`.
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "freebsd"))))]
|
||||
pub const DEFAULT_HOSTS_PATH: &str = "/etc/fips/hosts";
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub const DEFAULT_HOSTS_PATH: &str = "/usr/local/etc/fips/hosts";
|
||||
#[cfg(windows)]
|
||||
pub const DEFAULT_HOSTS_PATH: &str = r"C:\ProgramData\fips\hosts";
|
||||
|
||||
@@ -321,18 +321,18 @@ mod tests {
|
||||
|
||||
// --- default path tests ---
|
||||
|
||||
// Guard against the macOS path regression: the install layout
|
||||
// (`packaging/macos/`) ships config under `/usr/local/etc/fips/`, so the
|
||||
// default hosts path must follow it, or host-file aliases are silently
|
||||
// unloaded on macOS (see the `NotFound` no-op in `load_hosts_file`).
|
||||
#[cfg(target_os = "macos")]
|
||||
// Guard against the path regression: the macOS and FreeBSD install
|
||||
// layouts ship config under `/usr/local/etc/fips/`, so the default hosts
|
||||
// path must follow it, or host-file aliases are silently unloaded there
|
||||
// (see the `NotFound` no-op in `load_hosts_file`).
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
#[test]
|
||||
fn test_default_hosts_path_follows_macos_packaging_layout() {
|
||||
fn test_default_hosts_path_follows_packaging_layout() {
|
||||
assert_eq!(DEFAULT_HOSTS_PATH, "/usr/local/etc/fips/hosts");
|
||||
}
|
||||
|
||||
// Non-macOS Unix/Linux keeps the historic `/etc/fips/` location.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
// Other Unix/Linux keeps the historic `/etc/fips/` location.
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "freebsd"))))]
|
||||
#[test]
|
||||
fn test_default_hosts_path_keeps_etc_fips_layout() {
|
||||
assert_eq!(DEFAULT_HOSTS_PATH, "/etc/fips/hosts");
|
||||
|
||||
+159
-59
@@ -6,7 +6,8 @@
|
||||
//!
|
||||
//! Platform-specific implementations:
|
||||
//! - Linux: Uses the `tun` crate with `rtnetlink` for interface configuration
|
||||
//! - macOS: Uses the `tun` crate with `ifconfig`/`route` for interface configuration
|
||||
//! - macOS/FreeBSD: Uses the `tun` crate with `ifconfig`/`route` for interface
|
||||
//! configuration
|
||||
//! - Windows: Uses the `wintun` crate for TUN device support
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -18,7 +19,7 @@ use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
#[cfg(unix)]
|
||||
use std::io::Read;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
#[cfg(unix)]
|
||||
use std::io::Write;
|
||||
use std::net::Ipv6Addr;
|
||||
@@ -31,7 +32,7 @@ use tracing::error;
|
||||
use tracing::{debug, trace};
|
||||
#[cfg(windows)]
|
||||
use tracing::{error, warn};
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
|
||||
use tun::Layer;
|
||||
|
||||
/// Read-only handle to the per-destination path MTU map. Populated by
|
||||
@@ -237,16 +238,20 @@ impl TunDevice {
|
||||
// Create the TUN device. `mut` is only exercised on linux/macos, where
|
||||
// the name/layer/mtu are set below; other unix targets (android) pass
|
||||
// the default config through unchanged.
|
||||
#[cfg_attr(not(any(target_os = "linux", target_os = "macos")), allow(unused_mut))]
|
||||
#[cfg_attr(
|
||||
not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")),
|
||||
allow(unused_mut)
|
||||
)]
|
||||
let mut tun_config = tun::Configuration::default();
|
||||
|
||||
// On macOS, utun devices get kernel-assigned names (utun0, utun1, ...),
|
||||
// so we skip setting the name and read it back after creation.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[allow(deprecated)]
|
||||
tun_config.name(name).layer(Layer::L3).mtu(mtu);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// On macOS and FreeBSD the kernel assigns the device name (utun0...,
|
||||
// tun0...), so the name is not set here and is read back after
|
||||
// creation.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
tun_config.layer(Layer::L3).mtu(mtu);
|
||||
@@ -254,7 +259,25 @@ impl TunDevice {
|
||||
|
||||
let device = tun::create(&tun_config)?;
|
||||
|
||||
// Read the actual device name (on macOS this is the kernel-assigned utun* name)
|
||||
// FreeBSD: enable TUNSIFHEAD. Without it, tunoutput() rejects every
|
||||
// non-IPv4 packet with EAFNOSUPPORT, so nothing IPv6 can be sent
|
||||
// through the interface. With it, every frame on the fd carries a
|
||||
// 4-byte network-order address-family prefix (like macOS utun),
|
||||
// which the reader and writer handle.
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
const TUNSIFHEAD: libc::c_ulong = 0x8004_7460; // _IOW('t', 96, int)
|
||||
let enable: libc::c_int = 1;
|
||||
if unsafe { libc::ioctl(device.as_raw_fd(), TUNSIFHEAD, &enable) } != 0 {
|
||||
return Err(TunError::Configure(format!(
|
||||
"TUNSIFHEAD ioctl failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Read the actual device name (on macOS and FreeBSD this is the
|
||||
// kernel-assigned utunN / tunN name)
|
||||
let actual_name = {
|
||||
use tun::AbstractDevice;
|
||||
device
|
||||
@@ -304,13 +327,30 @@ impl TunDevice {
|
||||
/// The buffer should be at least MTU + header size (typically 1500+ bytes).
|
||||
///
|
||||
/// The tun crate's `Read` impl transparently strips the macOS utun
|
||||
/// packet information header, so this returns a raw IP packet on all
|
||||
/// platforms.
|
||||
/// packet information header, and on FreeBSD (TUNSIFHEAD) the
|
||||
/// equivalent 4-byte address-family prefix is stripped here, so this
|
||||
/// returns a raw IP packet on all platforms. `Ok(0)` means the frame
|
||||
/// carried no payload; callers should skip it.
|
||||
///
|
||||
/// The raw `io::Error` is returned so callers can inspect `ErrorKind`
|
||||
/// (e.g. `WouldBlock`) or `raw_os_error()` without string matching.
|
||||
pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||
self.device.read(buf)
|
||||
let n = self.device.read(buf)?;
|
||||
// FreeBSD tun in TUNSIFHEAD mode prefixes every frame with its
|
||||
// address family. Strip it here so callers see the same raw-IP
|
||||
// contract as on Linux and macOS. Non-IPv6 families pass through
|
||||
// stripped — matching macOS, where the tun crate does the same —
|
||||
// and are dropped by the IPv6 version check in handle_tun_packet.
|
||||
#[cfg(target_os = "freebsd")]
|
||||
let n = match parse_utun_af_prefix(&buf[..n]) {
|
||||
Some(_) if n > 4 => {
|
||||
buf.copy_within(4..n, 0);
|
||||
n - 4
|
||||
}
|
||||
// Header-only or truncated frame: no payload.
|
||||
_ => 0,
|
||||
};
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Shutdown and delete the TUN device.
|
||||
@@ -364,35 +404,34 @@ impl TunDevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS utun protocol family value for IPv6 (matches `<sys/socket.h>`
|
||||
/// `AF_INET6` on Darwin). Used as the 4-byte big-endian packet-info
|
||||
/// header prepended to every utun frame.
|
||||
#[cfg(target_os = "macos")]
|
||||
const UTUN_AF_INET6: u32 = 30;
|
||||
/// Address-family value for IPv6 in the 4-byte packet-info header used
|
||||
/// by macOS `utun` and FreeBSD `tun` (TUNSIFHEAD) devices: the target's
|
||||
/// `AF_INET6` — 30 on Darwin, 28 on FreeBSD.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
const UTUN_AF_INET6: u32 = libc::AF_INET6 as u32;
|
||||
|
||||
/// Build the 4-byte big-endian utun packet-info header for an IPv6 frame.
|
||||
/// Build the 4-byte big-endian packet-info header for an IPv6 frame.
|
||||
///
|
||||
/// utun devices on macOS require a 4-byte address-family prefix on every
|
||||
/// frame: a single big-endian `u32` carrying the protocol family. For
|
||||
/// IPv6 traffic (the only family FIPS sends) this is `AF_INET6 = 30`,
|
||||
/// which serializes as `[0x00, 0x00, 0x00, 0x1e]`.
|
||||
#[cfg(target_os = "macos")]
|
||||
/// macOS utun and FreeBSD tun (TUNSIFHEAD) devices require a 4-byte
|
||||
/// address-family prefix on every frame: a single big-endian `u32`
|
||||
/// carrying the protocol family. For IPv6 traffic (the only family FIPS
|
||||
/// sends) this is the target's `AF_INET6`.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
#[inline]
|
||||
fn utun_af_inet6_header() -> [u8; 4] {
|
||||
UTUN_AF_INET6.to_be_bytes()
|
||||
}
|
||||
|
||||
/// Parse the 4-byte big-endian utun packet-info header.
|
||||
/// Parse the 4-byte big-endian packet-info header.
|
||||
///
|
||||
/// Returns the address-family value (`AF_INET6 = 30` for IPv6 frames),
|
||||
/// or `None` if the buffer is shorter than the 4-byte header. The `tun`
|
||||
/// crate's `Read` impl strips this transparently for us in the read
|
||||
/// path; this helper exists for round-trip testability with
|
||||
/// [`utun_af_inet6_header`] and for any future code path that reads
|
||||
/// from the dup'd fd directly.
|
||||
#[cfg(target_os = "macos")]
|
||||
/// Returns the address-family value, or `None` if the buffer is shorter
|
||||
/// than the 4-byte header. On FreeBSD [`TunDevice::read_packet`] strips
|
||||
/// the prefix with this; on macOS the `tun` crate's `Read` impl strips
|
||||
/// it before we see the frame, so there the helper is exercised only by
|
||||
/// the round-trip tests below.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
fn parse_utun_af_prefix(buf: &[u8]) -> Option<u32> {
|
||||
if buf.len() < 4 {
|
||||
return None;
|
||||
@@ -421,7 +460,7 @@ impl TunWriter {
|
||||
///
|
||||
/// Blocks forever, reading packets from the channel and writing them
|
||||
/// to the TUN device. Returns when the channel is closed (all senders dropped).
|
||||
#[cfg_attr(target_os = "macos", allow(unused_mut))]
|
||||
#[cfg_attr(any(target_os = "macos", target_os = "freebsd"), allow(unused_mut))]
|
||||
pub fn run(mut self) {
|
||||
use super::tcp_mss::clamp_tcp_mss;
|
||||
|
||||
@@ -445,11 +484,12 @@ impl TunWriter {
|
||||
);
|
||||
}
|
||||
|
||||
// On macOS, utun devices require a 4-byte packet information header
|
||||
// prepended to each packet. The tun crate handles this for its own
|
||||
// On macOS (utun) and FreeBSD (tun with TUNSIFHEAD), the device
|
||||
// requires a 4-byte network-order address-family header prepended
|
||||
// to each packet. The tun crate handles this for its own
|
||||
// Read/Write impl, but we use a dup'd fd directly. We use writev
|
||||
// to avoid allocating a buffer on every packet.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
let write_result = {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let af_header = utun_af_inet6_header();
|
||||
@@ -478,7 +518,7 @@ impl TunWriter {
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
let write_result = self.file.write_all(&packet);
|
||||
|
||||
if let Err(e) = write_result {
|
||||
@@ -507,7 +547,7 @@ impl TunWriter {
|
||||
/// This is designed to run in a dedicated thread since TUN reads are blocking.
|
||||
/// The loop exits when the TUN interface is deleted (EFAULT) or an unrecoverable
|
||||
/// error occurs.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
|
||||
#[cfg(unix)]
|
||||
pub fn run_tun_reader(
|
||||
mut device: TunDevice,
|
||||
@@ -551,10 +591,10 @@ pub fn run_tun_reader(
|
||||
///
|
||||
/// Used to ensure the shutdown pipe read-end is always closed when
|
||||
/// `run_tun_reader` returns, regardless of which exit path is taken.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
struct ShutdownFd(std::os::unix::io::RawFd);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
impl Drop for ShutdownFd {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
@@ -563,12 +603,14 @@ impl Drop for ShutdownFd {
|
||||
}
|
||||
}
|
||||
|
||||
/// TUN packet reader loop (macOS).
|
||||
/// TUN packet reader loop (macOS / FreeBSD).
|
||||
///
|
||||
/// Uses `select()` to multiplex between the TUN fd and a shutdown pipe,
|
||||
/// avoiding the need to close the TUN fd externally (which would cause a
|
||||
/// double-close when `TunDevice` drops).
|
||||
#[cfg(target_os = "macos")]
|
||||
/// double-close when `TunDevice` drops). Both platforms need the pipe
|
||||
/// because downing the interface does not reliably unblock a thread
|
||||
/// parked in a blocking TUN read on either of them.
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_tun_reader(
|
||||
mut device: TunDevice,
|
||||
@@ -642,13 +684,19 @@ pub fn run_tun_reader(
|
||||
return; // _shutdown_fd closes on drop
|
||||
}
|
||||
}
|
||||
Ok(_) => break, // No more data
|
||||
// No more data, or an empty/skipped frame — either way
|
||||
// fall back to select for the next readable event.
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock {
|
||||
break; // Done for this select round
|
||||
}
|
||||
// EBADF is expected during shutdown when the fd is closed
|
||||
if e.raw_os_error() != Some(libc::EBADF) {
|
||||
// EBADF is expected during shutdown when the fd is
|
||||
// closed; ENXIO when the interface was destroyed out
|
||||
// from under the fd (FreeBSD).
|
||||
if e.raw_os_error() != Some(libc::EBADF)
|
||||
&& e.raw_os_error() != Some(libc::ENXIO)
|
||||
{
|
||||
error!(name = %name, error = %e, "TUN read error");
|
||||
}
|
||||
return; // _shutdown_fd closes on drop
|
||||
@@ -1362,13 +1410,14 @@ mod platform {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
mod platform {
|
||||
use super::TunError;
|
||||
use std::net::Ipv6Addr;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// Check if IPv6 is disabled system-wide.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn is_ipv6_disabled() -> bool {
|
||||
// macOS: check via sysctl; if the key doesn't exist, IPv6 is enabled
|
||||
std::process::Command::new("sysctl")
|
||||
@@ -1378,6 +1427,20 @@ mod platform {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if IPv6 is disabled system-wide.
|
||||
#[cfg(target_os = "freebsd")]
|
||||
pub fn is_ipv6_disabled() -> bool {
|
||||
// kern.features.inet6 is 1 when the kernel has IPv6 support. On a
|
||||
// kernel built without INET6 the OID is absent entirely and sysctl
|
||||
// exits nonzero, so a failed lookup means disabled. Only a failure
|
||||
// to run sysctl at all assumes enabled.
|
||||
std::process::Command::new("sysctl")
|
||||
.args(["-n", "kern.features.inet6"])
|
||||
.output()
|
||||
.map(|o| !o.status.success() || String::from_utf8_lossy(&o.stdout).trim() != "1")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if a network interface already exists.
|
||||
pub async fn interface_exists(name: &str) -> bool {
|
||||
Command::new("ifconfig")
|
||||
@@ -1392,9 +1455,11 @@ mod platform {
|
||||
|
||||
/// Shut down a network interface by name.
|
||||
///
|
||||
/// On macOS, utun devices are automatically destroyed when the file
|
||||
/// descriptor is closed. Bringing the interface down causes any
|
||||
/// blocking reads to return an error, which unblocks the reader thread.
|
||||
/// utun (macOS) and devfs-cloned tun (FreeBSD) devices are
|
||||
/// automatically destroyed by the kernel when the last file
|
||||
/// descriptor closes. Bringing the interface down causes any
|
||||
/// blocking reads to return an error, which unblocks the reader
|
||||
/// thread.
|
||||
pub async fn delete_interface(name: &str) -> Result<(), TunError> {
|
||||
run_cmd("ifconfig", &[name, "down"]).await
|
||||
}
|
||||
@@ -1408,6 +1473,12 @@ mod platform {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// FreeBSD: a manually-configured interface with no autoconf
|
||||
// link-local comes up with ND6_IFF_IFDISABLED set, which silently
|
||||
// drops all IPv6.
|
||||
#[cfg(target_os = "freebsd")]
|
||||
run_cmd("ifconfig", &[name, "inet6", "-ifdisabled"]).await?;
|
||||
|
||||
// Set MTU
|
||||
run_cmd("ifconfig", &[name, "mtu", &mtu.to_string()]).await?;
|
||||
|
||||
@@ -1415,6 +1486,7 @@ mod platform {
|
||||
run_cmd("ifconfig", &[name, "up"]).await?;
|
||||
|
||||
// Add route for fd00::/8 (FIPS address space) via this interface
|
||||
#[cfg(target_os = "macos")]
|
||||
run_cmd(
|
||||
"route",
|
||||
&[
|
||||
@@ -1429,6 +1501,25 @@ mod platform {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// FreeBSD: a leftover route from a previous run is not an error.
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
let output = Command::new("route")
|
||||
.args(["-6", "add", "fd00::/8", "-interface", name])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| TunError::Configure(format!("route failed: {}", e)))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.contains("File exists") && !stderr.contains("already in table") {
|
||||
return Err(TunError::Configure(format!(
|
||||
"route -6 add fd00::/8 failed: {}",
|
||||
stderr.trim()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1656,32 +1747,41 @@ mod tests {
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// macOS utun packet-info header (AF_INET6 4-byte big-endian prefix)
|
||||
// macOS utun / FreeBSD TUNSIFHEAD packet-info header (4-byte big-endian
|
||||
// AF prefix)
|
||||
//
|
||||
// These tests are pure-data byte-buffer manipulation and require no
|
||||
// privilege, no actual TUN device, no system calls. They pin the wire
|
||||
// format that `TunWriter::run` emits ahead of every IPv6 frame on the
|
||||
// dup'd utun fd, and the inverse parse used for round-trip checking.
|
||||
// dup'd fd, and the inverse parse `read_packet` uses on FreeBSD.
|
||||
// ========================================================================
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_utun_header {
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
mod utun_header {
|
||||
use super::super::{UTUN_AF_INET6, parse_utun_af_prefix, utun_af_inet6_header};
|
||||
|
||||
#[test]
|
||||
fn af_inet6_constant_matches_darwin() {
|
||||
// Darwin's <sys/socket.h> defines AF_INET6 = 30. If this ever
|
||||
// diverges, every utun write FIPS issues will be misclassified
|
||||
// by the kernel and dropped.
|
||||
fn af_inet6_constant_matches_platform() {
|
||||
// <sys/socket.h> defines AF_INET6 = 30 on Darwin and 28 on
|
||||
// FreeBSD. If this ever diverges, every TUN write FIPS issues
|
||||
// will be misclassified by the kernel and dropped, and every
|
||||
// inbound frame will be skipped as non-IPv6.
|
||||
#[cfg(target_os = "macos")]
|
||||
assert_eq!(UTUN_AF_INET6, 30);
|
||||
#[cfg(target_os = "freebsd")]
|
||||
assert_eq!(UTUN_AF_INET6, 28);
|
||||
assert_eq!(libc::AF_INET6 as u32, UTUN_AF_INET6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_produces_big_endian_af_inet6() {
|
||||
// The kernel reads the 4-byte prefix as a big-endian u32.
|
||||
// 30 == 0x0000001e, so the wire bytes are [0, 0, 0, 0x1e].
|
||||
// The kernel reads the 4-byte prefix as a big-endian u32:
|
||||
// 30 == 0x0000001e (Darwin), 28 == 0x0000001c (FreeBSD).
|
||||
let header = utun_af_inet6_header();
|
||||
#[cfg(target_os = "macos")]
|
||||
assert_eq!(header, [0x00, 0x00, 0x00, 0x1e]);
|
||||
#[cfg(target_os = "freebsd")]
|
||||
assert_eq!(header, [0x00, 0x00, 0x00, 0x1c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user