Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20bf14e91c | ||
|
|
e860731c01 | ||
|
|
f556b60ce4 | ||
|
|
422726f1c8 | ||
|
|
dd021d8642 | ||
|
|
f20c72a829 | ||
|
|
44cd17cf84 | ||
|
|
fb0a21e5e6 | ||
|
|
66b937f710 | ||
|
|
524aed7fa1 | ||
|
|
11e3fdeae6 | ||
|
|
636c45b3d7 | ||
|
|
f602687d93 | ||
|
|
b8b0fda1e0 | ||
|
|
9a3fae9a0c | ||
|
|
a31ac36957 | ||
|
|
9001b14fed | ||
|
|
63ac69a222 | ||
|
|
1f6bdff8f8 | ||
|
|
643d6b01e1 | ||
|
|
17c8e70aa3 | ||
|
|
389ac09907 | ||
|
|
5308e9648c | ||
|
|
819614fa7d | ||
|
|
fab8b698d8 | ||
|
|
a6f23a5ddb | ||
|
|
27dfaab360 |
19
.SRCINFO
Normal file
19
.SRCINFO
Normal file
@@ -0,0 +1,19 @@
|
||||
pkgbase = numa-git
|
||||
pkgdesc = Portable DNS resolver in Rust — .numa local domains, ad blocking, developer overrides, DNS-over-HTTPS
|
||||
pkgver = 0.10.1.r0.g0000000
|
||||
pkgrel = 1
|
||||
url = https://github.com/razvandimescu/numa
|
||||
arch = x86_64
|
||||
license = MIT
|
||||
options = !lto
|
||||
makedepends = cargo
|
||||
makedepends = git
|
||||
depends = gcc-libs
|
||||
depends = glibc
|
||||
provides = numa
|
||||
conflicts = numa
|
||||
backup = etc/numa.toml
|
||||
source = numa::git+https://github.com/razvandimescu/numa.git
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = numa-git
|
||||
34
.github/dependabot.yml
vendored
Normal file
34
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
check-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: clippy
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
check-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: build
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: test
|
||||
run: cargo test
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: numa-windows-x86_64
|
||||
path: target/debug/numa.exe
|
||||
|
||||
19
.github/workflows/homebrew-bump.yml
vendored
19
.github/workflows/homebrew-bump.yml
vendored
@@ -1,8 +1,12 @@
|
||||
name: Bump Homebrew Tap
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to bump (e.g. 0.10.0 or v0.10.0)'
|
||||
type: string
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -16,17 +20,14 @@ jobs:
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Determine version
|
||||
id: ver
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
V="${{ github.event.release.tag_name }}"
|
||||
else
|
||||
V="${{ github.event.inputs.version }}"
|
||||
fi
|
||||
V="${V#v}"
|
||||
V="${INPUT_VERSION#v}"
|
||||
echo "version=$V" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fetch sha256 checksums from release assets
|
||||
|
||||
159
.github/workflows/publish-aur.yml
vendored
Normal file
159
.github/workflows/publish-aur.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
# `publish-aur.yml` - Arch Linux AUR Package Workflow
|
||||
# --------------------
|
||||
# This workflow automates the validation and publishing of the 'numa-git' package to the
|
||||
# Arch User Repository (AUR). The AUR is a community-driven repository for Arch Linux users.
|
||||
#
|
||||
# Workflow Overview:
|
||||
# 1. Validate: Builds and tests the package for Arch Linux x86_64 using a clean
|
||||
# Arch Linux container.
|
||||
# 2. Audit: Checks Rust dependencies for known security vulnerabilities using
|
||||
# 'cargo-audit'.
|
||||
# 3. Publish: If on the 'main' branch, it pushes the updated PKGBUILD and
|
||||
# .SRCINFO to the AUR.
|
||||
#
|
||||
# Security Best Practices:
|
||||
# - SHA Pinning: All GitHub Actions are pinned to a full-length commit SHA (e.g., v6.0.2 @ SHA)
|
||||
# to ensure the code is immutable and protects against supply-chain attacks where a tag
|
||||
# might be maliciously moved to a compromised commit.
|
||||
# - SSH Hygiene: Uses ssh-agent to keep the private key in memory rather than on disk.
|
||||
# - Audit: Runs 'cargo audit' to prevent publishing known vulnerable dependencies.
|
||||
|
||||
name: Publish - Arch Linux AUR Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# The 'validate' job ensures that the PKGBUILD is correct and the software builds/tests
|
||||
# successfully on Arch Linux before we attempt to publish it.
|
||||
validate:
|
||||
name: Validate PKGBUILD (${{ matrix.arch }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x86_64]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build and Test Package
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
AUR_PKGNAME: ${{ secrets.AUR_PACKAGE_NAME }}
|
||||
run: |
|
||||
# We use a temporary directory to avoid Docker permission issues with the workspace.
|
||||
mkdir -p build-dir
|
||||
cp PKGBUILD build-dir/
|
||||
|
||||
docker run --rm -v $PWD/build-dir:/pkg -w /pkg archlinux:latest /bin/bash -c "
|
||||
# ARCH LINUX SECURITY REQUIREMENT:
|
||||
# 'makepkg' (the tool that builds Arch packages) refuses to run as root for safety.
|
||||
# We must create a standard user and give them sudo access.
|
||||
|
||||
# Install build-time dependencies.
|
||||
# 'base-devel' includes essential tools like gcc, make, and binutils.
|
||||
# Install 'rust' directly to avoid the interactive virtual-package
|
||||
# prompt for 'cargo' on current Arch images.
|
||||
pacman -Syu --noconfirm --needed base-devel rust git sudo cargo-audit
|
||||
|
||||
useradd -m builduser
|
||||
chown -R builduser:builduser /pkg
|
||||
|
||||
# Allow the build user to install dependencies during the build process.
|
||||
echo 'builduser ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builduser
|
||||
|
||||
# Fetch the source tree first so pkgver() and cargo-audit have a
|
||||
# real Cargo.lock to inspect.
|
||||
sudo -u builduser makepkg -o --nobuild --nocheck --nodeps --noprepare
|
||||
|
||||
# SECURITY AUDIT:
|
||||
# Fail early if any dependencies have known security vulnerabilities.
|
||||
sudo -u builduser sh -lc 'cd /pkg/src/numa && cargo audit'
|
||||
|
||||
# BUILD & TEST:
|
||||
# 'makepkg -s' will:
|
||||
# 1. Download source files (cloning this repo)
|
||||
# 2. Run prepare(), build(), and check() (running cargo test)
|
||||
# 3. Create the final .pkg.tar.zst package
|
||||
sudo -u builduser makepkg -s --noconfirm
|
||||
"
|
||||
|
||||
# The 'publish' job updates the AUR repository with our latest PKGBUILD and .SRCINFO.
|
||||
publish:
|
||||
name: Publish to AUR
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# Securely configure SSH for AUR access.
|
||||
- name: Configure SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
# Official AUR Ed25519 fingerprint (prevents Man-in-the-Middle attacks).
|
||||
echo "aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN" >> ~/.ssh/known_hosts
|
||||
|
||||
# Use ssh-agent to keep the private key in memory rather than writing it to disk.
|
||||
eval $(ssh-agent -s)
|
||||
echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" | tr -d '\r' | ssh-add -
|
||||
|
||||
# Export the agent socket so subsequent 'git' commands can use it.
|
||||
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
|
||||
echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV
|
||||
|
||||
- name: Push to AUR
|
||||
env:
|
||||
AUR_PKGNAME: ${{ secrets.AUR_PACKAGE_NAME }}
|
||||
AUR_EMAIL: ${{ secrets.AUR_EMAIL }}
|
||||
AUR_USER: ${{ secrets.AUR_USERNAME }}
|
||||
run: |
|
||||
# AUR repos are managed via Git. Each package has its own repo at:
|
||||
# ssh://aur@aur.archlinux.org/<package-name>.git
|
||||
git clone ssh://aur@aur.archlinux.org/$AUR_PKGNAME.git aur-repo
|
||||
|
||||
cp PKGBUILD aur-repo/
|
||||
cd aur-repo
|
||||
|
||||
# METADATA GENERATION:
|
||||
# '.SRCINFO' is a machine-readable version of the PKGBUILD.
|
||||
# We must run this as a non-root user ('builduser') inside the container.
|
||||
docker run --rm -v $(pwd):/pkg archlinux:latest /bin/bash -c "
|
||||
pacman -Syu --noconfirm --needed binutils git sudo
|
||||
useradd -m builduser
|
||||
chown -R builduser:builduser /pkg
|
||||
cd /pkg
|
||||
sudo -u builduser git config --global --add safe.directory '*'
|
||||
# makepkg -od fetches the source first so pkgver() can calculate the version.
|
||||
# --noprepare skips the prepare() function, which invokes cargo and would
|
||||
# otherwise require a full rust toolchain in this metadata-only container.
|
||||
# pkgver() runs before prepare(), so .SRCINFO still gets the correct version.
|
||||
sudo -u builduser makepkg -od --noprepare && sudo -u builduser makepkg --printsrcinfo > .SRCINFO
|
||||
"
|
||||
|
||||
# Reclaim ownership: the in-container 'chown -R builduser:builduser /pkg'
|
||||
# propagates through the bind mount, leaving .git/ owned by the container's
|
||||
# builduser UID. Without this, subsequent 'git config' on the host fails with
|
||||
# "could not lock config file .git/config: Permission denied".
|
||||
sudo chown -R "$(id -u):$(id -g)" .
|
||||
|
||||
# Set the commit identity using secrets for security and auditability.
|
||||
git config user.name "$AUR_USER"
|
||||
git config user.email "$AUR_EMAIL"
|
||||
|
||||
# Stage and commit both the human-readable PKGBUILD and machine-readable .SRCINFO.
|
||||
git add PKGBUILD .SRCINFO
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "chore: update PKGBUILD to ${{ github.sha }}"
|
||||
git push origin master
|
||||
else
|
||||
echo "No changes to commit (metadata and PKGBUILD are already up-to-date)."
|
||||
fi
|
||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
(Get-FileHash "${{ matrix.name }}.zip" -Algorithm SHA256).Hash.ToLower() + " ${{ matrix.name }}.zip" | Out-File "${{ matrix.name }}.zip.sha256" -Encoding ascii
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: |
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
needs: [build, publish]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
@@ -108,3 +108,10 @@ jobs:
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.sha256
|
||||
|
||||
bump-homebrew:
|
||||
needs: release
|
||||
uses: ./.github/workflows/homebrew-bump.yml
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
|
||||
8
.github/workflows/static.yml
vendored
8
.github/workflows/static.yml
vendored
@@ -30,18 +30,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install pandoc
|
||||
run: sudo apt-get install -y pandoc
|
||||
- name: Generate blog HTML
|
||||
run: make blog
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v6
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './site'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/target
|
||||
/build-dir
|
||||
CLAUDE.md
|
||||
docs/
|
||||
site/blog/posts/
|
||||
|
||||
241
Cargo.lock
generated
241
Cargo.lock
generated
@@ -17,6 +17,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloca"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
@@ -75,18 +84,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.0"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6"
|
||||
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.6.2"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
|
||||
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
@@ -94,15 +103,15 @@ dependencies = [
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -368,25 +377,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
|
||||
dependencies = [
|
||||
"alloca",
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"page_size",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
@@ -394,9 +402,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
@@ -441,9 +449,9 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "9.0.0"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
|
||||
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
@@ -702,12 +710,6 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -755,9 +757,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -770,7 +772,6 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
@@ -810,7 +811,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -944,17 +945,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -963,9 +953,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -1143,7 +1133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "numa"
|
||||
version = "0.10.1"
|
||||
version = "0.10.3"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"axum",
|
||||
@@ -1162,7 +1152,7 @@ dependencies = [
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"socket2 0.5.10",
|
||||
"socket2",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
@@ -1172,9 +1162,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.7.1"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
|
||||
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
]
|
||||
@@ -1197,6 +1187,16 @@ version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
@@ -1219,12 +1219,6 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
@@ -1314,8 +1308,8 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.3",
|
||||
"thiserror 2.0.18",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -1336,7 +1330,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -1351,7 +1345,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.3",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
@@ -1422,9 +1416,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.13.2"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
|
||||
checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
@@ -1655,11 +1649,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1698,16 +1692,6 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
@@ -1761,33 +1745,13 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1869,24 +1833,24 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.50.0"
|
||||
version = "1.51.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.1"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1918,44 +1882,42 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -2188,6 +2150,22 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
@@ -2197,6 +2175,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
@@ -2361,12 +2345,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
@@ -2382,9 +2363,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.16.0"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
|
||||
checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"data-encoding",
|
||||
@@ -2394,7 +2375,7 @@ dependencies = [
|
||||
"oid-registry",
|
||||
"ring",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "numa"
|
||||
version = "0.10.1"
|
||||
version = "0.10.3"
|
||||
authors = ["razvandimescu <razvan@dimescu.com>"]
|
||||
edition = "2021"
|
||||
description = "Portable DNS resolver in Rust — .numa local domains, ad blocking, developer overrides, DNS-over-HTTPS"
|
||||
@@ -14,7 +14,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time",
|
||||
axum = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
toml = "1.1"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
reqwest = { version = "0.12", features = ["rustls-tls", "gzip", "http2"], default-features = false }
|
||||
@@ -22,8 +22,8 @@ hyper = { version = "1", features = ["client", "http1", "server"] }
|
||||
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "tokio"] }
|
||||
http-body-util = "0.1"
|
||||
futures = "0.3"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
rcgen = { version = "0.13", features = ["pem", "x509-parser"] }
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
rcgen = { version = "0.14", features = ["pem", "x509-parser"] }
|
||||
time = "0.3"
|
||||
rustls = "0.23"
|
||||
tokio-rustls = "0.26"
|
||||
@@ -32,7 +32,7 @@ ring = "0.17"
|
||||
rustls-pemfile = "2.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
http = "1"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.88-alpine AS builder
|
||||
FROM rust:1.94-alpine AS builder
|
||||
RUN apk add --no-cache musl-dev cmake make perl
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@@ -11,7 +11,7 @@ COPY numa.toml com.numa.dns.plist numa.service ./
|
||||
RUN touch src/main.rs src/lib.rs
|
||||
RUN cargo build --release
|
||||
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.23
|
||||
COPY --from=builder /app/target/release/numa /usr/local/bin/numa
|
||||
EXPOSE 53/udp 80/tcp 443/tcp 853/tcp 5380/tcp
|
||||
ENTRYPOINT ["numa"]
|
||||
|
||||
62
PKGBUILD
Normal file
62
PKGBUILD
Normal file
@@ -0,0 +1,62 @@
|
||||
# Maintainer: razvandimescu <razvan@dimescu.com>
|
||||
pkgname=numa-git
|
||||
_pkgname=numa
|
||||
pkgver=0.10.1.r0.g0000000 # Placeholder — pkgver() rewrites this on each makepkg run
|
||||
pkgrel=1
|
||||
pkgdesc="Portable DNS resolver in Rust — .numa local domains, ad blocking, developer overrides, DNS-over-HTTPS"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/razvandimescu/numa"
|
||||
license=('MIT')
|
||||
options=('!lto')
|
||||
depends=('gcc-libs' 'glibc')
|
||||
makedepends=('cargo' 'git')
|
||||
provides=("$_pkgname")
|
||||
conflicts=("$_pkgname")
|
||||
backup=('etc/numa.toml')
|
||||
source=("$_pkgname::git+$url.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
( set -o pipefail
|
||||
git describe --long --tags 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' ||
|
||||
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
) | sed 's/^v//'
|
||||
}
|
||||
|
||||
prepare() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
# numa v0.10.1+ uses FHS-compliant paths on Linux by default
|
||||
# (/var/lib/numa for data, journalctl for logs), so no source
|
||||
# patching is needed. The earlier sed targeted /usr/local/bin/numa,
|
||||
# which only appears in a comment in current main.
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cargo fetch --locked
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cargo build --frozen --release
|
||||
}
|
||||
|
||||
check() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cargo test --frozen
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
install -Dm755 "target/release/$_pkgname" "$pkgdir/usr/bin/$_pkgname"
|
||||
|
||||
# numa.service uses {{exe_path}} as a placeholder substituted by
|
||||
# `numa install` at runtime via replace_exe_path(). For an AUR
|
||||
# package install (no `numa install` step), we substitute it
|
||||
# statically here so systemd gets a real ExecStart path.
|
||||
sed 's|{{exe_path}}|/usr/bin/numa /etc/numa.toml|g' numa.service > numa.service.patched
|
||||
install -Dm644 "numa.service.patched" "$pkgdir/usr/lib/systemd/system/numa.service"
|
||||
|
||||
install -Dm644 "numa.toml" "$pkgdir/etc/numa.toml"
|
||||
install -Dm644 "LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
@@ -21,6 +21,9 @@ brew install razvandimescu/tap/numa
|
||||
# Linux
|
||||
curl -fsSL https://raw.githubusercontent.com/razvandimescu/numa/main/install.sh | sh
|
||||
|
||||
# Arch Linux (AUR)
|
||||
yay -S numa-git
|
||||
|
||||
# Windows — download from GitHub Releases
|
||||
# All platforms
|
||||
cargo install numa
|
||||
|
||||
237
src/buffer.rs
237
src/buffer.rs
@@ -84,6 +84,11 @@ impl BytePacketBuffer {
|
||||
|
||||
/// Read a qname, handling label compression (pointer jumps).
|
||||
/// Converts wire format like [3]www[6]google[3]com[0] into "www.google.com".
|
||||
///
|
||||
/// Label bytes are escaped per RFC 1035 §5.1:
|
||||
/// - literal `.` within a label → `\.`
|
||||
/// - literal `\` → `\\`
|
||||
/// - bytes outside `0x21..=0x7E` (excluding `.` and `\`) → `\DDD` (3-digit decimal)
|
||||
pub fn read_qname(&mut self, outstr: &mut String) -> Result<()> {
|
||||
let mut pos = self.pos();
|
||||
let mut jumped = false;
|
||||
@@ -121,7 +126,18 @@ impl BytePacketBuffer {
|
||||
|
||||
let str_buffer = self.get_range(pos, len as usize)?;
|
||||
for &b in str_buffer {
|
||||
outstr.push(b.to_ascii_lowercase() as char);
|
||||
let c = b.to_ascii_lowercase();
|
||||
match c {
|
||||
b'.' => outstr.push_str("\\."),
|
||||
b'\\' => outstr.push_str("\\\\"),
|
||||
0x21..=0x7E => outstr.push(c as char),
|
||||
_ => {
|
||||
outstr.push('\\');
|
||||
outstr.push((b'0' + c / 100) as char);
|
||||
outstr.push((b'0' + (c / 10) % 10) as char);
|
||||
outstr.push((b'0' + c % 10) as char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delim = ".";
|
||||
@@ -163,24 +179,68 @@ impl BytePacketBuffer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a qname in wire format, parsing RFC 1035 §5.1 text escapes.
|
||||
/// See `read_qname` for the escape grammar.
|
||||
pub fn write_qname(&mut self, qname: &str) -> Result<()> {
|
||||
if qname.is_empty() || qname == "." {
|
||||
self.write_u8(0)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for label in qname.split('.') {
|
||||
let len = label.len();
|
||||
if len == 0 {
|
||||
continue; // skip empty labels from trailing dot
|
||||
let bytes = qname.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
let len_pos = self.pos;
|
||||
self.write_u8(0)?; // placeholder length byte, backpatched below
|
||||
let body_start = self.pos;
|
||||
|
||||
while i < bytes.len() && bytes[i] != b'.' {
|
||||
let b = bytes[i];
|
||||
if b == b'\\' {
|
||||
i += 1;
|
||||
let c1 = *bytes.get(i).ok_or("trailing backslash in qname")?;
|
||||
if c1.is_ascii_digit() {
|
||||
let c2 = *bytes
|
||||
.get(i + 1)
|
||||
.ok_or("invalid \\DDD escape: expected 3 digits")?;
|
||||
let c3 = *bytes
|
||||
.get(i + 2)
|
||||
.ok_or("invalid \\DDD escape: expected 3 digits")?;
|
||||
if !c2.is_ascii_digit() || !c3.is_ascii_digit() {
|
||||
return Err("invalid \\DDD escape: expected 3 digits".into());
|
||||
}
|
||||
if len > 0x3f {
|
||||
return Err("Single label exceeds 63 characters of length".into());
|
||||
let val =
|
||||
(c1 - b'0') as u16 * 100 + (c2 - b'0') as u16 * 10 + (c3 - b'0') as u16;
|
||||
if val > 255 {
|
||||
return Err(format!("\\DDD escape out of range: {}", val).into());
|
||||
}
|
||||
self.write_u8(val as u8)?;
|
||||
i += 3;
|
||||
} else {
|
||||
// \. \\ and any other \X → literal next byte
|
||||
self.write_u8(c1)?;
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
self.write_u8(b)?;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
self.write_u8(len as u8)?;
|
||||
for b in label.as_bytes() {
|
||||
self.write_u8(*b)?;
|
||||
if self.pos - body_start > 0x3f {
|
||||
return Err("Single label exceeds 63 characters of length".into());
|
||||
}
|
||||
}
|
||||
|
||||
let label_len = self.pos - body_start;
|
||||
if label_len == 0 && i < bytes.len() {
|
||||
// Empty label from leading/consecutive dots — roll back the placeholder.
|
||||
self.pos = len_pos;
|
||||
} else {
|
||||
self.set(len_pos, label_len as u8)?;
|
||||
}
|
||||
|
||||
if i < bytes.len() && bytes[i] == b'.' {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,3 +272,160 @@ impl BytePacketBuffer {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn roundtrip(wire: &[u8]) -> String {
|
||||
let mut buf = BytePacketBuffer::from_bytes(wire);
|
||||
let mut out = String::new();
|
||||
buf.read_qname(&mut out).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
fn write_then_read(text: &str) -> String {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname(text).unwrap();
|
||||
let wire_end = buf.pos();
|
||||
buf.seek(0).unwrap();
|
||||
let mut out = String::new();
|
||||
buf.read_qname(&mut out).unwrap();
|
||||
assert_eq!(
|
||||
buf.pos(),
|
||||
wire_end,
|
||||
"reader should consume exactly what writer wrote"
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_plain_domain() {
|
||||
// [3]www[6]google[3]com[0]
|
||||
let wire = b"\x03www\x06google\x03com\x00";
|
||||
assert_eq!(roundtrip(wire), "www.google.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_label_with_literal_dot_is_escaped() {
|
||||
// fanf2's example: [8]exa.mple[3]com[0] — two labels, first contains 0x2E
|
||||
let wire = b"\x08exa.mple\x03com\x00";
|
||||
assert_eq!(roundtrip(wire), "exa\\.mple.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_label_with_backslash_is_escaped() {
|
||||
// [4]a\bc[3]com[0]
|
||||
let wire = b"\x04a\\bc\x03com\x00";
|
||||
assert_eq!(roundtrip(wire), "a\\\\bc.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_label_with_nonprintable_byte_uses_decimal_escape() {
|
||||
// [4]\x00foo[3]com[0] — null byte at label start
|
||||
let wire = b"\x04\x00foo\x03com\x00";
|
||||
assert_eq!(roundtrip(wire), "\\000foo.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_label_with_space_uses_decimal_escape() {
|
||||
// Space (0x20) is outside 0x21..=0x7E, so it must be decimal-escaped.
|
||||
let wire = b"\x05a b c\x00";
|
||||
assert_eq!(roundtrip(wire), "a\\032b\\032c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_plain_domain() {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname("www.google.com").unwrap();
|
||||
assert_eq!(&buf.buf[..buf.pos], b"\x03www\x06google\x03com\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_escaped_dot_does_not_split_label() {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname("exa\\.mple.com").unwrap();
|
||||
assert_eq!(&buf.buf[..buf.pos], b"\x08exa.mple\x03com\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_escaped_backslash() {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname("a\\\\bc.com").unwrap();
|
||||
assert_eq!(&buf.buf[..buf.pos], b"\x04a\\bc\x03com\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_decimal_escape_yields_raw_byte() {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname("\\000foo.com").unwrap();
|
||||
assert_eq!(&buf.buf[..buf.pos], b"\x04\x00foo\x03com\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_skips_empty_labels() {
|
||||
// Leading dot — first (empty) label is rolled back.
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname(".foo.com").unwrap();
|
||||
assert_eq!(&buf.buf[..buf.pos], b"\x03foo\x03com\x00");
|
||||
|
||||
// Consecutive dots — middle empty label is rolled back.
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname("foo..com").unwrap();
|
||||
assert_eq!(&buf.buf[..buf.pos], b"\x03foo\x03com\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_rejects_out_of_range_decimal_escape() {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
assert!(buf.write_qname("\\999foo.com").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_rejects_trailing_backslash() {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
assert!(buf.write_qname("foo\\").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_rejects_short_decimal_escape() {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
assert!(buf.write_qname("\\1").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_rejects_label_over_63_bytes() {
|
||||
// 64 bytes exceeds the wire-format label cap.
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
assert!(buf.write_qname(&"a".repeat(64)).is_err());
|
||||
|
||||
// 63 bytes is the maximum permitted label length.
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
assert!(buf.write_qname(&"a".repeat(63)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_preserves_dot_in_label() {
|
||||
assert_eq!(write_then_read("exa\\.mple.com"), "exa\\.mple.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_preserves_backslash_in_label() {
|
||||
assert_eq!(write_then_read("a\\\\b.com"), "a\\\\b.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_preserves_nonprintable_byte() {
|
||||
assert_eq!(write_then_read("\\000foo.com"), "\\000foo.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_name_empty_and_dot_both_produce_single_zero() {
|
||||
let mut a = BytePacketBuffer::new();
|
||||
a.write_qname("").unwrap();
|
||||
let mut b = BytePacketBuffer::new();
|
||||
b.write_qname(".").unwrap();
|
||||
assert_eq!(&a.buf[..a.pos], b"\x00");
|
||||
assert_eq!(&b.buf[..b.pos], b"\x00");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use log::{debug, trace};
|
||||
use ring::digest;
|
||||
use ring::signature;
|
||||
|
||||
use crate::buffer::BytePacketBuffer;
|
||||
use crate::cache::{DnsCache, DnssecStatus};
|
||||
use crate::packet::DnsPacket;
|
||||
use crate::question::QueryType;
|
||||
@@ -720,22 +721,29 @@ pub fn verify_ds(ds: &DnsRecord, dnskey: &DnsRecord, owner: &str) -> bool {
|
||||
|
||||
// -- Canonical wire format --
|
||||
|
||||
/// Encode a DNS name in canonical wire form per RFC 4034 §6.2:
|
||||
/// uncompressed, with ASCII letters lowercased.
|
||||
///
|
||||
/// Lowercasing happens *after* escape resolution because `\065` yields
|
||||
/// `'A'`, which canonical form must convert to `'a'`.
|
||||
pub fn name_to_wire(name: &str) -> Vec<u8> {
|
||||
let mut wire = Vec::with_capacity(name.len() + 2);
|
||||
if name == "." || name.is_empty() {
|
||||
wire.push(0);
|
||||
return wire;
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
buf.write_qname(name)
|
||||
.expect("name_to_wire: input must parse as a valid DNS name");
|
||||
let mut wire = buf.filled().to_vec();
|
||||
|
||||
let mut i = 0;
|
||||
while i < wire.len() {
|
||||
let label_len = wire[i] as usize;
|
||||
if label_len == 0 {
|
||||
break;
|
||||
}
|
||||
for label in name.split('.') {
|
||||
if label.is_empty() {
|
||||
continue;
|
||||
i += 1;
|
||||
let end = i + label_len;
|
||||
wire[i..end].make_ascii_lowercase();
|
||||
i = end;
|
||||
}
|
||||
wire.push(label.len() as u8);
|
||||
for &b in label.as_bytes() {
|
||||
wire.push(b.to_ascii_lowercase());
|
||||
}
|
||||
}
|
||||
wire.push(0);
|
||||
|
||||
wire
|
||||
}
|
||||
|
||||
@@ -1475,6 +1483,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_to_wire_escaped_dot_in_label_is_not_a_separator() {
|
||||
// `exa\.mple.com` is two labels: `exa.mple` (8 bytes including the 0x2E) and `com`.
|
||||
let wire = name_to_wire("exa\\.mple.com");
|
||||
assert_eq!(
|
||||
wire,
|
||||
vec![8, b'e', b'x', b'a', b'.', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_to_wire_decimal_escape_is_lowercased() {
|
||||
// \065 = 'A', must become 'a' in canonical form.
|
||||
let wire = name_to_wire("\\065bc.com");
|
||||
assert_eq!(wire, vec![3, b'a', b'b', b'c', 3, b'c', b'o', b'm', 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_zone_cases() {
|
||||
assert_eq!(parent_zone("example.com"), "com");
|
||||
|
||||
19
src/main.rs
19
src/main.rs
@@ -223,7 +223,11 @@ async fn main() -> numa::Result<()> {
|
||||
) {
|
||||
Ok(tls_config) => Some(ArcSwap::from(tls_config)),
|
||||
Err(e) => {
|
||||
if let Some(advisory) = numa::tls::try_data_dir_advisory(&e, &resolved_data_dir) {
|
||||
eprint!("{}", advisory);
|
||||
} else {
|
||||
log::warn!("TLS setup failed, HTTPS proxy disabled: {}", e);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -231,8 +235,21 @@ async fn main() -> numa::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let socket = match UdpSocket::bind(&config.server.bind_addr).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
if let Some(advisory) =
|
||||
numa::system_dns::try_port53_advisory(&config.server.bind_addr, &e)
|
||||
{
|
||||
eprint!("{}", advisory);
|
||||
std::process::exit(1);
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
let ctx = Arc::new(ServerCtx {
|
||||
socket: UdpSocket::bind(&config.server.bind_addr).await?,
|
||||
socket,
|
||||
zone_map: build_zone_map(&config.zones)?,
|
||||
cache: RwLock::new(DnsCache::new(
|
||||
config.cache.max_entries,
|
||||
|
||||
@@ -2,6 +2,17 @@ use std::net::SocketAddr;
|
||||
|
||||
use log::info;
|
||||
|
||||
fn print_recursive_hint() {
|
||||
let is_recursive = crate::config::load_config("numa.toml")
|
||||
.map(|c| c.config.upstream.mode == crate::config::UpstreamMode::Recursive)
|
||||
.unwrap_or(false);
|
||||
if !is_recursive {
|
||||
eprintln!(" Want full DNS sovereignty? Add to numa.toml:");
|
||||
eprintln!(" [upstream]");
|
||||
eprintln!(" mode = \"recursive\"\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_loopback_or_stub(addr: &str) -> bool {
|
||||
matches!(addr, "127.0.0.1" | "127.0.0.53" | "0.0.0.0" | "::1" | "")
|
||||
}
|
||||
@@ -46,6 +57,60 @@ pub fn discover_system_dns() -> SystemDnsInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Advisory for port-53 bind failures (EADDRINUSE or EACCES); `None`
|
||||
/// if not applicable so the caller can fall back to the raw error.
|
||||
pub fn try_port53_advisory(bind_addr: &str, err: &std::io::Error) -> Option<String> {
|
||||
if !is_port_53(bind_addr) {
|
||||
return None;
|
||||
}
|
||||
let (title, cause) = match err.kind() {
|
||||
std::io::ErrorKind::AddrInUse => (
|
||||
"port 53 is already in use",
|
||||
"Another process is already bound to port 53. On Linux this is\n \
|
||||
typically systemd-resolved; on Windows, the DNS Client service.",
|
||||
),
|
||||
std::io::ErrorKind::PermissionDenied => (
|
||||
"permission denied",
|
||||
"Port 53 is privileged — binding it requires root on Linux/macOS\n \
|
||||
or Administrator on Windows.",
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
let o = "\x1b[1;38;2;192;98;58m"; // bold orange
|
||||
let r = "\x1b[0m";
|
||||
Some(format!(
|
||||
"
|
||||
{o}Numa{r} — cannot bind to {bind_addr}: {title}.
|
||||
|
||||
{cause}
|
||||
|
||||
Fix — pick one:
|
||||
|
||||
1. Install Numa as the system resolver (frees port 53):
|
||||
|
||||
sudo numa install (on Windows, run as Administrator)
|
||||
|
||||
2. Run on a non-privileged port for testing.
|
||||
Create ~/.config/numa/numa.toml with:
|
||||
|
||||
[server]
|
||||
bind_addr = \"127.0.0.1:5354\"
|
||||
api_port = 5380
|
||||
|
||||
Then run: numa
|
||||
Test with: dig @127.0.0.1 -p 5354 example.com
|
||||
|
||||
"
|
||||
))
|
||||
}
|
||||
|
||||
fn is_port_53(bind_addr: &str) -> bool {
|
||||
bind_addr
|
||||
.parse::<SocketAddr>()
|
||||
.map(|s| s.port() == 53)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn discover_macos() -> SystemDnsInfo {
|
||||
use log::{debug, warn};
|
||||
@@ -174,6 +239,9 @@ fn discover_linux() -> SystemDnsInfo {
|
||||
let default_upstream = if let Some(ns) = upstream {
|
||||
info!("detected system upstream: {}", ns);
|
||||
Some(ns)
|
||||
} else if let Some(ns) = resolvectl_dns_server() {
|
||||
info!("detected system upstream via resolvectl: {}", ns);
|
||||
Some(ns)
|
||||
} else {
|
||||
// Fallback to backup from a previous `numa install`
|
||||
let backup = {
|
||||
@@ -631,9 +699,7 @@ fn install_windows() -> Result<(), String> {
|
||||
} else {
|
||||
eprintln!(" Numa will start automatically on next boot.\n");
|
||||
}
|
||||
eprintln!(" Want full DNS sovereignty? Add to numa.toml:");
|
||||
eprintln!(" [upstream]");
|
||||
eprintln!(" mode = \"recursive\"\n");
|
||||
print_recursive_hint();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1124,9 +1190,7 @@ fn install_service_macos() -> Result<(), String> {
|
||||
eprintln!(" Numa will auto-start on boot and restart if killed.");
|
||||
eprintln!(" Logs: /usr/local/var/log/numa.log");
|
||||
eprintln!(" Run 'sudo numa uninstall' to restore original DNS.\n");
|
||||
eprintln!(" Want full DNS sovereignty? Add to numa.toml:");
|
||||
eprintln!(" [upstream]");
|
||||
eprintln!(" mode = \"recursive\"\n");
|
||||
print_recursive_hint();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1331,9 +1395,7 @@ fn install_service_linux() -> Result<(), String> {
|
||||
eprintln!(" Numa will auto-start on boot and restart if killed.");
|
||||
eprintln!(" Logs: journalctl -u numa -f");
|
||||
eprintln!(" Run 'sudo numa uninstall' to restore original DNS.\n");
|
||||
eprintln!(" Want full DNS sovereignty? Add to numa.toml:");
|
||||
eprintln!(" [upstream]");
|
||||
eprintln!(" mode = \"recursive\"\n");
|
||||
print_recursive_hint();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1753,4 +1815,43 @@ Wireless LAN adapter Wi-Fi:
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.contains_key("Wi-Fi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_port53_advisory_addr_in_use() {
|
||||
let err = std::io::Error::from(std::io::ErrorKind::AddrInUse);
|
||||
let msg = try_port53_advisory("0.0.0.0:53", &err).expect("should advise on port 53");
|
||||
assert!(msg.contains("cannot bind to"));
|
||||
assert!(msg.contains("already in use"));
|
||||
assert!(msg.contains("numa install"));
|
||||
assert!(msg.contains("bind_addr"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_port53_advisory_permission_denied() {
|
||||
let err = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
|
||||
let msg = try_port53_advisory("0.0.0.0:53", &err).expect("should advise on port 53");
|
||||
assert!(msg.contains("cannot bind to"));
|
||||
assert!(msg.contains("permission denied"));
|
||||
assert!(msg.contains("numa install"));
|
||||
assert!(msg.contains("bind_addr"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_port53_advisory_skips_non_53_ports() {
|
||||
let err = std::io::Error::from(std::io::ErrorKind::AddrInUse);
|
||||
assert!(try_port53_advisory("127.0.0.1:5354", &err).is_none());
|
||||
assert!(try_port53_advisory("[::]:853", &err).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_port53_advisory_skips_unrelated_error_kinds() {
|
||||
let err = std::io::Error::from(std::io::ErrorKind::NotFound);
|
||||
assert!(try_port53_advisory("0.0.0.0:53", &err).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_port53_advisory_skips_malformed_bind_addr() {
|
||||
let err = std::io::Error::from(std::io::ErrorKind::AddrInUse);
|
||||
assert!(try_port53_advisory("not-an-address", &err).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
98
src/tls.rs
98
src/tls.rs
@@ -5,7 +5,9 @@ use std::sync::Arc;
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::ctx::ServerCtx;
|
||||
use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyPair, KeyUsagePurpose, SanType};
|
||||
use rcgen::{
|
||||
BasicConstraints, CertificateParams, DnType, IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType,
|
||||
};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use rustls::ServerConfig;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
@@ -40,6 +42,40 @@ pub fn regenerate_tls(ctx: &ServerCtx) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Advisory for TLS-setup failures caused by a non-writable data dir;
|
||||
/// `None` if not applicable so the caller can fall back to the raw error.
|
||||
pub fn try_data_dir_advisory(err: &crate::Error, data_dir: &Path) -> Option<String> {
|
||||
let io_err = err.downcast_ref::<std::io::Error>()?;
|
||||
if io_err.kind() != std::io::ErrorKind::PermissionDenied {
|
||||
return None;
|
||||
}
|
||||
let o = "\x1b[1;38;2;192;98;58m";
|
||||
let r = "\x1b[0m";
|
||||
Some(format!(
|
||||
"
|
||||
{o}Numa{r} — HTTPS proxy disabled: cannot write TLS CA to {}.
|
||||
|
||||
The data directory is not writable by the current user. Numa needs
|
||||
to persist a local Certificate Authority there to serve .numa over
|
||||
HTTPS. DNS resolution and plain-HTTP proxy continue to work.
|
||||
|
||||
Fix — pick one:
|
||||
|
||||
1. Install Numa as the system resolver (sets up a writable data dir):
|
||||
|
||||
sudo numa install (on Windows, run as Administrator)
|
||||
|
||||
2. Point data_dir at a path you can write.
|
||||
Create ~/.config/numa/numa.toml with:
|
||||
|
||||
[server]
|
||||
data_dir = \"/path/you/can/write\"
|
||||
|
||||
",
|
||||
data_dir.display()
|
||||
))
|
||||
}
|
||||
|
||||
/// Build a TLS config with a cert covering all provided service names.
|
||||
/// Wildcards under single-label TLDs (*.numa) are rejected by browsers,
|
||||
/// so we list each service explicitly as a SAN.
|
||||
@@ -53,8 +89,8 @@ pub fn build_tls_config(
|
||||
alpn: Vec<Vec<u8>>,
|
||||
data_dir: &Path,
|
||||
) -> crate::Result<Arc<ServerConfig>> {
|
||||
let (ca_cert, ca_key) = ensure_ca(data_dir)?;
|
||||
let (cert_chain, key) = generate_service_cert(&ca_cert, &ca_key, tld, service_names)?;
|
||||
let (ca_der, issuer) = ensure_ca(data_dir)?;
|
||||
let (cert_chain, key) = generate_service_cert(&ca_der, &issuer, tld, service_names)?;
|
||||
|
||||
// Ensure a crypto provider is installed (rustls needs one)
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
@@ -72,7 +108,7 @@ pub fn build_tls_config(
|
||||
Ok(Arc::new(config))
|
||||
}
|
||||
|
||||
fn ensure_ca(dir: &Path) -> crate::Result<(rcgen::Certificate, KeyPair)> {
|
||||
fn ensure_ca(dir: &Path) -> crate::Result<(CertificateDer<'static>, Issuer<'static, KeyPair>)> {
|
||||
let ca_key_path = dir.join("ca.key");
|
||||
let ca_cert_path = dir.join(CA_FILE_NAME);
|
||||
|
||||
@@ -80,10 +116,12 @@ fn ensure_ca(dir: &Path) -> crate::Result<(rcgen::Certificate, KeyPair)> {
|
||||
let key_pem = std::fs::read_to_string(&ca_key_path)?;
|
||||
let cert_pem = std::fs::read_to_string(&ca_cert_path)?;
|
||||
let key_pair = KeyPair::from_pem(&key_pem)?;
|
||||
let params = CertificateParams::from_ca_cert_pem(&cert_pem)?;
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
let ca_der = rustls_pemfile::certs(&mut cert_pem.as_bytes())
|
||||
.next()
|
||||
.ok_or("empty CA PEM file")??;
|
||||
let issuer = Issuer::from_ca_cert_der(&ca_der, key_pair)?;
|
||||
info!("loaded CA from {:?}", ca_cert_path);
|
||||
return Ok((cert, key_pair));
|
||||
return Ok((ca_der, issuer));
|
||||
}
|
||||
|
||||
// Generate new CA
|
||||
@@ -111,14 +149,16 @@ fn ensure_ca(dir: &Path) -> crate::Result<(rcgen::Certificate, KeyPair)> {
|
||||
}
|
||||
|
||||
info!("generated CA at {:?}", ca_cert_path);
|
||||
Ok((cert, key_pair))
|
||||
let ca_der = cert.der().clone();
|
||||
let issuer = Issuer::new(params, key_pair);
|
||||
Ok((ca_der, issuer))
|
||||
}
|
||||
|
||||
/// Generate a cert with explicit SANs for each service name.
|
||||
/// Always regenerated at startup (~5ms) — no disk caching needed.
|
||||
fn generate_service_cert(
|
||||
ca_cert: &rcgen::Certificate,
|
||||
ca_key: &KeyPair,
|
||||
ca_der: &CertificateDer<'static>,
|
||||
issuer: &Issuer<'_, KeyPair>,
|
||||
tld: &str,
|
||||
service_names: &[String],
|
||||
) -> crate::Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
|
||||
@@ -153,7 +193,7 @@ fn generate_service_cert(
|
||||
params.not_before = OffsetDateTime::now_utc();
|
||||
params.not_after = OffsetDateTime::now_utc() + Duration::days(CERT_VALIDITY_DAYS);
|
||||
|
||||
let cert = params.signed_by(&key_pair, ca_cert, ca_key)?;
|
||||
let cert = params.signed_by(&key_pair, issuer)?;
|
||||
|
||||
info!(
|
||||
"generated TLS cert for: {}",
|
||||
@@ -164,9 +204,39 @@ fn generate_service_cert(
|
||||
.join(", ")
|
||||
);
|
||||
|
||||
let cert_der = CertificateDer::from(cert.der().to_vec());
|
||||
let ca_der = CertificateDer::from(ca_cert.der().to_vec());
|
||||
let cert_der = cert.der().clone();
|
||||
let ca_cert_der = ca_der.clone();
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
|
||||
|
||||
Ok((vec![cert_der, ca_der], key_der))
|
||||
Ok((vec![cert_der, ca_cert_der], key_der))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn try_data_dir_advisory_permission_denied() {
|
||||
let err: crate::Error =
|
||||
Box::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
|
||||
let path = PathBuf::from("/usr/local/var/numa");
|
||||
let msg = try_data_dir_advisory(&err, &path).expect("should advise");
|
||||
assert!(msg.contains("HTTPS proxy disabled"));
|
||||
assert!(msg.contains("/usr/local/var/numa"));
|
||||
assert!(msg.contains("numa install"));
|
||||
assert!(msg.contains("data_dir"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_data_dir_advisory_skips_other_io_kinds() {
|
||||
let err: crate::Error = Box::new(std::io::Error::from(std::io::ErrorKind::NotFound));
|
||||
assert!(try_data_dir_advisory(&err, &PathBuf::from("/x")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_data_dir_advisory_skips_non_io_errors() {
|
||||
let err: crate::Error = "rcgen failure".into();
|
||||
assert!(try_data_dir_advisory(&err, &PathBuf::from("/x")).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
138
tests/docker/smoke-port53.sh
Executable file
138
tests/docker/smoke-port53.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Port-53 conflict advisory integration test.
|
||||
#
|
||||
# Builds numa from source inside a debian:bookworm container, pre-binds
|
||||
# port 53 with a UDP socket, then runs numa bare (default bind_addr
|
||||
# 0.0.0.0:53). Verifies:
|
||||
# - process exits with code 1
|
||||
# - stderr contains the advisory ("cannot bind to")
|
||||
# - stderr contains both fix suggestions ("numa install", "bind_addr")
|
||||
#
|
||||
# This is the end-to-end test for the fix in:
|
||||
# src/main.rs — AddrInUse match arm → eprint advisory + process::exit(1)
|
||||
#
|
||||
# No systemd-resolved needed — the conflict is simulated by a Python
|
||||
# UDP socket held open before numa starts.
|
||||
#
|
||||
# Requirements: docker
|
||||
# Usage: ./tests/docker/smoke-port53.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
GREEN="\033[32m"; RED="\033[31m"; RESET="\033[0m"
|
||||
|
||||
pass() { printf " ${GREEN}✓${RESET} %s\n" "$1"; }
|
||||
fail() { printf " ${RED}✗${RESET} %s\n" "$1"; printf " %s\n" "$2"; FAILED=$((FAILED+1)); }
|
||||
FAILED=0
|
||||
|
||||
echo "── smoke-port53: building + testing numa on debian:bookworm ──"
|
||||
echo " (first run is slow: image pull + cold cargo build, ~5-8 min)"
|
||||
echo
|
||||
|
||||
OUTPUT=$(docker run --rm \
|
||||
--platform linux/amd64 \
|
||||
-v "$PWD:/src:ro" \
|
||||
-v numa-port53-cargo:/root/.cargo \
|
||||
-v numa-port53-target:/work/target \
|
||||
debian:bookworm bash -c '
|
||||
set -e
|
||||
|
||||
apt-get update -qq && apt-get install -y -qq curl build-essential python3 2>&1 | tail -3
|
||||
|
||||
# Install rustup if not already in the cargo cache volume
|
||||
if ! command -v cargo &>/dev/null; then
|
||||
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --quiet
|
||||
fi
|
||||
. "$HOME/.cargo/env"
|
||||
|
||||
# Copy source to a writable workdir
|
||||
mkdir -p /work
|
||||
tar -C /src --exclude=./target --exclude=./.git -cf - . | tar -C /work -xf -
|
||||
cd /work
|
||||
|
||||
echo "── cargo build --release --locked ──"
|
||||
cargo build --release --locked 2>&1 | tail -5
|
||||
echo
|
||||
|
||||
# Write the holder script to a file to avoid quoting hell.
|
||||
# Holds port 53 until killed — no sleep race.
|
||||
cat > /tmp/hold53.py << '"'"'PYEOF'"'"'
|
||||
import socket, signal
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 0)
|
||||
s.bind(("", 53))
|
||||
signal.pause()
|
||||
PYEOF
|
||||
|
||||
python3 /tmp/hold53.py &
|
||||
HOLDER_PID=$!
|
||||
|
||||
# Verify the holder is actually up before proceeding
|
||||
sleep 0.3
|
||||
if ! kill -0 $HOLDER_PID 2>/dev/null; then
|
||||
echo "holder_failed=1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "── running numa with port 53 already bound ──"
|
||||
# timeout 5: guards against numa not exiting (advisory not fired, bug present)
|
||||
# Capture stderr to a file so the exit code is not clobbered by || or $()
|
||||
set +e
|
||||
timeout 5 ./target/release/numa > /tmp/numa-stderr.txt 2>&1
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
STDERR=$(cat /tmp/numa-stderr.txt)
|
||||
|
||||
kill $HOLDER_PID 2>/dev/null || true
|
||||
|
||||
echo "exit_code=$EXIT_CODE"
|
||||
printf "%s" "$STDERR" | sed "s/^/ numa: /"
|
||||
' 2>&1)
|
||||
|
||||
echo "$OUTPUT"
|
||||
|
||||
echo
|
||||
echo "── assertions ──"
|
||||
|
||||
if echo "$OUTPUT" | grep -q "holder_failed=1"; then
|
||||
echo " SETUP FAILED: could not pre-bind port 53 inside container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXIT_CODE=$(echo "$OUTPUT" | grep '^exit_code=' | cut -d= -f2)
|
||||
|
||||
if [ "${EXIT_CODE:-}" = "1" ]; then
|
||||
pass "exits with code 1"
|
||||
else
|
||||
fail "exits with code 1" "got: exit_code=${EXIT_CODE:-<missing>}"
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "cannot bind to"; then
|
||||
pass "advisory printed to stderr"
|
||||
else
|
||||
fail "advisory printed to stderr" "stderr did not contain 'cannot bind to'"
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "numa install"; then
|
||||
pass "advisory offers 'sudo numa install'"
|
||||
else
|
||||
fail "advisory offers 'sudo numa install'" "not found in output"
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "bind_addr"; then
|
||||
pass "advisory offers non-privileged port alternative"
|
||||
else
|
||||
fail "advisory offers non-privileged port alternative" "'bind_addr' not found in output"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
printf "${GREEN}── smoke-port53 passed ──${RESET}\n"
|
||||
exit 0
|
||||
else
|
||||
printf "${RED}── smoke-port53 failed ($FAILED assertion(s)) ──${RESET}\n"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user