Build Housecarl 0.1.0: the nftables firewall manager
Zones, address sets (IPv4 and IPv6 mixed, dynamic sets with timeouts) and services, ordered rules in the early, input, forward and output chains, port forwards with hairpin NAT and IPv6 pinholes, source NAT, redirects, and switchable built-ins (loopback, conntrack, ICMPv6, antispoofing, MSS clamping, DHCPv6 client, flowtable). Every change is validated, checked with nft -c and applied in one transaction to Housecarl's own table, inet housecarl; changes made over the network roll back unless confirmed, and the confirmed ruleset is loaded at boot by housecarl-ruleset.service. An empty configuration applies nothing. Also: hit counters and the drop log (nflog), 'hc import' of an existing nftables ruleset and 'hc takeover', the hc CLI, the REST API and the web UI, users, scoped tokens (sets:write:NAME), the audit log, the web UI's certificate through Jarl, Debian packages, docs, unit tests with real nft 1.1.3, and an e2e test with real traffic (71 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54beb0bba3
commit
34292eeebb
118 files changed
+22165
-114
No files matched your search
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/sh
|
||||
# Publishes a built release (make dist, make cli-dist, make deb for each
|
||||
# architecture) to Gitea:
|
||||
#
|
||||
# * a release for tag v<VERSION> with the CHANGELOG section for VERSION as
|
||||
# its notes, and every file in dist/ (tarballs, CLI binaries, .debs)
|
||||
# plus a SHA256SUMS of them attached;
|
||||
# * the .debs in the owner's Debian package registry, so servers can
|
||||
# 'apt install housecarl' and get updates with apt (docs/install.md).
|
||||
#
|
||||
# Run by .gitea/workflows/release.yml when a v* tag is pushed. Running it
|
||||
# again for the same version replaces the attachments and packages.
|
||||
#
|
||||
# A version with a hyphen (0.2.0-rc.1) is a pre-release: Gitea marks the
|
||||
# release so and doesn't count it as the latest, and its packages go to
|
||||
# the "testing" distribution, so servers that follow "stable" never get
|
||||
# a pre-release from apt.
|
||||
#
|
||||
# Release notes: the CHANGELOG section "## [VERSION]". A pre-release
|
||||
# without one of its own uses the section of the version it leads up to
|
||||
# ("## [0.2.0]"), or else "## [Unreleased]".
|
||||
#
|
||||
# 'gitea-release.sh notes' only prints the notes, failing if there are
|
||||
# none (the workflow checks this before building).
|
||||
#
|
||||
# Variables:
|
||||
# GITEA_URL e.g. https://gitea.example.com (in Actions:
|
||||
# GITHUB_SERVER_URL)
|
||||
# GITEA_REPO owner/name (in Actions: GITHUB_REPOSITORY)
|
||||
# GITEA_TOKEN a token with write:repository and write:package
|
||||
# VERSION default: the VERSION file
|
||||
# DEB_DISTRIBUTION the apt distribution (default: stable, testing for
|
||||
# a pre-release)
|
||||
# DEB_COMPONENT the apt component (default: main)
|
||||
#
|
||||
# Needs curl and jq.
|
||||
set -eu
|
||||
|
||||
VERSION=${VERSION:-$(cat VERSION)}
|
||||
prerelease=false
|
||||
case $VERSION in *-*) prerelease=true ;; esac
|
||||
|
||||
# section prints the CHANGELOG section headed "## [$1]" (without the
|
||||
# heading); nothing if there is none.
|
||||
section() {
|
||||
awk -v v="$1" '
|
||||
/^## \[/ { if (on) exit; on = index($0, "## [" v "]") == 1; next }
|
||||
on' CHANGELOG.md
|
||||
}
|
||||
|
||||
# The release notes (see above). A section with only blank lines counts
|
||||
# as missing.
|
||||
notes=$(section "$VERSION")
|
||||
from="[$VERSION]"
|
||||
if [ "$prerelease" = true ]; then
|
||||
for s in "${VERSION%%-*}" Unreleased; do
|
||||
[ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ] || break
|
||||
notes=$(section "$s")
|
||||
from="[$s]"
|
||||
done
|
||||
fi
|
||||
if [ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then
|
||||
if [ "$prerelease" = true ]; then
|
||||
echo "gitea-release: CHANGELOG.md has no '## [$VERSION]', '## [${VERSION%%-*}]' or '## [Unreleased]' section with notes" >&2
|
||||
else
|
||||
echo "gitea-release: CHANGELOG.md has no '## [$VERSION]' section with notes" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
if [ "${1:-}" = notes ]; then
|
||||
echo "gitea-release: notes for $VERSION from CHANGELOG.md $from" >&2
|
||||
printf '%s\n' "$notes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
: "${GITEA_URL:=${GITHUB_SERVER_URL:-}}" "${GITEA_REPO:=${GITHUB_REPOSITORY:-}}"
|
||||
: "${GITEA_URL:?set GITEA_URL}" "${GITEA_REPO:?set GITEA_REPO}" "${GITEA_TOKEN:?set GITEA_TOKEN}"
|
||||
if [ "$prerelease" = true ]; then
|
||||
DEB_DISTRIBUTION=${DEB_DISTRIBUTION:-testing}
|
||||
else
|
||||
DEB_DISTRIBUTION=${DEB_DISTRIBUTION:-stable}
|
||||
fi
|
||||
DEB_COMPONENT=${DEB_COMPONENT:-main}
|
||||
GITEA_URL=${GITEA_URL%/}
|
||||
owner=${GITEA_REPO%%/*}
|
||||
tag=v$VERSION
|
||||
api=$GITEA_URL/api/v1/repos/$GITEA_REPO
|
||||
|
||||
# curl with the token; fails on HTTP errors except the codes in $ok.
|
||||
call() {
|
||||
ok=$1
|
||||
shift
|
||||
out=$(mktemp)
|
||||
code=$(curl -sS -o "$out" -w '%{http_code}' -H "Authorization: token $GITEA_TOKEN" "$@")
|
||||
case " 200 201 204 $ok " in
|
||||
*" $code "*) cat "$out"; rm -f "$out"; return 0 ;;
|
||||
esac
|
||||
echo "gitea-release: HTTP $code from $*" >&2
|
||||
cat "$out" >&2
|
||||
echo >&2
|
||||
rm -f "$out"
|
||||
return 1
|
||||
}
|
||||
|
||||
files=$(find dist -maxdepth 1 -type f ! -name SHA256SUMS | sort)
|
||||
[ -n "$files" ] || { echo "gitea-release: nothing in dist/" >&2; exit 1; }
|
||||
# (Debian package versions have ~ for -: housecarl_0.2.0~rc.1_amd64.deb.)
|
||||
debversion=$(printf '%s' "$VERSION" | tr - '~')
|
||||
for f in $files; do
|
||||
case $f in *"$VERSION"* | *"$debversion"*) ;; *) echo "gitea-release: $f is not version $VERSION" >&2; exit 1 ;; esac
|
||||
done
|
||||
# shellcheck disable=SC2046 # file names have no spaces
|
||||
(cd dist && sha256sum $(printf '%s\n' "$files" | sed 's|^dist/||')) > dist/SHA256SUMS
|
||||
files="$files dist/SHA256SUMS"
|
||||
|
||||
# The release (created, or found when publishing again).
|
||||
release=$(call 404 "$api/releases/tags/$tag")
|
||||
id=$(printf '%s\n' "$release" | jq -r '.id // empty')
|
||||
if [ -z "$id" ]; then
|
||||
body=$(jq -n --arg tag "$tag" --arg notes "$notes" --argjson pre "$prerelease" \
|
||||
'{tag_name: $tag, name: ("Housecarl " + $tag), body: $notes, prerelease: $pre}')
|
||||
release=$(call "" -X POST -H 'Content-Type: application/json' -d "$body" "$api/releases")
|
||||
id=$(printf '%s\n' "$release" | jq -r .id)
|
||||
echo "gitea-release: created release $tag"
|
||||
fi
|
||||
for f in $files; do
|
||||
name=${f#dist/}
|
||||
old=$(printf '%s\n' "$release" | jq -r --arg n "$name" '.assets[]? | select(.name == $n) | .id')
|
||||
[ -z "$old" ] || call "" -X DELETE "$api/releases/$id/assets/$old" >/dev/null
|
||||
call "" -X POST -F "attachment=@$f" "$api/releases/$id/assets?name=$name" >/dev/null
|
||||
echo "gitea-release: attached $name"
|
||||
done
|
||||
|
||||
# The Debian packages. Gitea refuses a package version it already has
|
||||
# (409), so publishing again deletes it first.
|
||||
pool=$GITEA_URL/api/packages/$owner/debian/pool/$DEB_DISTRIBUTION/$DEB_COMPONENT
|
||||
for f in dist/*.deb; do
|
||||
[ -e "$f" ] || continue
|
||||
# shellcheck disable=SC2016,SC2046 # dpkg-deb's fields; split into "$@"
|
||||
set -- $(dpkg-deb --showformat '${Package} ${Version} ${Architecture}' --show "$f")
|
||||
if ! call 409 --upload-file "$f" "$pool/upload" >/dev/null; then
|
||||
echo "gitea-release: upload of $f failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$code" = 409 ]; then
|
||||
call "" -X DELETE "$pool/$1/$2/$3" >/dev/null
|
||||
call "" --upload-file "$f" "$pool/upload" >/dev/null
|
||||
fi
|
||||
echo "gitea-release: uploaded $1 $2 $3 to $DEB_DISTRIBUTION/$DEB_COMPONENT"
|
||||
done
|
||||
Reference in new issue
Block a user