Netmaker add-on: pull prebuilt image from Gitea registry

Home Assistant add-on that joins the instance to a Netmaker network via
netclient. Distributed as a repository add-on using a prebuilt arm64 image
(gitea.savant.io/homeassistant/netmaker) to avoid local buildx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Savant Coder
2026-07-21 14:05:20 -04:00
commit 2803cf6792
27 changed files with 638 additions and 0 deletions

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2026 Savant Coder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# Home Assistant Community App: Netmaker
![Project Stage][project-stage-shield]
![Supports aarch64 Architecture][aarch64-shield]
![Supports amd64 Architecture][amd64-shield]
Join your Home Assistant instance to a Netmaker WireGuard network.
## About
[Netmaker][netmaker] is a platform for creating and managing fast, secure
WireGuard networks. This app joins your Home Assistant instance to a Netmaker
network using the Netmaker client, `netclient`.
You provide an enrollment token generated in your Netmaker dashboard, and the
app registers your device and keeps the connection running in the background.
The host's WireGuard identity is persisted to the app's `/data` volume, so your
device remains the same node across restarts and updates.
## Installation
The installation of this app is straightforward and not different in comparison
to installing any other Home Assistant app.
1. Install this app.
1. Generate an enrollment token in your Netmaker dashboard (**Enrollment Keys**).
1. Set the token in the app's `enrollment_token` configuration option.
1. Start the "Netmaker" app.
1. Check the logs of the app to confirm it joined the network.
See the [documentation][docs] for full configuration details.
## Support
This add-on is a community port and is not affiliated with Netmaker.
## License
MIT License — see [LICENSE.md](LICENSE.md).
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
[docs]: netmaker/DOCS.md
[netmaker]: https://www.netmaker.io/
[project-stage-shield]: https://img.shields.io/badge/project%20stage-experimental-yellow.svg

39
netmaker/.README.j2 Normal file
View File

@ -0,0 +1,39 @@
# Home Assistant Community App: Netmaker
![Project Stage][project-stage-shield] ![Project Maintenance][maintenance-shield]
Join your Home Assistant instance to a Netmaker WireGuard network.
## About
Netmaker is a platform for creating and managing fast, secure WireGuard
networks. This app joins your Home Assistant instance to a Netmaker network
using the Netmaker client, `netclient`.
You provide an enrollment token from your Netmaker dashboard, and the app
registers your device and keeps the connection running in the background.
{% if channel == "edge" %}
## WARNING! THIS IS AN EDGE VERSION!
This Home Assistant Apps repository contains edge builds of apps.
Edge builds apps are based upon the latest development version.
- They may not work at all.
- They might stop working at any time.
- They could have a negative impact on your system.
{% endif %}
{% if channel == "beta" %}
## WARNING! THIS IS A BETA VERSION!
This Home Assistant Apps repository contains beta releases of apps.
- They might stop working at any time.
- They could have a negative impact on your system.
{% endif %}
[maintenance-shield]: https://img.shields.io/maintenance/yes/2026.svg
[project-stage-shield]: https://img.shields.io/badge/project%20stage-experimental-yellow.svg
[release-shield]: https://img.shields.io/badge/version-{{ version }}-blue.svg
[release]: {{ repo }}/tree/{{ version }}

95
netmaker/DOCS.md Normal file
View File

@ -0,0 +1,95 @@
# Home Assistant Community App: Netmaker
Netmaker is a platform for creating and managing fast, secure WireGuard
networks. This app joins your Home Assistant instance to a Netmaker network
using the Netmaker client, `netclient`.
## Prerequisites
In order to use this app, you'll need access to a Netmaker server and an
**enrollment token** (also called an enrollment key) for the network you want
to join.
You can run your own Netmaker server or use a hosted one. See the Netmaker
documentation for details:
<https://docs.netmaker.io/>
## Installation
1. Install this app.
1. Generate an enrollment token in your Netmaker dashboard, under
**Enrollment Keys**.
1. Open the app's **Configuration** tab and paste the token into the
`enrollment_token` option.
1. Start the "Netmaker" app.
1. Check the logs of the "Netmaker" app to confirm it joined the network.
1. Done!
The host's WireGuard identity and keys are stored on the app's persistent
`/data` volume, so your device stays the same node across restarts and updates.
## Configuration
Example configuration:
```yaml
enrollment_token: "eyJ...your-token..."
log_level: info
host_name: ""
port: 0
interface: ""
endpoint: ""
endpoint6: ""
firewall: ""
```
### Option: `enrollment_token` (required)
The enrollment token used to join your Netmaker network. Generate one in your
Netmaker dashboard under **Enrollment Keys**. The token already contains the
address of your Netmaker server, so no separate server URL is needed.
### Option: `log_level`
Controls the verbosity of the netclient logs. One of `trace`, `debug`, `info`,
`notice`, `warning`, `error`, `fatal`. Use `debug` or `trace` when
troubleshooting connection problems.
### Option: `host_name`
Optional name to register this device as within your Netmaker network. Leave
empty to use the default host name.
### Option: `port`
Optional UDP port for WireGuard to listen on. Leave at `0` to let netclient
choose a port. If you set a specific port, expose the same port in the
**Network** section of the app.
### Option: `interface`
Optional name for the WireGuard network interface created on the host. On Linux
the name must start with `netmaker`. Leave empty for the default.
### Option: `endpoint` / `endpoint6`
Optional static public IPv4/IPv6 endpoint for this host. Leave empty to let
Netmaker detect the endpoint automatically.
### Option: `firewall`
Optional firewall backend to use for ACL enforcement: `iptables` or `nftables`.
Leave empty to let netclient auto-detect. If neither tool is available,
netclient runs without firewall/ACL enforcement (the connection still works).
## Network
This app runs on the host network. netclient uses WireGuard over UDP; by
default port `51821/udp` is used. If you configure a specific `port`, expose it
in the **Network** section.
## Leaving a network
To leave the Netmaker network, uninstall the app. To rotate the node's
identity, clear the app's data (uninstall and reinstall) before joining again.

63
netmaker/Dockerfile Normal file
View File

@ -0,0 +1,63 @@
ARG BUILD_FROM=ghcr.io/hassio-addons/base/amd64:10.1.1
# hadolint ignore=DL3006
FROM ${BUILD_FROM}
# Set shell
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Setup base
ARG BUILD_ARCH=amd64
ARG NETCLIENT_VERSION="v1.6.0"
# hadolint ignore=DL3018
RUN \
apk add --no-cache \
iproute2 \
iptables \
ip6tables \
nftables \
wireguard-tools \
openresolv \
coreutils \
\
&& ln -sf /sbin/xtables-nft-multi /sbin/ip6tables \
&& ln -sf /sbin/xtables-nft-multi /sbin/iptables \
\
&& if [ "${BUILD_ARCH}" = "aarch64" ]; then ARCH="arm64"; fi \
&& if [ "${BUILD_ARCH}" = "amd64" ]; then ARCH="amd64"; fi \
\
&& curl -L -s -o /opt/netclient \
"https://github.com/gravitl/netclient/releases/download/${NETCLIENT_VERSION}/netclient-linux-${ARCH}" \
&& chmod 0755 /opt/netclient \
&& ln -sf /opt/netclient /usr/bin/netclient
# Copy root filesystem
COPY rootfs /
HEALTHCHECK \
CMD healthcheck
# Build arguments
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
# Labels
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Community Apps" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}

45
netmaker/apparmor.txt Normal file
View File

@ -0,0 +1,45 @@
#include <tunables/global>
profile netmaker flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# Capabilities
file,
signal (send) set=(kill,term,int,hup,cont),
# S6-Overlay
/init ix,
/bin/** ix,
/usr/bin/** ix,
/run/{s6,s6-rc*,service}/** ix,
/package/** ix,
/command/** ix,
/etc/services.d/** rwix,
/etc/cont-init.d/** rwix,
/etc/cont-finish.d/** rwix,
/run/{,**} rwk,
/dev/tty rw,
# Bashio
/usr/lib/bashio/** ix,
/tmp/** rwk,
# Access to options.json and other files within your app
/data/** rw,
# Netclient configuration/state (symlinked to /data/netclient)
/etc/netclient/{,**} rw,
# General - capabilities derived from complain mode analysis
capability net_bind_service,
capability dac_override,
capability fsetid,
capability setgid,
capability setuid,
capability chown,
capability kill,
# General - based on Config.yaml
capability net_admin,
capability net_raw,
}

4
netmaker/build.yaml Normal file
View File

@ -0,0 +1,4 @@
---
build_from:
aarch64: ghcr.io/hassio-addons/base/aarch64:10.1.1
amd64: ghcr.io/hassio-addons/base/amd64:10.1.1

46
netmaker/config.yaml Normal file
View File

@ -0,0 +1,46 @@
---
name: Netmaker
version: dev
slug: netmaker
description: Join your Home Assistant instance to a Netmaker WireGuard network
url: https://github.com/hassio-addons/app-netmaker
image: gitea.savant.io/homeassistant/netmaker
startup: services
panel_icon: mdi:lan-connect
arch:
- aarch64
- amd64
init: false
hassio_api: true
host_network: true
privileged:
- NET_ADMIN
- NET_RAW
devices:
- /dev/net/tun
map:
- type: homeassistant_config
read_only: false
path: /config
- type: share
read_only: false
ports:
51821/udp: null
options:
enrollment_token: ""
log_level: info
host_name: ""
port: 0
interface: ""
endpoint: ""
endpoint6: ""
firewall: ""
schema:
enrollment_token: str
log_level: list(trace|debug|info|notice|warning|error|fatal)
host_name: str?
port: int(0,65535)?
interface: "match(^netmaker[a-zA-Z0-9_-]*$)?"
endpoint: str?
endpoint6: str?
firewall: list(iptables|nftables)?

View File

@ -0,0 +1,14 @@
#!/command/execlineb -S0
ifelse
{
importas -D 0 S6_KEEP_ENV S6_KEEP_ENV
eltest 0${S6_KEEP_ENV} -eq 0
}
{
s6-envdir -Lf -- /run/s6/container_environment
exec
$@
}
$@

View File

@ -0,0 +1,29 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
export LOG_FD
# ==============================================================================
# Wait for the local network (default HA interface) to be ready
# ==============================================================================
readonly WAIT_DELAY=5 # 5s
readonly WAIT_COUNT=60 # 60*5s = 300s = 5m
declare wait_counter=0
# Some services need a working local network to function properly.
# They can mark this service as dependency, and wait for the successful startup.
# Until HA has no default interface, we wait a little
while ! bashio::api.supervisor GET "/network/interface/default/info" false &> /dev/null; do
if (( wait_counter++ == WAIT_COUNT )); then
# We emit only a warning to let the app start, maybe this is the only connection to access the device, better to start than not.
# Let netclient figure out a way to connect to the network if the local network is temporarily down.
bashio::log.warning "Local network (default Home Assistant interface) is unreachable"
break
fi
bashio::log.info "Waiting for the local network (default Home Assistant interface) to be ready..."
sleep $WAIT_DELAY
done
if (( wait_counter != 0 && wait_counter <= WAIT_COUNT )); then
bashio::log.info "Local network is ready"
fi

View File

@ -0,0 +1 @@
oneshot

View File

@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/local-network/run

View File

@ -0,0 +1,40 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
export LOG_FD
# ==============================================================================
# Home Assistant Community App: Netmaker
# Cleans up on daemon exit and takes down the S6 supervision tree on failure
# ==============================================================================
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
readonly exit_code_service="${1}"
readonly exit_code_signal="${2}"
readonly service="netclient-daemon"
declare iface
bashio::log.info \
"Service ${service} exited with code ${exit_code_service}" \
"(by signal ${exit_code_signal})"
# Replicate the cleanup netclient's container entrypoint performs on shutdown.
# The daemon is exec'd, so its own shell trap does not survive; do it here.
iface=$(bashio::config 'interface')
if bashio::var.is_empty "${iface}"; then
iface="netmaker"
fi
ip rule delete pref 3000 2> /dev/null || true
ip rule delete pref 2500 2> /dev/null || true
ip rule delete pref 2000 2> /dev/null || true
ip link del "${iface}" 2> /dev/null || true
if [[ "${exit_code_service}" -eq 256 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo $((128 + $exit_code_signal)) > /run/s6-linux-init-container-results/exitcode
fi
[[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt
elif [[ "${exit_code_service}" -ne 0 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode
fi
exec /run/s6/basedir/bin/halt
fi

View File

@ -0,0 +1,39 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
export LOG_FD
# ==============================================================================
# Home Assistant Community App: Netmaker
# Runs the netclient daemon
# ==============================================================================
declare log_level
declare verbosity
bashio::log.info 'Starting Netmaker netclient...'
# Netclient stores its host identity, keys and node/server config under the
# hard-coded path /etc/netclient/. Persist it across restarts by pointing that
# path at the add-on's /data volume so the node is not re-registered each boot.
mkdir -p /data/netclient
if [ ! -L /etc/netclient ]; then
# Migrate anything an earlier image may have written to the real directory
if [ -d /etc/netclient ] && [ ! -d /data/netclient/.migrated ]; then
cp -a /etc/netclient/. /data/netclient/ 2>/dev/null || true
rm -rf /etc/netclient
fi
ln -sf /data/netclient /etc/netclient
fi
# Map the app's log_level to netclient's numeric verbosity (0..4)
log_level=$(bashio::config 'log_level')
case "${log_level}" in
trace) verbosity=4 ;;
debug) verbosity=3 ;;
info|notice) verbosity=2 ;;
warning) verbosity=1 ;;
error|fatal) verbosity=0 ;;
*) verbosity=2 ;;
esac
# Run the daemon in the foreground so s6 supervises it directly.
# functions.Daemon() blocks and handles SIGTERM (clean shutdown) / SIGHUP (reload).
exec /opt/netclient daemon -v "${verbosity}"

View File

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1,68 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
export LOG_FD
# ==============================================================================
# Home Assistant Community App: Netmaker
# Joins the netmaker network once, using an enrollment token
# ==============================================================================
declare token
declare -a options=()
declare value
declare wait_counter=0
readonly NODES_FILE="/etc/netclient/nodes.json"
readonly WAIT_DELAY=2
readonly WAIT_COUNT=30 # 30*2s = 60s
# An enrollment token is required for a headless join. The token is a base64
# blob that already embeds the netmaker server address, so nothing else is
# needed to reach the server.
token=$(bashio::config 'enrollment_token')
if bashio::var.is_empty "${token}"; then
bashio::exit.nok \
"No 'enrollment_token' configured. Generate one in your Netmaker dashboard" \
"(Enrollment Keys) and set it in this app's configuration."
fi
# If this host is already registered to a network, don't join again. The daemon
# keeps nodes.json populated; a non-empty object means we have joined before.
if bashio::fs.file_exists "${NODES_FILE}" \
&& jq --exit-status 'length > 0' "${NODES_FILE}" > /dev/null 2>&1; then
bashio::log.info "netclient is already joined to a network, skipping join"
exit 0
fi
# Build the optional join flags from configuration.
if value=$(bashio::config 'host_name') && bashio::var.has_value "${value}"; then
options+=(-o "${value}")
fi
if bashio::config.has_value 'port' && [[ "$(bashio::config 'port')" -ne 0 ]]; then
options+=(-p "$(bashio::config 'port')")
fi
if value=$(bashio::config 'interface') && bashio::var.has_value "${value}"; then
options+=(-I "${value}")
fi
if value=$(bashio::config 'endpoint') && bashio::var.has_value "${value}"; then
options+=(-e "${value}")
fi
if value=$(bashio::config 'endpoint6') && bashio::var.has_value "${value}"; then
options+=(-E "${value}")
fi
if value=$(bashio::config 'firewall') && bashio::var.has_value "${value}"; then
options+=(-f "${value}")
fi
# Give the daemon a moment to initialize its config directory before joining.
while [ ! -d /etc/netclient ]; do
if (( wait_counter++ == WAIT_COUNT )); then
break
fi
sleep "${WAIT_DELAY}"
done
bashio::log.info "Joining netmaker network..."
if ! /opt/netclient join -t "${token}" "${options[@]}"; then
bashio::exit.nok "Failed to join the netmaker network"
fi
bashio::log.info "Successfully joined the netmaker network"

View File

@ -0,0 +1 @@
oneshot

View File

@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/netclient-join/run

View File

@ -0,0 +1,35 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
export LOG_FD
# Netclient has no queryable status socket, so this treats the app as:
# - healthy while it is still trying to join (within HEALTHCHECK_RESTART_TIMEOUT)
# - healthy once it has joined a network and the daemon process is running
# - unhealthy if it never manages to join within HEALTHCHECK_RESTART_TIMEOUT
# Redirect outputs to the log (&1 and &2 are not initialized by Docker)
exec &> /proc/1/fd/1
if [[ "${LOG_FD:-}" =~ ^[0-9]+$ ]]; then
eval "exec ${LOG_FD}>&1" || true
fi
readonly HEALTHCHECK_RESTART_TIMEOUT=3600 # 1 hour
readonly NODES_FILE="/etc/netclient/nodes.json"
# STARTED_TIMESTAMP is stored in contenv at /var/run/s6/container_environment
if ! bashio::var.has_value "${STARTED_TIMESTAMP-}"; then
STARTED_TIMESTAMP=$(date +"%s")
printf "%s" "${STARTED_TIMESTAMP}" > /var/run/s6/container_environment/STARTED_TIMESTAMP
fi
# Healthy as soon as we are joined to at least one network.
if bashio::fs.file_exists "${NODES_FILE}" \
&& jq --exit-status 'length > 0' "${NODES_FILE}" > /dev/null 2>&1; then
exit 0
fi
# Not joined yet: stay healthy during the initial window, then fail so the
# container is restarted.
if (( $(date +"%s") - STARTED_TIMESTAMP > HEALTHCHECK_RESTART_TIMEOUT )); then
bashio::exit.nok "netclient has not joined a network within the expected time"
fi

View File

@ -0,0 +1,45 @@
---
configuration:
enrollment_token:
name: Enrollment token
description: >-
The enrollment token (enrollment key) used to join your Netmaker network.
Generate one in your Netmaker dashboard under "Enrollment Keys" and paste
it here. The token already contains the address of your Netmaker server.
log_level:
name: Log level
description: >-
Controls the verbosity of the netclient logs. Set to "debug" or "trace"
when troubleshooting connection problems.
host_name:
name: Host name
description: >-
Optional name to register this device as within your Netmaker network.
Leave empty to use the default host name.
port:
name: WireGuard port
description: >-
Optional UDP port for WireGuard to listen on. Leave at 0 to let netclient
choose a port. If you set this, expose the same port in the Network section.
interface:
name: Interface name
description: >-
Optional name for the WireGuard network interface created on the host.
On Linux the name must start with "netmaker". Leave empty for the default.
endpoint:
name: Static endpoint (IPv4)
description: >-
Optional static public IPv4 endpoint for this host. Leave empty to let
Netmaker detect the endpoint automatically.
endpoint6:
name: Static endpoint (IPv6)
description: >-
Optional static public IPv6 endpoint for this host. Leave empty to let
Netmaker detect the endpoint automatically.
firewall:
name: Firewall
description: >-
Optional firewall backend to use for ACL enforcement (iptables or
nftables). Leave empty to let netclient auto-detect.
network:
51821/udp: WireGuard port used by netclient

4
repository.yaml Normal file
View File

@ -0,0 +1,4 @@
---
name: Savant Home Assistant Add-ons
url: https://gitea.savant.io/homeassistant/hassio-addons
maintainer: Savant Coder <ramirez.philip@gmail.com>