From f247a676fd8d526ba7fb84d46af9f72c9db68954 Mon Sep 17 00:00:00 2001 From: Menno van Leeuwen Date: Mon, 11 Nov 2024 14:32:27 +0100 Subject: [PATCH] initial commit --- .github/workflows/docker-publish.yml | 92 ++++++++++++++++++++++++++++ .gitignore | 2 + Dockerfile | 15 +++++ LICENSE | 21 +++++++ README.md | 68 ++++++++++++++++++++ docker-compose.yml | 26 ++++++++ entrypoint.sh | 79 ++++++++++++++++++++++++ 7 files changed, 303 insertions(+) create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..8361b67 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,92 @@ +name: Docker Build and Publish + +on: + release: + types: [published] + push: + branches: [ "main" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "main" ] + +env: + # Use github.repository as / + IMAGE_NAME: ${{ github.repository }} + REGISTRY: ghcr.io + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v3.3.0 + with: + cosign-release: 'v2.1.1' + + # Set up BuildKit Docker container builder to enable modern features + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Login against a Docker registry + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + # Build and push Docker image with Buildx + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: github.event_name != 'pull_request' + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..090a1f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9c3ec46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM alpine:latest + +LABEL maintainer="Menno van Leeuwen " +LABEL description="A flexible UPnP port forwarding container that handles automatic port mapping and renewal" +LABEL repository="https://github.com/vleeuwenmenno/docker-auto-upnp" + +RUN apk --no-cache add \ + bash \ + miniupnpc \ + jq + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..735858c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Menno van Leeuwen + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a455d6 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Auto UPNP + +A simple Docker container for automatic UPnP port forwarding. This container automatically forwards ports on your router with the use of upnpc. + +## Features + +- JSON-based port configuration +- Automatic port mapping renewal +- Configurable mapping duration +- Built on Alpine Linux for minimal footprint + +## Usage + +### Quick Start + +```yaml +services: + upnp: + image: ghcr.io/vleeuwenmenno/auto-upnp:latest + network_mode: host + environment: + - PORTS=[{"port": 80, "protocol": "tcp"}, {"port": 443, "protocol": "tcp"}] +``` + +### Environment Variables + +- `PORTS`: JSON array of port mappings (required) + - Format: `[{"port": number, "protocol": "tcp|udp"}, ...]` +- `UPNP_DURATION`: Renewal interval for port mappings (default: `86400` seconds/24 hours) + +### Example Configurations + +#### Game Server (Satisfactory) + +```yaml +environment: + PORTS: | + [ + {"port": 7777, "protocol": "udp"}, + {"port": 15000, "protocol": "udp"}, + {"port": 15777, "protocol": "udp"} + ] +``` + +#### Web Server + +```yaml +environment: + PORTS: | + [ + {"port": 80, "protocol": "tcp"}, + {"port": 443, "protocol": "tcp"} + ] +``` + +## Building + +```bash +docker build -t auto-upnp . +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f81a025 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +name: auto-upnp +services: + upnp: + # image: ghcr.io/vleeuwenmenno/auto-upnp:latest + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + network_mode: host + environment: + UPNP_DURATION: 86400 # 24 hours in seconds + PORTS: | + [ + {"port": 7777, "protocol": "udp"}, + {"port": 15000, "protocol": "udp"}, + {"port": 15777, "protocol": "udp"}, + {"port": 27015, "protocol": "tcp"}, + {"port": 27015, "protocol": "udp"}, + {"port": 27031, "protocol": "udp"}, + {"port": 27032, "protocol": "udp"}, + {"port": 27033, "protocol": "udp"}, + {"port": 27034, "protocol": "udp"}, + {"port": 27035, "protocol": "udp"}, + {"port": 27036, "protocol": "tcp"}, + {"port": 27036, "protocol": "udp"} + ] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d3da1e9 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Default values +DURATION=${UPNP_DURATION:-86400} # 24 hours in seconds +REFRESH_INTERVAL=$((DURATION * 80 / 100)) # Refresh at 80% of duration +DEFAULT_PORTS='[]' # Empty array as default + +# Function to validate JSON array +validate_ports() { + if ! echo "$1" | jq empty; then + echo "Error: PORTS environment variable must be a valid JSON array" + exit 1 + fi +} + +# Function to add a single port mapping +add_port_mapping() { + local port=$1 + local protocol=$2 + + echo "Adding mapping for port $port/$protocol..." + upnpc -a "" "$port" "$port" "$protocol" "$DURATION" + local status=$? + + if [ $status -eq 0 ]; then + echo "Successfully mapped port $port/$protocol" + else + echo "Failed to map port $port/$protocol" + fi + + return $status +} + +# Function to remove a single port mapping +remove_port_mapping() { + local port=$1 + local protocol=$2 + + echo "Removing mapping for port $port/$protocol..." + upnpc -d "$port" "$protocol" +} + +# Function to add all port mappings +add_port_mappings() { + local ports_json=${PORTS:-$DEFAULT_PORTS} + validate_ports "$ports_json" + + echo "Adding/Refreshing port mappings..." + + # Iterate through the JSON array + echo "$ports_json" | jq -r '.[] | "\(.port) \(.protocol)"' | while read -r port protocol; do + add_port_mapping "$port" "$protocol" + done + + # List current mappings + echo "Current port mappings:" + upnpc -l +} + +# Function to remove all port mappings +remove_port_mappings() { + local ports_json=${PORTS:-$DEFAULT_PORTS} + + echo "Removing all port mappings..." + + # Iterate through the JSON array + echo "$ports_json" | jq -r '.[] | "\(.port) \(.protocol)"' | while read -r port protocol; do + remove_port_mapping "$port" "$protocol" + done +} + +# Trap SIGTERM and SIGINT +trap "remove_port_mappings; exit 0" TERM INT + +# Main loop +while true; do + add_port_mappings + sleep "$REFRESH_INTERVAL" +done \ No newline at end of file