Added mullvad init script

This commit is contained in:
Josh Stark 2026-02-22 10:05:30 +00:00
parent 3db855ba2d
commit a16867844d
21 changed files with 158 additions and 83 deletions

View File

@ -12,8 +12,8 @@ on:
env:
GITHUB_REPO: "linuxserver/docker-mods" #don't modify
ENDPOINT: "linuxserver/mods" #don't modify
BASEIMAGE: "replace_baseimage" #replace
MODNAME: "replace_modname" #replace
BASEIMAGE: "scratch" #replace
MODNAME: "wireguard-mullvad" #replace
MOD_VERSION: ${{ inputs.mod_version }} #don't modify
MULTI_ARCH: "true" #set to false if not needed

View File

@ -1,33 +0,0 @@
# syntax=docker/dockerfile:1
## Buildstage ##
FROM ghcr.io/linuxserver/baseimage-alpine:3.20 AS buildstage
RUN \
echo "**** install packages ****" && \
apk add --no-cache \
curl && \
echo "**** grab rclone ****" && \
mkdir -p /root-layer && \
if [ $(uname -m) = "x86_64" ]; then \
echo "Downloading x86_64 tarball" && \
curl -o \
/root-layer/rclone.deb -L \
"https://downloads.rclone.org/v1.47.0/rclone-v1.47.0-linux-amd64.deb"; \
elif [ $(uname -m) = "aarch64" ]; then \
echo "Downloading aarch64 tarball" && \
curl -o \
/root-layer/rclone.deb -L \
"https://downloads.rclone.org/v1.47.0/rclone-v1.47.0-linux-arm64.deb"; \
fi && \
# copy local files
COPY root/ /root-layer/
## Single layer deployed image ##
FROM scratch
LABEL maintainer="username"
# Add files from buildstage
COPY --from=buildstage /root-layer/ /

View File

@ -1,30 +0,0 @@
#!/usr/bin/with-contenv bash
# This is the init file used for adding os or pip packages to install lists.
# It takes advantage of the built-in init-mods-package-install init script that comes with the baseimages.
# If using this, we need to make sure we set this init as a dependency of init-mods-package-install so this one runs first
if ! command -v apprise; then
echo "**** Adding apprise and its deps to package install lists ****"
echo "apprise" >> /mod-pip-packages-to-install.list
## Ubuntu
if [ -f /usr/bin/apt ]; then
echo "\
python3 \
python3-pip \
runc" >> /mod-repo-packages-to-install.list
fi
# Alpine
if [ -f /sbin/apk ]; then
echo "\
cargo \
libffi-dev \
openssl-dev \
python3 \
python3-dev \
python3 \
py3-pip" >> /mod-repo-packages-to-install.list
fi
else
echo "**** apprise already installed, skipping ****"
fi

View File

@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-mod-imagename-modname-add-package/run

View File

@ -1,8 +0,0 @@
#!/usr/bin/with-contenv bash
# This is an install script that is designed to run after init-mods-package-install
# so it can take advantage of packages installed
# init-mods-end depends on this script so that later init and services wait until this script exits
echo "**** Setting up apprise ****"
apprise blah blah

View File

@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-mod-imagename-modname-install/run

View File

@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-mod-wireguard-mullvad-add-package/run

View File

@ -0,0 +1,154 @@
#!/usr/bin/with-contenv bash
fail() {
echo "[mullvad_setup] Error: $1" >&2
exit 1
}
skip() {
echo "[mullvad_setup] $1"
exit 0
}
config_file_base="/opt/mullvad"
target_link="/config/wg_confs/wg0.conf"
# Verify required environment variables are present. This script cannot run without them.
[[ -z "${MULLVAD_ACCOUNT}" ]] && fail "MULLVAD_ACCOUNT environment variable is not set"
[[ -z "${MULLVAD_PRIVATE_KEY}" ]] && fail "MULLVAD_PRIVATE_KEY environment variable is not set"
[[ -z "${MULLVAD_LOCATION}" ]] && fail "MULLVAD_LOCATION environment variable is not set"
# Set up the configuration output directory. Create it if it doesn't exist.
[[ -d "${config_file_base}" ]] || mkdir -p "${config_file_base}"
echo "[mullvad_setup] Cleaning config base..."
rm -f "${config_file_base}/"*
echo "[mullvad_setup] Removing old symlink if it exists..."
rm -f "$target_link"
# MULLVAD_LOCATION is a dynamic-value variable.
# It may may contain a value fo either:
# - a region (e.g. 'gb')
# - a city (e.g. 'gb-lon')
# - or specific nodes (e.g. 'gb-lon-wg-001,gb-lon-wg-002').
if [[ "$MULLVAD_LOCATION" =~ ^[a-z]{2}$ ]]; then
echo "[mullvad_setup] Location filter set to region '$MULLVAD_LOCATION'."
jq_filter=".countries[] | select(.code == \"$MULLVAD_LOCATION\") | .cities[] | .relays[]"
elif [[ "$MULLVAD_LOCATION" =~ ^[a-z]{2}-[a-z]{3}$ ]]; then
echo "[mullvad_setup] Location filter set to city '$MULLVAD_LOCATION'."
country="${MULLVAD_LOCATION%%-*}"
city="${MULLVAD_LOCATION##*-}"
jq_filter=".countries[] | select(.code == \"$country\") | .cities[] | select(.code == \"$city\") | .relays[]"
elif [[ "$MULLVAD_LOCATION" =~ ^([a-z]{2}-[a-z]{3}-wg-[0-9]{3},?)+$ ]]; then
echo "[mullvad_setup] Location filter set to nodes '$MULLVAD_LOCATION'."
jq_list=$(echo "$MULLVAD_LOCATION" | sed 's/,/", "/g' | sed 's/^/"/; s/$/"/')
jq_filter=".countries[] | .cities[] | .relays[] | select(.hostname | IN($jq_list))"
else
fail "Invalid MULLVAD_LOCATION format. Expected formats: 'gb', 'gb-lon', or 'gb-lon-wg-001,nl-ams-wg-001'."
fi
# The client's tunnel address needs to be obtained.
# A call to the Mullvad API is made using the account number and the public key derived from the private key.
# The API returns a comma-delimited string with the (ipv4) assigned tunnel address as the first value.
echo "[mullvad_setup] Fetching tunnel address from Mullvad API..."
wg_pubkey="$(wg pubkey <<<"$MULLVAD_PRIVATE_KEY")"
tunnel_address_response="$(curl -sSL https://api.mullvad.net/wg -d account="$MULLVAD_ACCOUNT" --data-urlencode pubkey="$wg_pubkey")" || fail "Could not talk to Mullvad API."
tunnel_address="${tunnel_address_response%%,*}"
# Now the specified node needs to be selected.
# The API is called again to get the list of all available nodes, which is then filtered down to matching nodes.
# A random node is selected from the filtered list. If only one node matches, that one is selected.
echo "[mullvad_setup] Fetching relay information from Mullvad API..."
relay_response="$(curl -LsS https://api.mullvad.net/public/relays/wireguard/v1/)" || fail "Unable to connect to Mullvad API."
relay_fields=$(echo "$relay_response" | jq -r "$jq_filter | [.hostname, .public_key, .ipv4_addr_in] | @tsv" | sort -R | head) || fail "Failed to parse Mullvad API response. Check your MULLVAD_LOCATION value."
[[ -z "$relay_fields" ]] && fail "No relays found matching the specified location filter '$MULLVAD_LOCATION'."
# Optionally, if the user defines a network which needs routing access to services running through wireguared,
# a PostUp and PreDown hook is created to add and remove the necessary routing rules when the tunnel is brought up and down.
if [[ -n "$LAN_NETWORKS" || -n "${ALLOW_ATTACHED_NETWORKS}" ]]; then
ip_route_add=""
ip_route_delete=""
chain_route_add=""
chain_route_delete=""
if [[ -n "$LAN_NETWORKS" ]]; then
IFS=',' read -ra lan_network_array <<< "${LAN_NETWORKS}"
DROUTE=$(ip route | grep default | awk '{print $3}');
for lan_network_item in $(echo "$LAN_NETWORKS" | tr "," " "); do
echo "[mullvad_setup] Adding iptables rule for LAN network $lan_network_item"
lan_network_item=$(echo "${lan_network_item}" | sed -e 's~^[ \t]*~~;s~[ \t]*$~~')
ip_route_add+="ip route add ${lan_network_item} via ${DROUTE}; "
ip_route_delete+="ip route delete ${lan_network_item}; "
chain_route_delete+="iptables -D OUTPUT -d ${lan_network_item} -j ACCEPT; "
if [[ "${lan_network_item}" = "${lan_network_array[0]}" ]]; then
chain_route_add+="iptables -I OUTPUT -d ${lan_network_item} -j ACCEPT; "
else
chain_route_add+="iptables -A OUTPUT -d ${lan_network_item} -j ACCEPT; "
fi
done
fi
# If set, the iptables rules will be amended to allow inbound traffic from services on the same attached network(s).
# This allows other containers on the same stack or global network to access services running through the WireGuard tunnel.
if [[ "${ALLOW_ATTACHED_NETWORKS:-}" == "true" ]]; then
attached_networks=$(ip route | grep -e "link" | awk '{print $1}');
for attached_network in $attached_networks; do
echo "[mullvad_setup] Adding iptables rule for attached docker network $attached_network"
chain_route_add+="iptables -A OUTPUT -d $attached_network -j ACCEPT; "
chain_route_delete+="iptables -D OUTPUT -d $attached_network -j ACCEPT; "
done
fi
post_up_hook="${ip_route_add}${chain_route_add}iptables -A OUTPUT ! -o %i -m mark ! --mark \$(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT;"
pre_down_hook="${ip_route_delete}iptables -D OUTPUT ! -o %i -m mark ! --mark \$(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT; $chain_route_delete"
fi
IFS=$'\t' read -r WG_HOST WG_PUBKEY WG_IPADDR <<< "$relay_fields"
echo "[mullvad_setup] Generating config for chosen node $WG_HOST..."
# The selected node is used to generate the WireGuard configuration file.
configuration_file="$config_file_base/$WG_HOST.conf"
umask 077
rm -f "$configuration_file.tmp"
cat > "$configuration_file.tmp" <<-_EOF
[Interface]
PrivateKey = $MULLVAD_PRIVATE_KEY
Address = $tunnel_address
DNS = ${MULLVAD_DNS:-"10.64.0.1"}
PostUp = $post_up_hook
PreDown = $pre_down_hook
[Peer]
PublicKey = $WG_PUBKEY
Endpoint = $WG_IPADDR:51820
AllowedIPs = 0.0.0.0/0
_EOF
mv "$configuration_file.tmp" "$configuration_file"
# Finally, a symlink is created to the generated configuration file.
# Use of a link allows the container owner to check which node was selected.
echo "[mullvad_setup] Symlink created $target_link -> $configuration_file"
ln -sf "$configuration_file" "$target_link"

View File

@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-mod-wireguard-mullvad-install/run

View File

@ -1,7 +0,0 @@
#!/usr/bin/with-contenv bash
# This is an example service that would run for the mod
# It depends on init-services, the baseimage hook for start of all longrun services
exec \
s6-setuidgid abc run my app