#!/usr/bin/env bash root=${PORTICAL_UPNP_ROOT_URL} duration=${PORTICAL_POLL_INTERVAL-15} label="portical.upnp.forward" verbose=false force=false while (( "$#" )); do case $1 in -r | --root) root="$2"; shift 2 ;; -d | --duration) duration="$2"; shift 2 ;; -l | --label) label="$2"; shift 2 ;; -v | --verbose) verbose=true; shift ;; -f | --force) force=true; shift ;; *) break ;; esac done function run_cmd() { if $verbose; then "$@" else "$@" > /dev/null 2>&1 fi local status=$? if [ $status -ne 0 ]; then echo "Error: Command '$*' failed with status $status" exit $status fi } function forward() { local network_driver=$1 local external_port=$2 local internal_port=$3 local protocol=$4 local machine=$5 local description="portical: (${external_port}:${internal_port}/${protocol}) ${machine}" echo "Setting up ${description}..." case $network_driver in macvlan | ipvlan) local network="container:${machine}" ;; host | bridge) local network="host" ;; *) echo "Unsupported network driver: ${network_driver}. Skipping..." return ;; esac if [ "$force" == false ] && [[ $(upnpc ${root:+-u "$root"} -l) == *"${description}"* ]]; then echo "Rule already exists. Skipping..." return else echo -n "Removing existing rule... " run_cmd docker run --rm --network ${network} danielbodart/portical \ upnpc ${root:+-u "$root"} -d "${external_port}" "${protocol}" echo "DONE" fi echo -n "Adding new rule... " run_cmd docker run --rm --network ${network} danielbodart/portical \ upnpc ${root:+-u "$root"} -e "${description}" -r "${internal_port}" "${external_port}" "${protocol}" echo "DONE" } function process_container() { local container=$1 label_value=$(docker inspect --format "{{ index .Config.Labels \"${label}\" }}" "$container") network_name=$(docker inspect -f '{{range $key, $_ := .NetworkSettings.Networks}}{{ $key }}{{end}}' "$container") network_driver=$(docker network inspect -f '{{.Driver}}' "$network_name") if [[ ${label_value} == "published" ]]; then echo "Extracting published port for ${container}... " label_value="$(docker inspect --format='{{ range $key, $value := .NetworkSettings.Ports }}{{ range $value }}{{.HostPort}}:{{ $key }} {{ end }}{{ end }}' "${container}" | sort | uniq | tr '\n' ',' | sed 's/:[0-9]*//' )" fi IFS=',' read -ra rules <<< "${label_value}" for rule in "${rules[@]}"; do if [[ ${rule} =~ ([0-9]+)(:([0-9]+))?(\/(tcp|udp))? ]]; then external_port="${BASH_REMATCH[1]}" internal_port="${BASH_REMATCH[3]:-${external_port}}" protocol="${BASH_REMATCH[5]}" if [[ $protocol == "" ]]; then forward "$network_driver" "$external_port" "$internal_port" tcp "$container" forward "$network_driver" "$external_port" "$internal_port" udp "$container" else forward "$network_driver" "$external_port" "$internal_port" "$protocol" "$container" fi fi done } function update() { echo "Finding all containers with label '${label}' set..." docker ps --filter "label=${label}" --format '{{.Names}}' | while read container; do process_container "$container" done echo "All portical rules:" upnpc ${root:+-u "$root"} -l | grep portical } function listen() { # Do an initial update just for good measure update echo "Listening for Docker 'start' events with label '${label}'..." docker events --filter "label=${label}" --filter "type=container" --filter "event=start" --format '{{.Actor.Attributes.name}}' | while read container; do process_container "$container" done } function poll() { while true; do update echo "Sleeping for ${duration} seconds..." sleep ${duration} done } command="${1-update}" shift if declare -f "$command" > /dev/null; then set +e "$command" "$@" else echo "Error: '$command' is not a valid command." >&2 exit 1 fi