Skip to content

Instantly share code, notes, and snippets.

@deric
Last active March 15, 2023 12:58
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save deric/b52628663e7758deec0a4ab5ab4e5908 to your computer and use it in GitHub Desktop.
Save deric/b52628663e7758deec0a4ab5ab4e5908 to your computer and use it in GitHub Desktop.
Generate iptables port forwarding for running containers (assumes default Docker chains already exists)
#!/bin/bash
# Copyright 2020-2022 Tomas Barton
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -o nounset -o pipefail
#
function -h {
cat <<USAGE
Generate iptables rules for running docker containers. Use
$(basename $0) -v -n
to inspect iptables rules without applying changes.
USAGE:
-b / --binary iptables binary
-i / --interface Docker virtual interface, default: docker0
-d / --debug Debugging output
-n / --noop Dry run / no iptables rule is applied
-v / --verbose Detailed output
USAGE
}; function --help { -h ;}
function msg { out "$*" >&1 ;}
function out { printf '%s\n' "$*" ;}
function iptables_apply {
local binary="$1"
local table="$2"
local action="$3"
local rule="$4"
local noop=$5
local verbose=$6
# check if the rule is already defined
eval "${binary} -t ${table} --check ${rule} 2>/dev/null"
if [[ $? -ne 0 ]]; then
if [[ $noop == true ]]; then
msg $rule;
else
if [[ $verbose == true ]]; then
msg "${rule}"
fi
eval "${binary} -t ${table} ${action} ${rule}";
fi
fi
}
function main {
local verbose=false
local debug=false
local noop=false
local interface="docker0"
local binary="iptables"
while [[ $# -gt 0 ]]
do
case "$1" in # Munging globals, beware
-i|--interface) interface="$2"; shift 2 ;;
-b|--binary) binary="$2"; shift 2 ;;
-n|--noop) noop=true; shift 1 ;;
-v|--verbose) verbose=true; shift 1 ;;
-d|--debug) debug=true; shift 1 ;;
*) err 'Argument error. Please see help: -h' ;;
esac
done
if [[ $debug == true ]]; then
set -x
fi
if [[ $noop == true ]]; then
msg "NOOP: Only printing iptables rules to be eventually applied"
fi
# list currently running container IDs
local containers=$(docker ps --format '{{.ID}}')
if [[ ! -z "$containers" ]]; then
while read -r cont; do
# old docker API response
local ip=$(docker inspect -f '{{.NetworkSettings.IPAddress}}' ${cont})
if [[ -z "${ip}" ]]; then
# newer docker API, probably > 23.01
ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${cont})
fi
if [[ $verbose == true ]]; then
msg "Container ${cont}"
fi
# extract port forwarding
local ports=$(docker inspect -f '{{json .NetworkSettings.Ports}}' ${cont})
if [[ "${ports}" != "{}" ]]; then
local fwd=$(echo "${ports}" | jq -r '. as $a| keys[] | select($a[.]!=null) as $f | "\($f)/\($a[$f][].HostPort)"')
if [[ ! -z "$fwd" ]]; then
# pass tripples likes `3000/tcp/29956`
while read -r pfwd; do
local dport protocol hport
local IFS="/"
read dport protocol hport <<< "${pfwd}"
if [[ -z "${ip}" ]]; then
err "ERROR: Empty IP for container: ${cont}"
fi
local rule="DOCKER -d ${ip}\/32 ! -i ${interface} -o ${interface} -p ${protocol} -m ${protocol} --dport ${dport} -j ACCEPT"
iptables_apply "${binary}" "filter" "-A" "${rule}" ${noop} ${verbose}
rule="POSTROUTING -s ${ip}\/32 -d ${ip}\/32 -p ${protocol} -m ${protocol} --dport ${dport} -j MASQUERADE"
iptables_apply "${binary}" "nat" "-A" "${rule}" ${noop} ${verbose}
rule="DOCKER ! -i ${interface} -p ${protocol} -m ${protocol} --dport ${hport} -j DNAT --to-destination ${ip}:${dport}"
iptables_apply "${binary}" "nat" "-A" "${rule}" ${noop} ${verbose}
done <<< "$fwd"
fi
fi
done <<< "$containers"
fi
}
if [[ ${1:-} ]] && declare -F | cut -d' ' -f3 | fgrep -qx -- "${1:-}"
then
case "$1" in
-h|--help) : ;;
*) ;;
esac
"$@"
else
main "$@"
fi
@panomitrius
Copy link

panomitrius commented Dec 18, 2020

Cool, thanks for this script. However it generates a lot of jq: error (at <stdin>:1): Cannot iterate over null (null) when run with -n, and lots of iptables v1.8.4 (legacy): host/network ' not found` when run in regular mode.

@deric
Copy link
Author

deric commented Dec 18, 2020

Could you check the port mapping for such case? Does it look like this?

            "Ports": {
                "80/tcp": null
            },

In such case the port doesn't have external mapping, thus no extra iptables rule needed (could be silently ignored).

@deric
Copy link
Author

deric commented Dec 18, 2020

I'm not really a jq programmer :) The issue is here:

docker inspect -f '{{json .NetworkSettings.Ports}}' ce92eb1cd3d0 | jq -r 'keys[] as $k | "\($k)/\(.[$k] | .[] .HostPort)"'

In fact this part is safe:

docker inspect -f '{{json .NetworkSettings.Ports}}' ce92eb1cd3d0 | jq -r 'keys[] as $k

but we can't read values .[$k] if the value is null:

"\($k)/\(.[$k] | .[] .HostPort)"

so we have to filter keys with null values:

select($a[.]!=null)

@deric
Copy link
Author

deric commented Dec 18, 2020

@panomitrius let me know if the updated version works for you. I don't know how to reproduce the host/network issue, I would need more information about that.

@panomitrius
Copy link

Thank's for a quick reply! It still doesn't run with correct host/network, for example the full output of the last error message (ran with -v) is:

Container 5f9b5f48da05
DOCKER -d \/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9000 -j ACCEPT
iptables v1.8.4 (legacy): host/network ' not found Try iptables -h' or 'iptables --help' for more information.
POSTROUTING -s \/32 -d \/32 -p tcp -m tcp --dport 9000 -j MASQUERADE
iptables v1.8.4 (legacy): host/network ' not found`

@deric
Copy link
Author

deric commented Dec 19, 2020

@panomitrius And what's the name of your docker interface? have you tied using -i flag?

@panomitrius
Copy link

@panomitrius And what's the name of your docker interface? have you tied using -i flag?

It's the default docker0..

@deric
Copy link
Author

deric commented Dec 21, 2020

Which version of Docker do you use? Could you share the docker ps --format '{{json .}}' output?

@panomitrius
Copy link

docker --version gives: Docker version 20.10.1, build 831ebea

docker ps --format '{{json .}}' gives a really long output. I'll share the output of one of the containers here:

{"Command":"\"docker-entrypoint.s…\"","CreatedAt":"2020-12-19 03:41:44 +0100 CET","ID":"13f4ddebf7a0","Image":"rocketchat/rocket.chat:latest","Labels":"com.docker.compose.oneoff=False,com.docker.compose.project.config_files=docker-compose.yml,com.docker.compose.version=1.27.4,maintainer=buildmaster@rocket.chat,traefik.backend=rocketchat,traefik.frontend.rule=Host: your.domain.tld,com.docker.compose.config-hash=5671c0526c55cb391d4db681e58eb2297eedbf58ef77bd10b5728a75ec764ccc,com.docker.compose.container-number=1,com.docker.compose.project=rocketchat-ld,com.docker.compose.project.working_dir=/var/docker/rocketchat-ld,com.docker.compose.service=rocketchatLD","LocalVolumes":"0","Mounts":"/var/docker/ro…","Names":"rocketchatLD","Networks":"rocketchat-ld_default","Ports":"3000/tcp, 0.0.0.0:3100-\u003e3100/tcp","RunningFor":"2 days ago","Size":"0B (virtual 1.47GB)","State":"running","Status":"Up 28 hours"}

@deric
Copy link
Author

deric commented Jan 4, 2021

@panomitrius Right, that's fairly recent release. I've tested the script only on Docker versions up to 19.03.x releases. Probably Docker changed the format of the response in the latest release.

@panomitrius
Copy link

Alright, thanks for your time @deric. I found this script that works for my purposes :)

@fdurand
Copy link

fdurand commented May 2, 2022

@deric can i ask you what licence do you have for this code and can i use it in a opensource project ?

@deric
Copy link
Author

deric commented May 2, 2022

@fdurand Sure, you can use it. I usually don't include license header on a single script. Apache 2 license would be ok?

@fdurand
Copy link

fdurand commented May 3, 2022

@deric Yes Apache 2 License is perfect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment