Merge pull request #12 from Paraphraser/20220718-router-master

20220718 router master
This commit is contained in:
Lukas Prettenthaler 2022-07-28 14:46:59 +02:00 committed by GitHub
commit 030632aada
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 399 additions and 40 deletions

View File

@ -103,8 +103,8 @@ jobs:
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:latest
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
bridge:
uses: ./.github/workflows/bridge.yml
router:
uses: ./.github/workflows/router.yml
needs: push
with:
tag: ${{ github.event.release.tag_name }}

View File

@ -1,4 +1,4 @@
name: Bridge Mode build
name: Router Mode build
on:
workflow_call:
inputs:
@ -43,11 +43,11 @@ jobs:
- name: Build unstable image
if: ${{ inputs.event == 'push' }}
run: buildah bud --platform ${{ matrix.platform.name }} --build-arg FROM_IMAGE=${{ format('ghcr.io/zyclonite/{0}', env.IMAGE_NAME) }} --build-arg FROM_VERSION=main -f ./Dockerfile.bridge -t ${{ env.IMAGE_NAME }}:${{ matrix.platform.tag }} .
run: buildah bud --platform ${{ matrix.platform.name }} --build-arg FROM_IMAGE=${{ format('ghcr.io/zyclonite/{0}', env.IMAGE_NAME) }} --build-arg FROM_VERSION=main -f ./Dockerfile.router -t ${{ env.IMAGE_NAME }}:${{ matrix.platform.tag }} .
- name: Build stable image
if: ${{ inputs.event == 'release' }}
run: buildah bud --platform ${{ matrix.platform.name }} --build-arg FROM_IMAGE=${{ format('ghcr.io/zyclonite/{0}', env.IMAGE_NAME) }} --build-arg FROM_VERSION=${{ inputs.tag }} -f ./Dockerfile.bridge -t ${{ env.IMAGE_NAME }}:${{ matrix.platform.tag }} .
run: buildah bud --platform ${{ matrix.platform.name }} --build-arg FROM_IMAGE=${{ format('ghcr.io/zyclonite/{0}', env.IMAGE_NAME) }} --build-arg FROM_VERSION=${{ inputs.tag }} -f ./Dockerfile.router -t ${{ env.IMAGE_NAME }}:${{ matrix.platform.tag }} .
- name: Check images created
run: buildah images | grep '${{ env.IMAGE_NAME }}'
@ -98,16 +98,16 @@ jobs:
- name: Push unstable images
if: ${{ inputs.event == 'push' }}
run: |
buildah manifest push --all --format v2s2 --creds zyclonite:${{ secrets.REGISTRY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://docker.io/zyclonite/${{ env.IMAGE_NAME }}:bridge-main
buildah manifest push --all --creds zyclonite:${{ secrets.QUAY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://quay.io/zyclonite/${{ env.IMAGE_NAME }}:bridge-main
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:bridge-main
buildah manifest push --all --format v2s2 --creds zyclonite:${{ secrets.REGISTRY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://docker.io/zyclonite/${{ env.IMAGE_NAME }}:router-main
buildah manifest push --all --creds zyclonite:${{ secrets.QUAY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://quay.io/zyclonite/${{ env.IMAGE_NAME }}:router-main
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:router-main
- name: Push stable images
if: ${{ inputs.event == 'release' }}
run: |
buildah manifest push --all --format v2s2 --creds zyclonite:${{ secrets.REGISTRY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://docker.io/zyclonite/${{ env.IMAGE_NAME }}:bridge
buildah manifest push --all --format v2s2 --creds zyclonite:${{ secrets.REGISTRY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://docker.io/zyclonite/${{ env.IMAGE_NAME }}:bridge-${{ github.event.release.tag_name }}
buildah manifest push --all --creds zyclonite:${{ secrets.QUAY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://quay.io/zyclonite/${{ env.IMAGE_NAME }}:bridge
buildah manifest push --all --creds zyclonite:${{ secrets.QUAY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://quay.io/zyclonite/${{ env.IMAGE_NAME }}:bridge-${{ github.event.release.tag_name }}
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:bridge
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:bridge-${{ github.event.release.tag_name }}
buildah manifest push --all --format v2s2 --creds zyclonite:${{ secrets.REGISTRY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://docker.io/zyclonite/${{ env.IMAGE_NAME }}:router
buildah manifest push --all --format v2s2 --creds zyclonite:${{ secrets.REGISTRY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://docker.io/zyclonite/${{ env.IMAGE_NAME }}:router-${{ github.event.release.tag_name }}
buildah manifest push --all --creds zyclonite:${{ secrets.QUAY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://quay.io/zyclonite/${{ env.IMAGE_NAME }}:router
buildah manifest push --all --creds zyclonite:${{ secrets.QUAY_PASSWORD }} ${{ env.IMAGE_NAME }}:latest docker://quay.io/zyclonite/${{ env.IMAGE_NAME }}:router-${{ github.event.release.tag_name }}
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:router
buildah manifest push --all --creds zyclonite:${{ secrets.GITHUB_TOKEN }} ${{ env.IMAGE_NAME }}:latest docker://ghcr.io/zyclonite/${{ env.IMAGE_NAME }}:router-${{ github.event.release.tag_name }}

View File

@ -4,20 +4,20 @@ ARG FROM_VERSION=latest
FROM ${FROM_IMAGE}:${FROM_VERSION}
LABEL org.opencontainers.image.title="zerotier" \
org.opencontainers.image.version="bridge-${ZT_VERSION}" \
org.opencontainers.image.description="ZeroTier One as Docker Image" \
org.opencontainers.image.version="router-${ZT_VERSION}" \
org.opencontainers.image.description="ZeroTier One router as Docker Image" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/zyclonite/zerotier-docker"
ENV LOG_PATH=/var/log/supervisor
COPY scripts/entrypoint-bridge.sh /usr/sbin/
COPY scripts/entrypoint-router.sh /usr/sbin/
RUN apk add --no-cache --purge --clean-protected iptables \
RUN apk add --no-cache --purge --clean-protected iptables tzdata \
&& rm -rf /var/cache/apk/*
EXPOSE 9993/udp
ENTRYPOINT ["entrypoint-bridge.sh"]
ENTRYPOINT ["entrypoint-router.sh"]
CMD ["-U"]

199
README-router.md Normal file
View File

@ -0,0 +1,199 @@
## zerotier router
### Description
This is a variation built on top of the zyclonite/zerotier container which implements a local network router. It is based upon the ZeroTier Knowledge Base article:
* [Route between ZeroTier and Physical Networks](https://zerotier.atlassian.net/wiki/spaces/SD/pages/224395274/Route+between+ZeroTier+and+Physical+Networks)
Technically, this could be described as a *half-router*:
* You can initiate connections *from* a remote client *to* devices on the LAN; but
* You can't initiate connections *to* the remote client *from* devices on the LAN.
### Command line example
``` console
$ docker run --name zerotier-one --device=/dev/net/tun \
--cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SYS_ADMIN \
--env TZ=Etc/UTC --env PUID=999 -env PGID=994 \
--env ZEROTIER_ONE_LOCAL_PHYS=eth0 \
--env ZEROTIER_ONE_USE_IPTABLES_NFT=false \
--env ZEROTIER_ONE_GATEWAY_MODE=inbound \
--env ZEROTIER_ONE_NETWORK_IDS=«yourDefaultNetworkID(s)» \
-v /var/lib/zerotier-one:/var/lib/zerotier-one zyclonite/zerotier:router
```
Note:
* Environment variables that can contain multiple values should be enclosed in quotes with the components separated by spaces. Example:
``` console
--env ZEROTIER_ONE_LOCAL_PHYS="eth0 wlan0"
```
### Compose file example
``` yaml
version: '3'
services:
zerotier:
image: "zyclonite/zerotier:router"
container_name: zerotier-one
devices:
- /dev/net/tun
network_mode: host
volumes:
- '/var/lib/zerotier-one:/var/lib/zerotier-one'
cap_add:
- NET_ADMIN
- SYS_ADMIN
- NET_RAW
restart: unless-stopped
environment:
- TZ=Etc/UTC
- PUID=999
- PGID=994
- ZEROTIER_ONE_LOCAL_PHYS=eth0
- ZEROTIER_ONE_USE_IPTABLES_NFT=false
- ZEROTIER_ONE_GATEWAY_MODE=inbound
# - ZEROTIER_ONE_NETWORK_IDS=«yourDefaultNetworkID(s)»
```
Note:
* The right hand sides of environment variables should *never* be enclosed in quotes. If you need to pass multiple values, separate them with spaces. Example:
``` yaml
environment:
- ZEROTIER_ONE_LOCAL_PHYS=eth0 wlan0
```
### Environment variables
* `TZ` timezone support. Example:
``` yaml
environment:
- TZ=Australia/Sydney
```
Defaults to `Etc/UTC` if omitted.
* `PUID` + `PGID` user and group IDs for ownership of persistent store. Example:
``` yaml
environment:
- PUID=1000
- PGID=1000
```
If omitted, `PUID` defaults to user ID 999, while `PGID` defaults to group ID 994.
These variables are only used to ensure consistent ownership of persistent storage on each launch. They do not affect how the container *runs.* Absent a `user:` directive, the container runs as root and does not downgrade its privileges.
* `ZEROTIER_ONE_LOCAL_PHYS` - a space-separated list of physical interfaces that should be configured to participate in NAT-based routing. Examples:
- Use only the physical Ethernet interface (this is also the default of the variable is omitted):
``` yaml
environment:
- ZEROTIER_ONE_LOCAL_PHYS=eth0
```
- If your computer only has WiFi active (eg Raspberry Pi Zero W2):
``` yaml
environment:
- ZEROTIER_ONE_LOCAL_PHYS=wlan0
```
- If your computer has both Ethernet and WiFi interfaces active and you wish to be able to route through each interface:
``` yaml
environment:
- ZEROTIER_ONE_LOCAL_PHYS=eth0 wlan0
```
This scheme could be appropriate where the physical interfaces were:
1. In the same broadcast domain (subnet). Disconnecting Ethernet would fail-over to WiFi.
2. In different broadcast domains, such as if you allocated different subnets for Ethernet and WiFi.
* `ZEROTIER_ONE_USE_IPTABLES_NFT` - controls the command the container uses to set up NAT forwarding. Example:
``` yaml
environment:
- ZEROTIER_ONE_USE_IPTABLES_NFT=true
```
- `false` means the container uses `iptables`. This is the default.
- `true` means the container uses `iptables-nft`.
Try `true` if NAT does not seem to be working. This is needed on Raspberry Pi Bullseye.
* `ZEROTIER_ONE_GATEWAY_MODE` - controls the traffic direction. Examples:
- Only permit traffic *from* the ZeroTier cloud *to* the local physical interfaces:
``` yaml
environment:
- ZEROTIER_ONE_GATEWAY_MODE=inbound
```
- Only permit traffic *from* the local physical interfaces *to* the ZeroTier cloud:
``` yaml
environment:
- ZEROTIER_ONE_GATEWAY_MODE=outbound
```
- Permit bi-directional traffic between the local physical interfaces and the ZeroTier cloud:
``` yaml
environment:
- ZEROTIER_ONE_GATEWAY_MODE=both
```
Defaults to `inbound` if omitted. Note that you will probably need one or more static routes configured in your local LAN router so that traffic originating in a local host which is not running the ZeroTier client can be directed to the gateway host.
* `ZEROTIER_ONE_NETWORK_IDS` a space-separated list of ZeroTier network IDs.
This variable is *only* effective on first launch. There is no default if it is omitted. Examples:
- to join a single network:
``` yaml
environment:
- ZEROTIER_ONE_NETWORK_IDS=aaaaaaaaaaaaaaaa
```
Equivalent of running the following command after the container first starts:
```
$ docker exec zerotier zerotier-cli join aaaaaaaaaaaaaaaa
```
- to join a multiple networks:
``` yaml
environment:
- ZEROTIER_ONE_NETWORK_IDS=aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbb
```
Equivalent of running the following commands after the container first starts:
```
$ docker exec zerotier zerotier-cli join aaaaaaaaaaaaaaaa
$ docker exec zerotier zerotier-cli join bbbbbbbbbbbbbbbb
```
It does not matter whether you use this environment variable or the `join` command, you still need to use ZeroTier Central to approve the computer for each network it joins.
### Managed route(s)
For each ZeroTier container that is configured as a router, ZeroTier needs at least one *Managed Route*.
The [ZeroTier Wiki](https://zerotier.atlassian.net/wiki/spaces/SD/pages/224395274/Route+between+ZeroTier+and+Physical+Networks#Configure-the-ZeroTier-managed-route) explains how to design managed routes.
You configure Managed Routes in ZeroTier Central.

View File

@ -32,14 +32,11 @@ or create an empty file with the network as name
/var/lib/zerotier-one/networks.d/8056c2e21c000001.conf
#### Bridge mode
It is the implementation of the local network bridge [paper](https://zerotier.atlassian.net/wiki/spaces/SD/pages/193134593/Bridge+your+ZeroTier+and+local+network+with+a+RaspberryPi)
#### Router mode
docker run --name zerotier-one --device=/dev/net/tun \
--cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SYS_ADMIN \
-v /var/lib/zerotier-one:/var/lib/zerotier-one zyclonite/zerotier:bridge
A variation on the container which implements a local network router. See:
That will start the zero-one, establish connection and build the bridge once the `zt` interface is up.
* [router README](./README-router.md)
#### Source

23
docker-compose-router.yml Normal file
View File

@ -0,0 +1,23 @@
version: '3'
services:
zerotier:
image: "zyclonite/zerotier:router"
container_name: zerotier-one
devices:
- /dev/net/tun
network_mode: host
volumes:
- '/var/lib/zerotier-one:/var/lib/zerotier-one'
cap_add:
- NET_ADMIN
- SYS_ADMIN
- NET_RAW
restart: unless-stopped
environment:
- TZ=Etc/UTC
- PUID=999
- PGID=994
- ZEROTIER_ONE_LOCAL_PHYS=eth0
- ZEROTIER_ONE_USE_IPTABLES_NFT=false
- ZEROTIER_ONE_GATEWAY_MODE=inbound
# - ZEROTIER_ONE_NETWORK_IDS=yourNetworkID

View File

@ -25,7 +25,7 @@ spec:
containers:
- name: zerotier
image: zyclonite/zerotier:bridge
image: zyclonite/zerotier:router
resources:
limits:
memory: "128Mi"

View File

@ -1,14 +0,0 @@
#!/usr/bin/env sh
set -Eeo pipefail
if [ "${1:0:1}" = '-' ]; then
set -- zerotier-one "$@"
fi
PHY_IFACE=eth0
ZT_IFACE="zt+"
iptables -t nat -A POSTROUTING -o $PHY_IFACE -j MASQUERADE
iptables -A FORWARD -i $PHY_IFACE -o $ZT_IFACE -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i $ZT_IFACE -o $PHY_IFACE -j ACCEPT
exec "$@"

154
scripts/entrypoint-router.sh Executable file
View File

@ -0,0 +1,154 @@
#!/usr/bin/env sh
set -Eeo pipefail
if [ "${1:0:1}" = '-' ]; then
set -- zerotier-one "$@"
fi
# useful paths
CONFIG_DIR="/var/lib/zerotier-one"
NETWORKS_DIR="${CONFIG_DIR}/networks.d"
# set up network auto-join if (a) the networks directory does not exist
# and (b) the ZEROTIER_ONE_NETWORK_IDS environment variable is non-null.
if [ ! -d "${NETWORKS_DIR}" -a -n "${ZEROTIER_ONE_NETWORK_IDS}" ] ; then
echo "Assuming container first run."
mkdir -p "${NETWORKS_DIR}"
for NETWORK_ID in ${ZEROTIER_ONE_NETWORK_IDS} ; do
echo "Configuring auto-join of network ID: ${NETWORK_ID}"
touch "${NETWORKS_DIR}/${NETWORK_ID}.conf"
echo "You will need to authorize this host at:"
echo " https://my.zerotier.com/network/${NETWORK_ID}"
done
fi
# make sure permissions are always as expected (self-repair)
PUID="${PUID:-"999"}"
PGID="${PGID:-"994"}"
if [ "$(id -u)" = '0' -a -d "${CONFIG_DIR}" ]; then
chown -Rc "${PUID}:${PGID}" "${CONFIG_DIR}"
fi
# is routing enabled?
if [ $(sysctl -n net.ipv4.ip_forward) -ne 1 ] ; then
# no! there is no point in setting up rules or termination handler
echo "$(date) - IPv4 forwarding not enabled - launching ZeroTier-One in non-routing mode"
# just exec the client (this script ends here)
exec "$@"
fi
echo "$(date) - launching ZeroTier-One in routing mode"
# use an appropriate default for a local physical interface
# (using eth0 maintains backwards compatibility)
PHY_IFACES="${ZEROTIER_ONE_LOCAL_PHYS:-"eth0"}"
# default to iptables (maintains backwards compatibility)
IPTABLES_CMD=iptables
# but support an override to use iptables-nft
[ "${ZEROTIER_ONE_USE_IPTABLES_NFT}" = "true" ] && IPTABLES_CMD=iptables-nft
# the default forwarding mode is inbound (backwards compatible)
GATEWAY_MODE="${ZEROTIER_ONE_GATEWAY_MODE:-"inbound"}"
# the wildcard for the local zerotier interface is
ZT_IFACE="zt+"
# function to add and remove the requisite rules
# - $1 is either "A" (add) or "D" (delete)
# - $2 is comment
update_iptables() {
case "${GATEWAY_MODE}" in
"inbound" )
echo "$2 ${IPTABLES_CMD} rules for inbound traffic (ZeroTier to local interfaces ${PHY_IFACES})"
for PHY_IFACE in ${PHY_IFACES} ; do
${IPTABLES_CMD} -t nat -${1} POSTROUTING -o ${PHY_IFACE} -j MASQUERADE
${IPTABLES_CMD} -${1} FORWARD -i ${PHY_IFACE} -o ${ZT_IFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT
${IPTABLES_CMD} -${1} FORWARD -i ${ZT_IFACE} -o ${PHY_IFACE} -j ACCEPT
done
;;
"outbound" )
echo "$2 ${IPTABLES_CMD} rules for outbound traffic (local interfaces ${PHY_IFACES} to ZeroTier)"
${IPTABLES_CMD} -t nat -${1} POSTROUTING -o ${ZT_IFACE} -j MASQUERADE
for PHY_IFACE in ${PHY_IFACES} ; do
${IPTABLES_CMD} -${1} FORWARD -i ${ZT_IFACE} -o ${PHY_IFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT
${IPTABLES_CMD} -${1} FORWARD -i ${PHY_IFACE} -o ${ZT_IFACE} -j ACCEPT
done
;;
"both" )
echo "$2 ${IPTABLES_CMD} rules for bi-directional traffic (local interfaces ${PHY_IFACES} to/from ZeroTier)"
${IPTABLES_CMD} -t nat -${1} POSTROUTING -o ${ZT_IFACE} -j MASQUERADE
for PHY_IFACE in ${PHY_IFACES} ; do
${IPTABLES_CMD} -t nat -${1} POSTROUTING -o ${PHY_IFACE} -j MASQUERADE
${IPTABLES_CMD} -${1} FORWARD -i ${ZT_IFACE} -o ${PHY_IFACE} -j ACCEPT
${IPTABLES_CMD} -${1} FORWARD -i ${PHY_IFACE} -o ${ZT_IFACE} -j ACCEPT
done
;;
* )
echo "Warning: ZEROTIER_ONE_GATEWAY_MODE=${GATEWAY_MODE} is not supported - ignored"
;;
esac
}
# add rules to set up NAT-routing
update_iptables "A" "adding"
# define where the ZeroTier daemon will write its output (if any)
TAIL_PIPE=$(mktemp /tmp/zerotier-ipc-XXXXXX)
# start listening and echoing anything that appears there into this process
tail -f "${TAIL_PIPE}" &
# make a note of the process ID for tail
TAIL_PIPE_PID=${!}
# start the ZeroTier daemon in detached state
nohup "$@" </dev/null >"${TAIL_PIPE}" 2>&1 &
# make a note of the process ID
ZEROTIER_DAEMON_PID=${!}
# report
echo "$(date) - ZeroTier daemon is running as process ${ZEROTIER_DAEMON_PID}"
# function to handle cleanup
termination_handler() {
echo "$(date) - terminating ZeroTier-One"
# remove rules
update_iptables "D" "removing"
# relay the termination message to the daemon
if [ -d "/proc/${ZEROTIER_DAEMON_PID}" ] ; then
kill -TERM ${ZEROTIER_DAEMON_PID}
wait ${ZEROTIER_DAEMON_PID}
fi
# tell the pipe listener to go away too
if [ -d "/proc/${TAIL_PIPE_PID}" ] ; then
kill -TERM ${TAIL_PIPE_PID}
wait ${TAIL_PIPE_PID}
fi
# clean up the pipe file
rm "${TAIL_PIPE}"
}
# set up termination handler (usually catches TERM)
trap termination_handler INT TERM HUP
# suspend this script while the zerotier daemon is running
wait ${ZEROTIER_DAEMON_PID}
# would not usually expect to arrive here inside a Docker container but
# it can happen if the user does a "sudo killall zerotier-one" rather
# that use Docker commands
echo "$(date) - the ZeroTier daemon has quit unexpectedly - cleaning up"
# run the termination handler
termination_handler