diff --git a/Dockerfile b/Dockerfile index b63c63e..d3aa386 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:24.04 -ARG docker_version="27.3.1" +ARG docker_version="28.3.1" ARG use_iptables_legacy=false ENV DEBIAN_FRONTEND=noninteractive diff --git a/README.md b/README.md index 1d836f1..ff18e7a 100644 --- a/README.md +++ b/README.md @@ -681,6 +681,8 @@ UFW 是 Ubuntu 上很流行的一个 iptables 前端,可以非常方便的管 ufw-docker service delete allow web + ufw-docker service delete allow web 80/tcp + ### 试试 我们使用 [Vagrant](https://www.vagrantup.com/) 来创建一个本地的测试环境。 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 29e6ed4..d385a5d 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash set -euo pipefail -[[ -n "${DEBUG:-}" ]] && set -x +[[ "${DEBUG:-}" = true ]] && set -x [[ 0 -eq "$#" ]] && set -- start ufw_docker_agent=ufw-docker-agent @@ -10,8 +10,12 @@ function ufw-allow-or-deny-service() { declare id="$1" declare port="$2" - if [[ "$port" = deny ]]; then - run-ufw-docker delete allow "$id" + if [[ "$port" = deny || "$port" = */deny ]]; then + port="${port%deny}" + port="${port%/}" + declare -a opts=("$id") + [[ -z "$port" ]] || opts+=("$port") + run-ufw-docker delete allow "${opts[@]}" else run-ufw-docker add-service-rule "$id" "$port" fi @@ -22,8 +26,14 @@ function update-ufw-rules() { -e 's/^declare -x ufw_public_//' \ -e 's/="/ /' \ -e 's/"$//' | - while read -r id port; do - ufw-allow-or-deny-service "${id}" "${port#*/}" + while read -r id ruleset; do + declare -a rules=( $(tr ',' '\n' <<< "$ruleset") ) + for rule in "${rules[@]}"; do + [[ "$rule" = */deny ]] && ufw-allow-or-deny-service "${id}" "${rule#*/}" + done + for rule in "${rules[@]}"; do + [[ "$rule" = */deny ]] || ufw-allow-or-deny-service "${id}" "${rule#*/}" + done done } diff --git a/test/bach b/test/bach index 27885eb..871528a 160000 --- a/test/bach +++ b/test/bach @@ -1 +1 @@ -Subproject commit 27885eb79c11e4652dede994c886ae5f9e30994f +Subproject commit 871528a805b21e3c4432e4c23cacc3c4f4ddfc39 diff --git a/test/ufw-docker-service.test.sh b/test/ufw-docker-service.test.sh index 0a9125c..42c12f1 100755 --- a/test/ufw-docker-service.test.sh +++ b/test/ufw-docker-service.test.sh @@ -6,9 +6,8 @@ source "$working_dir"/bach/bach.sh @setup { set -euo pipefail - ufw_docker_agent=ufw-docker-agent - ufw_docker_agent_image=chaifeng/ufw-docker-agent:181005 + ufw_docker_agent_image=chaifeng/ufw-docker-agent:090502 } @setup-test { @@ -19,9 +18,17 @@ source "$working_dir"/bach/bach.sh @ignore echo @ignore err - DEFAULT_PROTO=tcp - GREP_REGEXP_INSTANCE_NAME="[-_.[:alnum:]]\\+" + UFW_DOCKER_AGENT_IMAGE=chaifeng/ufw-docker-agent:090502 + builtin source <(@sed -n -e '/^# UFW-DOCKER GLOBAL VARIABLES START #$/,/^# UFW-DOCKER GLOBAL VARIABLES END #$/{' -e '/^PATH=/d' -e 'p' -e '}' "$working_dir/../ufw-docker") + DEBUG=false + + unset RANDOM + RANDOM=42 + + @allow-real sed -e '/^ufw_public_/!d' -e 's/^ufw_public_//' -e 's/=/ /' + @allow-real tr ',' '\n' + @allow-real grep -E '^[0-9]+(/(tcp|udp))?$' } function die() { @@ -110,6 +117,26 @@ test-ufw-docker--service-delete-allow-webapp-assert() { } +test-ufw-docker--service-delete-allow-webapp-8080-tcp() { + load-ufw-docker-function ufw-docker--service + + ufw-docker--service delete allow webapp 8080/tcp +} +test-ufw-docker--service-delete-allow-webapp-8080-tcp-assert() { + ufw-docker--service-delete webapp 8080/tcp +} + + +test-ufw-docker--service-delete-allow-webapp-8080() { + load-ufw-docker-function ufw-docker--service + + ufw-docker--service delete allow webapp 8080 +} +test-ufw-docker--service-delete-allow-webapp-8080-assert() { + ufw-docker--service-delete webapp 8080 +} + + test-ufw-docker--get-service-id() { load-ufw-docker-function ufw-docker--get-service-id ufw-docker--get-service-id database @@ -129,8 +156,6 @@ test-ufw-docker--get-service-name-assert() { test-ufw-docker--service-allow-invalid-port-syntax() { - @mockfalse grep -E '^[0-9]+(/(tcp|udp))?$' - load-ufw-docker-function ufw-docker--service-allow ufw-docker--service-allow webapp invalid-port } @@ -141,7 +166,6 @@ test-ufw-docker--service-allow-invalid-port-syntax-assert() { test-ufw-docker--service-allow-an-non-existed-service() { - @mocktrue grep -E '^[0-9]+(/(tcp|udp))?$' @mock ufw-docker--get-service-id web404 === @stdout "" load-ufw-docker-function ufw-docker--service-allow @@ -154,7 +178,6 @@ test-ufw-docker--service-allow-an-non-existed-service-assert() { test-ufw-docker--service-allow-a-service-without-ports-published() { - @mocktrue grep -E '^[0-9]+(/(tcp|udp))?$' @mock ufw-docker--get-service-id private-web === @stdout abcd1234 @mock ufw-docker--get-service-name private-web === @stdout private-web @mock ufw-docker--list-service-ports private-web === @stdout "" @@ -169,7 +192,6 @@ test-ufw-docker--service-allow-a-service-without-ports-published-assert() { test-ufw-docker--service-allow-a-service-while-agent-not-running() { - @mocktrue grep -E '^[0-9]+(/(tcp|udp))?$' @mock ufw-docker--get-service-id webapp === @stdout abcd1234 @mock ufw-docker--get-service-name webapp === @stdout webapp @mock ufw-docker--list-service-ports webapp === @stdout "53 53/udp" "80 80/tcp" "8080 8080/tcp" @@ -182,15 +204,14 @@ test-ufw-docker--service-allow-a-service-while-agent-not-running-assert() { docker service create --name ufw-docker-agent --mode global \ --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ --mount type=bind,source=/etc/ufw,target=/etc/ufw,readonly=true \ - --env ufw_docker_agent_image="chaifeng/ufw-docker-agent:181005" \ + --env ufw_docker_agent_image="chaifeng/ufw-docker-agent:090502" \ --env DEBUG="false" \ --env "ufw_public_abcd1234=webapp/80/tcp" \ - "chaifeng/ufw-docker-agent:181005" + "chaifeng/ufw-docker-agent:090502" } test-ufw-docker--service-allow-a-service-add-new-env() { - @mocktrue grep -E '^[0-9]+(/(tcp|udp))?$' @mock ufw-docker--get-service-id webapp === @stdout abcd1234 @mock ufw-docker--get-service-name webapp === @stdout webapp @mock ufw-docker--list-service-ports webapp === @stdout "53 53/udp" "80 80/tcp" "8080 8080/tcp" @@ -202,16 +223,15 @@ test-ufw-docker--service-allow-a-service-add-new-env() { } test-ufw-docker--service-allow-a-service-add-new-env-assert() { docker service update --update-parallelism=0 \ - --env-add ufw_docker_agent_image="chaifeng/ufw-docker-agent:181005" \ + --env-add ufw_docker_agent_image="chaifeng/ufw-docker-agent:090502" \ --env-add DEBUG="false" \ --env-add "ufw_public_abcd1234=webapp/80/tcp" \ - --image "chaifeng/ufw-docker-agent:181005" \ + --image "chaifeng/ufw-docker-agent:090502" \ ufw-docker-agent } test-ufw-docker--service-allow-a-service-update-a-env() { - @mocktrue grep -E '^[0-9]+(/(tcp|udp))?$' @mock ufw-docker--get-service-id webapp === @stdout abcd1234 @mock ufw-docker--get-service-name webapp === @stdout webapp @mock ufw-docker--list-service-ports webapp === @stdout "53 53/udp" "80 80/tcp" "8080 8080/tcp" @@ -223,11 +243,60 @@ test-ufw-docker--service-allow-a-service-update-a-env() { } test-ufw-docker--service-allow-a-service-update-a-env-assert() { docker service update --update-parallelism=0 \ - --env-add ufw_docker_agent_image="chaifeng/ufw-docker-agent:181005" \ + --env-add ufw_docker_agent_image="chaifeng/ufw-docker-agent:090502" \ --env-add DEBUG="false" \ - --env-add "ufw_public_abcd1234=webapp/80/tcp" \ --env-rm "ufw_public_a_different_id" \ - --image "chaifeng/ufw-docker-agent:181005" \ + --env-add "ufw_public_abcd1234=webapp/80/tcp" \ + --image "chaifeng/ufw-docker-agent:090502" \ + ufw-docker-agent +} + + +test-ufw-docker--service-allow-a-service-add-value-to-an-env() { + @mock ufw-docker--get-service-id webapp === @stdout abcd1234 + @mock ufw-docker--get-service-name webapp === @stdout webapp + @mock ufw-docker--list-service-ports webapp === @stdout "5353 53/udp" "8080 80/tcp" "18080 8080/tcp" + @mocktrue docker service inspect ufw-docker-agent + @mock ufw-docker--get-env-list === @stdout "a_different_id webapp/8080/tcp" "abcd1234 webapp/5353/udp" + + load-ufw-docker-function ufw-docker--service-allow + ufw-docker--service-allow webapp 80/tcp + ufw-docker--service-allow webapp 8080/tcp +} +test-ufw-docker--service-allow-a-service-add-value-to-an-env-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="chaifeng/ufw-docker-agent:090502" \ + --env-add DEBUG="false" \ + --env-rm "ufw_public_a_different_id" \ + --env-add "ufw_public_abcd1234=webapp/8080/tcp,webapp/5353/udp" \ + --image "chaifeng/ufw-docker-agent:090502" \ + ufw-docker-agent + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="chaifeng/ufw-docker-agent:090502" \ + --env-add DEBUG="false" \ + --env-rm "ufw_public_a_different_id" \ + --env-add "ufw_public_abcd1234=webapp/18080/tcp,webapp/5353/udp" \ + --image "chaifeng/ufw-docker-agent:090502" \ + ufw-docker-agent +} + +test-ufw-docker--service-allow-a-service-denied-port() { + @mock ufw-docker--get-service-id webapp === @stdout abcd1234 + @mock ufw-docker--get-service-name webapp === @stdout webapp + @mock ufw-docker--list-service-ports webapp === @stdout "5353 53/udp" "8080 80/tcp" "18080 8080/tcp" + @mocktrue docker service inspect ufw-docker-agent + @mock ufw-docker--get-env-list === @stdout "a_different_id webapp/8080/tcp" "abcd1234 webapp/8080/tcp/deny" "abcd1234 webapp/5353/udp" + + load-ufw-docker-function ufw-docker--service-allow + ufw-docker--service-allow webapp 80/tcp +} +test-ufw-docker--service-allow-a-service-denied-port-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="chaifeng/ufw-docker-agent:090502" \ + --env-add DEBUG="false" \ + --env-rm "ufw_public_a_different_id" \ + --env-add "ufw_public_abcd1234=webapp/8080/tcp,webapp/5353/udp" \ + --image "chaifeng/ufw-docker-agent:090502" \ ufw-docker-agent } @@ -236,27 +305,50 @@ test-ufw-docker--get-env-list() { @mock docker service inspect ufw-docker-agent \ --format '{{range $k,$v := .Spec.TaskTemplate.ContainerSpec.Env}}{{ $v }}{{"\n"}}{{end}}' \ === @stdout \ - "ufw_docker_agent_image=192.168.56.130:5000/chaifeng/ufw-docker-agent:test" \ - "DEBUG=true" \ - "ufw_public_zv6esvmwnmmgnlauqn7m77jo4=webapp/9090/tcp" \ - "OTHER_ENV=blabla" + "ufw_docker_agent_image=192.168.56.130:5000/chaifeng/ufw-docker-agent:test" \ + "DEBUG=true" \ + "ufw_public_id111111=webapp/9090/tcp" \ + "ufw_public_id222222=foo/2222/udp" \ + "OTHER_ENV=blabla" - @mock sed -e '/^ufw_public_/!d' \ - -e 's/^ufw_public_//' \ - -e 's/=/ /' === @real sed -e '/^ufw_public_/!d' \ - -e 's/^ufw_public_//' \ - -e 's/=/ /' + @allow-real sed "s/^/id111111 /g" + @allow-real sed "s/^/id222222 /g" load-ufw-docker-function ufw-docker--get-env-list ufw-docker--get-env-list } test-ufw-docker--get-env-list-assert() { - @stdout "zv6esvmwnmmgnlauqn7m77jo4 webapp/9090/tcp" + @stdout "id111111 webapp/9090/tcp" + @stdout "id222222 foo/2222/udp" +} + +test-ufw-docker--get-env-list-multiple() { + @mock docker service inspect ufw-docker-agent \ + --format '{{range $k,$v := .Spec.TaskTemplate.ContainerSpec.Env}}{{ $v }}{{"\n"}}{{end}}' \ + === @stdout \ + "ufw_docker_agent_image=192.168.56.130:5000/chaifeng/ufw-docker-agent:test" \ + "DEBUG=true" \ + "ufw_public_id111111=webapp/9090/tcp,webapp/8888/tcp,webapp/5555/udp" \ + "ufw_public_id222222=foo/2222/udp,foo/3333/tcp" \ + "OTHER_ENV=blabla" + + @allow-real sed "s/^/id111111 /g" + @allow-real sed "s/^/id222222 /g" + + load-ufw-docker-function ufw-docker--get-env-list + ufw-docker--get-env-list +} +test-ufw-docker--get-env-list-multiple-assert() { + @stdout "id111111 webapp/9090/tcp" + @stdout "id111111 webapp/8888/tcp" + @stdout "id111111 webapp/5555/udp" + @stdout "id222222 foo/2222/udp" + @stdout "id222222 foo/3333/tcp" } test-ufw-docker--service-delete-no-matches() { - @mock ufw-docker--get-env-list === @stdout "ffff111 foo/80/tcp" "eeee2222 bar/53/udp" + @mockfalse ufw-docker--get-service-id webapp load-ufw-docker-function ufw-docker--service-delete ufw-docker--service-delete webapp @@ -266,9 +358,15 @@ test-ufw-docker--service-delete-no-matches-assert() { @fail } +function mock-abcd1234-webapp() { + @mock ufw-docker--get-service-name webapp === @stdout webapp + @mock ufw-docker--get-service-id webapp === @stdout "abcd1234" + @mock ufw-docker--list-service-ports webapp === @stdout "22 2222/tcp" "80 8080/tcp" "53 5353/udp" +} test-ufw-docker--service-delete-matches() { - @mock ufw-docker--get-env-list === @stdout "ffff111 foo/80/tcp" "eeee2222 bar/53/udp" "abcd1234 webapp/5000/tcp" + mock-abcd1234-webapp + @mock ufw-docker--get-env-list === @stdout "xxx 888/tcp" "abcd1234 webapp/22/tcp" load-ufw-docker-function ufw-docker--service-delete ufw-docker--service-delete webapp @@ -282,6 +380,130 @@ test-ufw-docker--service-delete-matches-assert() { "${ufw_docker_agent}" } +test-ufw-docker--service-delete-matches2() { + mock-abcd1234-webapp + @mock ufw-docker--get-env-list === @stdout "xxx 888/tcp" "abcd1234 webapp/22/tcp" "abcd1234 webapp/53/udp" "abcd1234 webapp/80/tcp" + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp +} +test-ufw-docker--service-delete-matches2-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ + --env-add "ufw_public_abcd1234=webapp/deny" \ + --env-add "DEBUG=false" \ + --image "${ufw_docker_agent_image}" \ + "${ufw_docker_agent}" +} + + +test-ufw-docker--service-delete-matches-with-a-port() { + mock-abcd1234-webapp + @mock ufw-docker--get-env-list === @stdout "xxx 888/tcp" "abcd1234 webapp/80/tcp" + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp 8080 +} +test-ufw-docker--service-delete-matches-with-a-port-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ + --env-add "ufw_public_abcd1234=webapp/80/tcp/deny" \ + --env-add "DEBUG=false" \ + --image "${ufw_docker_agent_image}" \ + "${ufw_docker_agent}" +} + + +test-ufw-docker--service-delete-matches-with-a-port2() { + mock-abcd1234-webapp + @mock ufw-docker--get-env-list === @stdout "xxx 888/tcp" "abcd1234 webapp/80/tcp" "abcd1234 webapp/53/udp" "abcd1234 webapp/53/tcp" + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp 8080 +} +test-ufw-docker--service-delete-matches-with-a-port2-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ + --env-add "ufw_public_abcd1234=webapp/80/tcp/deny,webapp/53/udp,webapp/53/tcp" \ + --env-add "DEBUG=false" \ + --image "${ufw_docker_agent_image}" \ + "${ufw_docker_agent}" +} + + +test-ufw-docker--service-delete-matches-with-a-port-but-no-previous-rule() { + mock-abcd1234-webapp + @mock ufw-docker--get-env-list === @stdout "xxx 888/tcp" "abcd1234 webapp/53/tcp" + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp 2222 +} +test-ufw-docker--service-delete-matches-with-a-port-but-no-previous-rule-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ + --env-add "ufw_public_abcd1234=webapp/22/tcp/deny,webapp/53/tcp" \ + --env-add "DEBUG=false" \ + --image "${ufw_docker_agent_image}" \ + "${ufw_docker_agent}" +} + + +test-ufw-docker--service-delete-matches-with-a-port-proto-pair() { + mock-abcd1234-webapp + @mock ufw-docker--get-env-list === @stdout "xxx 888/tcp" "abcd1234 webapp/80/tcp" + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp 8080/tcp +} +test-ufw-docker--service-delete-matches-with-a-port-proto-pair-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ + --env-add "ufw_public_abcd1234=webapp/80/tcp/deny" \ + --env-add "DEBUG=false" \ + --image "${ufw_docker_agent_image}" \ + "${ufw_docker_agent}" +} + +test-ufw-docker--service-delete-matches-with-a-port-proto-pair2() { + mock-abcd1234-webapp + @mock ufw-docker--get-env-list === @stdout "xxx 888/tcp" "abcd1234 webapp/80/tcp" "abcd1234 webapp/53/udp" "abcd1234 webapp/53/tcp" + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp 5353/udp +} +test-ufw-docker--service-delete-matches-with-a-port-proto-pair2-assert() { + docker service update --update-parallelism=0 \ + --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ + --env-add "ufw_public_abcd1234=webapp/53/udp/deny,webapp/80/tcp,webapp/53/tcp" \ + --env-add "DEBUG=false" \ + --image "${ufw_docker_agent_image}" \ + "${ufw_docker_agent}" +} + +test-ufw-docker--service-delete-matches-with-a-not-matched-port() { + mock-abcd1234-webapp + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp 3333 +} +test-ufw-docker--service-delete-matches-with-a-not-matched-port-assert() { + @do-nothing + @fail +} + +test-ufw-docker--service-delete-matches-with-a-not-matched-protocal() { + @mock ufw-docker--get-service-id webapp === @stdout "abcd1234" + @mock ufw-docker--get-service-name webapp === @stdout webapp + @mock ufw-docker--list-service-ports webapp === @stdout "22 2222/tcp" "80 8080/tcp" "53 5353/udp" + + load-ufw-docker-function ufw-docker--service-delete + ufw-docker--service-delete webapp 8080/udp +} +test-ufw-docker--service-delete-matches-with-a-not-matched-protocal-assert() { + @do-nothing + @fail +} + test-ufw-docker--list-service-ports() { load-ufw-docker-function ufw-docker--list-service-ports ufw-docker--list-service-ports foo @@ -289,3 +511,52 @@ test-ufw-docker--list-service-ports() { test-ufw-docker--list-service-ports-assert() { docker service inspect foo --format '{{range .Endpoint.Spec.Ports}}{{.PublishedPort}} {{.TargetPort}}/{{.Protocol}}{{"\n"}}{{end}}' } + +function setup-mock-for-testing-docker-entrypoint() { + @mock date '+%Y%m%d%H%M%S' === @stdout 200902140731 + + declare -gx ufw_public_id111111=alpha/80/tcp + declare -gx ufw_public_id222222=beta/deny + declare -gx ufw_public_id333333=gamma/8080/tcp/deny,gamma/5353/udp + + @allow-real sed -e '/^declare -x ufw_public_/!d' -e 's/^declare -x ufw_public_//' -e 's/="/ /' -e 's/"$//' + @allow-real tr ',' '\n' +} + +test-dockerentrypoint() { + setup-mock-for-testing-docker-entrypoint + declare -x ufw_public_id333333=gamma/8080/tcp/deny,gamma/5353/udp + + @run "$working_dir"/../docker-entrypoint.sh update-ufw-rules +} +test-dockerentrypoint-assert() { + declare -a docker_opts=(run --rm -t --name ufw-docker-agent-42-200902140731 + --cap-add NET_ADMIN --network host --env DEBUG=false + -v /var/run/docker.sock:/var/run/docker.sock + -v /etc/ufw:/etc/ufw + chaifeng/ufw-docker-agent:090502 + ) + docker "${docker_opts[@]}" add-service-rule id111111 80/tcp + docker "${docker_opts[@]}" delete allow id222222 + docker "${docker_opts[@]}" delete allow id333333 8080/tcp + docker "${docker_opts[@]}" add-service-rule id333333 5353/udp +} + +test-dockerentrypoint-deny-first() { + setup-mock-for-testing-docker-entrypoint + declare -x ufw_public_id333333=gamma/5353/udp,gamma/8080/tcp/deny + + @run "$working_dir"/../docker-entrypoint.sh update-ufw-rules +} +test-dockerentrypoint-deny-first-assert() { + declare -a docker_opts=(run --rm -t --name ufw-docker-agent-42-200902140731 + --cap-add NET_ADMIN --network host --env DEBUG=false + -v /var/run/docker.sock:/var/run/docker.sock + -v /etc/ufw:/etc/ufw + chaifeng/ufw-docker-agent:090502 + ) + docker "${docker_opts[@]}" add-service-rule id111111 80/tcp + docker "${docker_opts[@]}" delete allow id222222 + docker "${docker_opts[@]}" delete allow id333333 8080/tcp + docker "${docker_opts[@]}" add-service-rule id333333 5353/udp +} diff --git a/test/ufw-docker.test.sh b/test/ufw-docker.test.sh index 52ab301..e1ec2c4 100755 --- a/test/ufw-docker.test.sh +++ b/test/ufw-docker.test.sh @@ -19,12 +19,10 @@ source "$working_dir"/bach/bach.sh @mock docker -v === @stdout Docker version 0.0.0, build dummy @mockpipe remove_blank_lines - @ignore echo + #@ignore echo @ignore err - DEFAULT_PROTO=tcp - GREP_REGEXP_INSTANCE_NAME="[-_.[:alnum:]]\\+" - + builtin source <(@sed -n -e '/^# UFW-DOCKER GLOBAL VARIABLES START #$/,/^# UFW-DOCKER GLOBAL VARIABLES END #$/{' -e '/^PATH=/d' -e 'p' -e '}' "$working_dir/../ufw-docker") UFW_DOCKER_AGENT_IMAGE=chaifeng/ufw-docker-agent:090502-legacy } @@ -34,7 +32,6 @@ function ufw-docker() { function load-ufw-docker-function() { set -euo pipefail - @load_function "$working_dir/../ufw-docker" "$1" } @@ -214,7 +211,7 @@ test-ufw-docker-list-httpd() { ufw-docker list httpd } test-ufw-docker-list-httpd-assert() { - ufw-docker--list 'httpd-container-name\(/v6\)\?' "" tcp "" + ufw-docker--list httpd-container-name "" tcp "" } @@ -267,7 +264,7 @@ test-ufw-docker-delete-allow-httpd() { ufw-docker delete allow httpd } test-ufw-docker-delete-allow-httpd-assert() { - ufw-docker--delete 'httpd-container-name\(/v6\)\?' "" tcp "" + ufw-docker--delete httpd-container-name "" tcp "" } @@ -487,6 +484,7 @@ test-IPv6-ufw-docker--allow-instance-all-published-port-multinetwork-select-netw test-ufw-docker--add-rule-a-non-existing-rule() { @mockfalse ufw-docker--list webapp 5000 tcp "" + @ignore echo load-ufw-docker-function ufw-docker--add-rule ufw-docker--add-rule webapp 172.18.0.4 5000 tcp @@ -497,6 +495,7 @@ test-ufw-docker--add-rule-a-non-existing-rule-assert() { test-ufw-docker--add-rule-a-non-existing-rule-with-network() { @mockfalse ufw-docker--list webapp 5000 tcp default + @ignore echo load-ufw-docker-function ufw-docker--add-rule ufw-docker--add-rule webapp 172.18.0.4 5000 tcp default @@ -510,6 +509,7 @@ test-ufw-docker--add-rule-modify-an-existing-rule() { @mocktrue ufw-docker--list webapp 5000 tcp default @mock ufw --dry-run route allow proto tcp from any to 172.18.0.4 port 5000 comment "allow webapp 5000/tcp default" === @echo @mockfalse grep "^Skipping" + @ignore echo load-ufw-docker-function ufw-docker--add-rule ufw-docker--add-rule webapp 172.18.0.4 5000 tcp default @@ -525,6 +525,7 @@ test-IPv6-ufw-docker--add-rule-modify-an-existing-rule() { @mocktrue ufw-docker--list webapp/v6 5000 tcp default @mock ufw --dry-run route allow proto tcp from any to fd00:cf::42 port 5000 comment "allow webapp/v6 5000/tcp default" === @echo @mockfalse grep "^Skipping" + @ignore echo load-ufw-docker-function ufw-docker--add-rule ufw-docker--add-rule webapp/v6 fd00:cf::42 5000 tcp default @@ -540,6 +541,7 @@ test-ufw-docker--add-rule-skip-an-existing-rule() { @mocktrue ufw-docker--list webapp 5000 tcp "" @mocktrue ufw --dry-run route allow proto tcp from any to 172.18.0.4 port 5000 comment "allow webapp 5000/tcp" @mocktrue grep "^Skipping" + @ignore echo load-ufw-docker-function ufw-docker--add-rule ufw-docker--add-rule webapp 172.18.0.4 5000 tcp "" @@ -553,6 +555,7 @@ test-ufw-docker--add-rule-modify-an-existing-rule-without-port() { @mocktrue ufw-docker--list webapp "" tcp "" @mock ufw --dry-run route allow proto tcp from any to 172.18.0.4 comment "allow webapp" === @echo @mockfalse grep "^Skipping" + @ignore echo load-ufw-docker-function ufw-docker--add-rule @@ -568,7 +571,7 @@ test-ufw-docker--add-rule-modify-an-existing-rule-without-port-assert() { test-ufw-docker--instance-name-found-a-name() { @mock docker inspect --format="{{.Name}}" foo @mock sed -e 's,^/,,' - @mockfalse grep "^$GREP_REGEXP_INSTANCE_NAME\$" + @mockfalse grep "^$GREP_REGEXP_NAME\$" @mock echo -n foo @@ -584,77 +587,151 @@ test-ufw-docker--instance-name-found-a-name-assert() { test-ufw-docker--instance-name-found-an-id() { @mock docker inspect --format="{{.Name}}" fooid @mock sed -e 's,^/,,' - @mockfalse grep "^$GREP_REGEXP_INSTANCE_NAME\$" + @mockfalse grep "^$GREP_REGEXP_NAME\$" + @mock echo -n fooid load-ufw-docker-function ufw-docker--instance-name ufw-docker--instance-name fooid } test-ufw-docker--instance-name-found-an-id-assert() { docker inspect --format="{{.Name}}" fooid + @dryrun echo -n fooid } +function mock-ufw-status-numbered-foo() { + @mock ufw status numbered === @echo "Status: active + + To Action From + -- ------ ---- +[ 1] OpenSSH ALLOW IN Anywhere +[ 2] Anywhere ALLOW IN 192.168.56.128/28 +[ 3] 172.17.0.3 80/tcp ALLOW FWD Anywhere # allow foo 80/tcp bridge +[ 4] 172.20.0.3 80/tcp ALLOW FWD Anywhere # allow bar 80/tcp bar-external +[ 5] 172.17.0.3 53/udp ALLOW FWD Anywhere # allow foo 53/udp foo-internal +[ 6] 172.17.0.3 53/tcp ALLOW FWD Anywhere # allow foo 53/tcp +[ 7] 172.18.0.2 29090/tcp ALLOW FWD Anywhere # allow id111111 29090/tcp +[ 8] 172.18.0.2 48080/tcp ALLOW FWD Anywhere # allow id222222 48080/tcp +[ 9] 172.18.0.2 40080/tcp ALLOW FWD Anywhere # allow id333333 40080/tcp +[10] OpenSSH (v6) ALLOW IN Anywhere (v6) +[11] Anywhere (v6) ALLOW IN fd00:a:b:0:cafe::/80 +[12] fd00:a:b:deaf::3 80/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 80/tcp bridge +[13] fd05:8f23:c937:2::3 80/tcp ALLOW FWD Anywhere (v6) # allow bar/v6 80/tcp bar-external +[14] fd00:a:b:deaf::3 53/udp ALLOW FWD Anywhere (v6) # allow foo/v6 53/udp foo-internal +[15] fd00:a:b:deaf::3 53/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 53/tcp +" + +} + +test-ufw-docker--status() { + mock-ufw-status-numbered-foo + @allow-real grep '# allow [-_.[:alnum:]]\+\(/v6\)\?\( [[:digit:]]\+/\(tcp\|udp\)\( [-_.[:alnum:]]\+\)\?\)\?$' + + load-ufw-docker-function ufw-docker--list + load-ufw-docker-function ufw-docker--status + ufw-docker--status +} +test-ufw-docker--status-assert() { + test-ufw-docker--list-all-assert +} + +test-ufw-docker--list-all() { + mock-ufw-status-numbered-foo + @allow-real grep '# allow [-_.[:alnum:]]\+\(/v6\)\?\( [[:digit:]]\+/\(tcp\|udp\)\( [-_.[:alnum:]]\+\)\?\)\?$' + + load-ufw-docker-function ufw-docker--list + ufw-docker--list +} +test-ufw-docker--list-all-assert() { + @stdout "[ 3] 172.17.0.3 80/tcp ALLOW FWD Anywhere # allow foo 80/tcp bridge" + @stdout "[ 4] 172.20.0.3 80/tcp ALLOW FWD Anywhere # allow bar 80/tcp bar-external" + @stdout "[ 5] 172.17.0.3 53/udp ALLOW FWD Anywhere # allow foo 53/udp foo-internal" + @stdout "[ 6] 172.17.0.3 53/tcp ALLOW FWD Anywhere # allow foo 53/tcp" + @stdout "[ 7] 172.18.0.2 29090/tcp ALLOW FWD Anywhere # allow id111111 29090/tcp" + @stdout "[ 8] 172.18.0.2 48080/tcp ALLOW FWD Anywhere # allow id222222 48080/tcp" + @stdout "[ 9] 172.18.0.2 40080/tcp ALLOW FWD Anywhere # allow id333333 40080/tcp" + @stdout "[12] fd00:a:b:deaf::3 80/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 80/tcp bridge" + @stdout "[13] fd05:8f23:c937:2::3 80/tcp ALLOW FWD Anywhere (v6) # allow bar/v6 80/tcp bar-external" + @stdout "[14] fd00:a:b:deaf::3 53/udp ALLOW FWD Anywhere (v6) # allow foo/v6 53/udp foo-internal" + @stdout "[15] fd00:a:b:deaf::3 53/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 53/tcp" +} test-ufw-docker--list-name() { - @mocktrue ufw status numbered - @mockfalse grep "# allow foo\\( [[:digit:]]\\+\\/\\(tcp\\|udp\\)\\)\\( [[:graph:]]*\\)\$" - @mockfalse grep "# allow foo\\( [[:digit:]]\\+\\/\\(tcp\\|udp\\)\\)\$" + mock-ufw-status-numbered-foo + @allow-real grep '# allow foo\(/v6\)\?\( [[:digit:]]\+/\(tcp\|udp\)\( [-_.[:alnum:]]\+\)\?\)\?$' load-ufw-docker-function ufw-docker--list ufw-docker--list foo } test-ufw-docker--list-name-assert() { - grep "# allow foo\$" + @stdout "[ 3] 172.17.0.3 80/tcp ALLOW FWD Anywhere # allow foo 80/tcp bridge" + @stdout "[ 5] 172.17.0.3 53/udp ALLOW FWD Anywhere # allow foo 53/udp foo-internal" + @stdout "[ 6] 172.17.0.3 53/tcp ALLOW FWD Anywhere # allow foo 53/tcp" + @stdout "[12] fd00:a:b:deaf::3 80/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 80/tcp bridge" + @stdout "[14] fd00:a:b:deaf::3 53/udp ALLOW FWD Anywhere (v6) # allow foo/v6 53/udp foo-internal" + @stdout "[15] fd00:a:b:deaf::3 53/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 53/tcp" } test-ufw-docker--list-name-udp() { - @mocktrue ufw status numbered + mock-ufw-status-numbered-foo + @allow-real grep '# allow foo\(/v6\)\? [[:digit:]]\+/udp\( [-_.[:alnum:]]\+\)\?$' + load-ufw-docker-function ufw-docker--list ufw-docker--list foo "" udp } test-ufw-docker--list-name-udp-assert() { - grep "# allow foo\\( [[:digit:]]\\+\\/\\(tcp\\|udp\\)\\)\\( [[:graph:]]*\\)\$" + @stdout "[ 5] 172.17.0.3 53/udp ALLOW FWD Anywhere # allow foo 53/udp foo-internal" + @stdout "[14] fd00:a:b:deaf::3 53/udp ALLOW FWD Anywhere (v6) # allow foo/v6 53/udp foo-internal" } -test-ufw-docker--list-name-80() { - @mocktrue ufw status numbered +test-ufw-docker--list-name-80-_-bridge() { + mock-ufw-status-numbered-foo + @allow-real grep '# allow foo\(/v6\)\? 80/tcp bridge$' + load-ufw-docker-function ufw-docker--list - ufw-docker--list foo 80 + ufw-docker--list foo 80 "" bridge } -test-ufw-docker--list-name-80-assert() { - grep "# allow foo\\( 80\\/tcp\\)\\( [[:graph:]]*\\)\$" +test-ufw-docker--list-name-80-_-bridge-assert() { + @stdout "[ 3] 172.17.0.3 80/tcp ALLOW FWD Anywhere # allow foo 80/tcp bridge" + @stdout "[12] fd00:a:b:deaf::3 80/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 80/tcp bridge" } -test-ufw-docker--list-name-80-udp() { - @mocktrue ufw status numbered +test-ufw-docker--list-name-53-udp() { + mock-ufw-status-numbered-foo + @allow-real grep '# allow foo\(/v6\)\? 53/udp\( [-_.[:alnum:]]\+\)\?$' + load-ufw-docker-function ufw-docker--list - ufw-docker--list foo 80 udp + ufw-docker--list foo 53 udp } -test-ufw-docker--list-name-80-udp-assert() { - grep "# allow foo\\( 80\\/udp\\)\\( [[:graph:]]*\\)\$" +test-ufw-docker--list-name-53-udp-assert() { + @stdout "[ 5] 172.17.0.3 53/udp ALLOW FWD Anywhere # allow foo 53/udp foo-internal" + @stdout "[14] fd00:a:b:deaf::3 53/udp ALLOW FWD Anywhere (v6) # allow foo/v6 53/udp foo-internal" } -test-ufw-docker--list-grep-without-network() { - @mocktrue ufw status numbered +test-ufw-docker--list-grep-with-incorrect-network() { + mock-ufw-status-numbered-foo + @allow-real grep '# allow foo\(/v6\)\? 53/udp incorrect-network$' + load-ufw-docker-function ufw-docker--list - ufw-docker--list foo 80 udp + ufw-docker--list foo 53 udp incorrect-network } -test-ufw-docker--list-grep-without-network-assert() { - grep "# allow foo\\( 80\\/udp\\)\\( [[:graph:]]*\\)\$" +test-ufw-docker--list-grep-with-incorrect-network-assert() { + @fail } -test-ufw-docker--list-grep-without-network-and-port() { - @mocktrue ufw status numbered - @mockfalse grep "# allow foo\\( 80\\/tcp\\)\\( [[:graph:]]*\\)\$" +test-ufw-docker--list-foo-80-_-_() { + mock-ufw-status-numbered-foo + @allow-real grep '# allow foo\(/v6\)\? 80/tcp\( [-_.[:alnum:]]\+\)\?$' load-ufw-docker-function ufw-docker--list ufw-docker--list foo 80 } -test-ufw-docker--list-grep-without-network-and-port-assert() { - grep "# allow foo\\( 80\\/tcp\\)\$" +test-ufw-docker--list-foo-80-_-_-assert() { + @stdout "[ 3] 172.17.0.3 80/tcp ALLOW FWD Anywhere # allow foo 80/tcp bridge" + @stdout "[12] fd00:a:b:deaf::3 80/tcp ALLOW FWD Anywhere (v6) # allow foo/v6 80/tcp bridge" } @@ -684,6 +761,7 @@ test-ufw-docker--delete-empty-result-assert() { test-ufw-docker--delete-all() { @mock ufw-docker--list-number webapp 80 tcp === @stdout 5 8 9 @mockpipe sort -rn + @ignore echo load-ufw-docker-function ufw-docker--delete ufw-docker--delete webapp 80 tcp @@ -693,3 +771,91 @@ test-ufw-docker--delete-all-assert() { ufw delete 8 ufw delete 9 } + +test-ufw-docker--check-install_ipv4() { + @mock mktemp === @stdout /tmp/after_rules_tmp + @mock sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" /etc/ufw/after.rules + @mock tee "/tmp/after_rules_tmp" + @capture tee -a /tmp/after_rules_tmp + @allow-real cat + + load-ufw-docker-function ufw-docker--check-install + ufw-docker--check-install +} +test-ufw-docker--check-install_ipv4-assert() { + rm-on-exit /tmp/after_rules_tmp + sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" /etc/ufw/after.rules + @assert-capture tee -a /tmp/after_rules_tmp <<\EOF +# BEGIN UFW AND DOCKER +*filter +:ufw-user-forward - [0:0] +:ufw-docker-logging-deny - [0:0] +:DOCKER-USER - [0:0] +-A DOCKER-USER -j ufw-user-forward + +-A DOCKER-USER -j RETURN -s 10.0.0.0/8 +-A DOCKER-USER -j RETURN -s 172.16.0.0/12 +-A DOCKER-USER -j RETURN -s 192.168.0.0/16 + +-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN + +-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 +-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 +-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 +-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 +-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 +-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 + +-A DOCKER-USER -j RETURN + +-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] " +-A ufw-docker-logging-deny -j DROP + +COMMIT +# END UFW AND DOCKER +EOF + diff -u --color=auto /etc/ufw/after.rules /tmp/after_rules_tmp +} + +test-ufw-docker--check-install_ipv4-subnets() { + @mock ufw-docker--list-docker-subnets IPv4 192.168.56.128/28 172.16.0.0/12 === @stdout "172.16.0.0/12" "192.168.56.128/28" + @mock mktemp === @stdout /tmp/after_rules_tmp + @mock sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" /etc/ufw/after.rules + @mock tee "/tmp/after_rules_tmp" + @capture tee -a /tmp/after_rules_tmp + @allow-real cat + + load-ufw-docker-function ufw-docker--check-install + ufw-docker--check-install --docker-subnets 192.168.56.128/28 172.16.0.0/12 +} +test-ufw-docker--check-install_ipv4-subnets-assert() { + rm-on-exit /tmp/after_rules_tmp + sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" /etc/ufw/after.rules + @assert-capture tee -a /tmp/after_rules_tmp <<\EOF +# BEGIN UFW AND DOCKER +*filter +:ufw-user-forward - [0:0] +:ufw-docker-logging-deny - [0:0] +:DOCKER-USER - [0:0] +-A DOCKER-USER -j ufw-user-forward + +-A DOCKER-USER -j RETURN -s 172.16.0.0/12 +-A DOCKER-USER -j RETURN -s 192.168.56.128/28 + +-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN + +-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 +-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.56.128/28 +-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 +-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.56.128/28 + +-A DOCKER-USER -j RETURN + +-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] " +-A ufw-docker-logging-deny -j DROP + +COMMIT +# END UFW AND DOCKER +EOF + diff -u --color=auto /etc/ufw/after.rules /tmp/after_rules_tmp +} diff --git a/ufw-docker b/ufw-docker index 012c1be..cf31e51 100755 --- a/ufw-docker +++ b/ufw-docker @@ -2,17 +2,21 @@ set -euo pipefail [[ -n "${DEBUG:-}" ]] && set -x -LANG=en_US.UTF-8 -LANGUAGE=en_US: -LC_ALL=en_US.UTF-8 +# UFW-DOCKER GLOBAL VARIABLES START # +LC_ALL=C PATH="/bin:/usr/bin:/sbin:/usr/sbin:/snap/bin/" -GREP_REGEXP_INSTANCE_NAME="[-_.[:alnum:]]\\+" +GREP_REGEXP_NAME="[-_.[:alnum:]]\\+" DEFAULT_PROTO=tcp +ENV_VALUE_SPLITTER=',' ufw_docker_agent=ufw-docker-agent ufw_docker_agent_image="${UFW_DOCKER_AGENT_IMAGE:-chaifeng/${ufw_docker_agent}:250702-nf_tables}" +after_rules="/etc/ufw/after.rules" +after6_rules="/etc/ufw/after6.rules" +# UFW-DOCKER GLOBAL VARIABLES END # + if [[ "${ufw_docker_agent_image}" = *-@(legacy|nf_tables) ]]; then if iptables --version | grep -F '(legacy)' &>/dev/null; then ufw_docker_agent_image="${ufw_docker_agent_image%-*}-legacy" @@ -24,31 +28,37 @@ fi test -n "$ufw_docker_agent_image" function ufw-docker--status() { - ufw-docker--list "$GREP_REGEXP_INSTANCE_NAME\(/v6\)\?" + ufw-docker--list } function ufw-docker--list() { - local INSTANCE_NAME="$1" + local INSTANCE_NAME="${1:-}" local INSTANCE_PORT="${2:-}" - local PROTO="${3:-${DEFAULT_PROTO}}" + local PROTO="${3:-}" local NETWORK="${4:-}" local params_count="$#" + local _grep_regexp - if [[ -z "$INSTANCE_PORT" ]]; then + [[ -n "${INSTANCE_NAME:-}" ]] || INSTANCE_NAME="$GREP_REGEXP_NAME" + if [[ -z "${INSTANCE_PORT:-}" && -z "${PROTO:-}" && -z "${NETWORK-}" ]]; then INSTANCE_PORT="[[:digit:]]\\+" - PROTO="\\(tcp\\|udp\\)" - fi - - if [[ -z "$NETWORK" ]]; then - NETWORK="[[:graph:]]*" + [[ -n "${PROTO:-}" ]] || PROTO="\\(tcp\\|udp\\)" + _grep_regexp="\\( ${INSTANCE_PORT}/${PROTO}\\( ${GREP_REGEXP_NAME}\\)\\?\\)\\?" + else + [[ -n "${INSTANCE_PORT:-}" ]] || INSTANCE_PORT="[[:digit:]]\\+" + [[ -n "${PROTO:-}" ]] || PROTO="${DEFAULT_PROTO}" + if [[ -n "${NETWORK:-}" ]]; then + NETWORK=" ${NETWORK}" + else + NETWORK="\\( ${GREP_REGEXP_NAME}\\)\\?" + fi + _grep_regexp=" ${INSTANCE_PORT}/${PROTO}${NETWORK}" fi local ufw_output ufw_output="$(ufw status numbered)" - grep "# allow ${INSTANCE_NAME}\\( ${INSTANCE_PORT}\\/${PROTO}\\)\\( ${NETWORK}\\)\$" <<< "$ufw_output" || - grep "# allow ${INSTANCE_NAME}\\( ${INSTANCE_PORT}\\/${PROTO}\\)\$" <<< "$ufw_output" || - grep "# allow ${INSTANCE_NAME}\$" <<< "$ufw_output" + grep "# allow ${INSTANCE_NAME}\\(/v6\\)\\?${_grep_regexp}\$" <<< "$ufw_output" } function ufw-docker--list-number() { @@ -155,7 +165,7 @@ function ufw-docker--instance-name() { { { docker inspect --format='{{.Name}}' "$INSTANCE_ID" 2>/dev/null | sed -e 's,^/,,' | - grep "^${GREP_REGEXP_INSTANCE_NAME}\$" 2>/dev/null + grep "^${GREP_REGEXP_NAME}\$" 2>/dev/null } || echo -n "$INSTANCE_ID"; } | remove_blank_lines } @@ -171,7 +181,7 @@ function ufw-docker--service() { shift || true declare service_id_or_name="${1:?Missing swarm service name or service ID}" - "ufw-docker--service-${service_action}" "${service_id_or_name}" + "ufw-docker--service-${service_action}" "$@" ;; allow) shift || true @@ -221,22 +231,24 @@ function ufw-docker--service-allow() { declare service_id service_id="$(ufw-docker--get-service-id "${service_name}")" [[ -z "${service_id:-}" ]] && die "Could not find service \"$service_name\"" - service_name="$(ufw-docker--get-service-name "${service_name}")" + declare env_value= published_port= target_port= exec 9< <(ufw-docker--list-service-ports "$service_name") - while read -u 9 -r port target_port; do + while read -u 9 -r published_port target_port; do if [[ "$target_port" = "${service_port}/${service_proto}" ]]; then - declare service_env="ufw_public_${service_id}=${service_name}/${port}/${service_proto}" + env_value="${service_name}/${published_port}/${service_proto}" break; fi done exec 9<&- - [[ -z "${service_env:-}" ]] && die "Service $service_name does not publish port $service_port." + [[ -z "${env_value:-}" ]] && die "Service $service_name does not publish port $service_port." + declare service_env if ! docker service inspect "$ufw_docker_agent" &>/dev/null; then err "Not found ufw-docker-agent service, creating ..." + service_env="ufw_public_${service_id}=${env_value}" docker service create --name "$ufw_docker_agent" --mode global \ --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ --mount type=bind,source=/etc/ufw,target=/etc/ufw,readonly=true \ @@ -245,15 +257,20 @@ function ufw-docker--service-allow() { --env "${service_env}" \ "${ufw_docker_agent_image}" else - declare -a service_env_list - service_env_list+=(--env-add "${service_env}") - + declare -a value_list=() service_env_list=() exec 8< <(ufw-docker--get-env-list) while read -u 8 -r id value; do - [[ "$id" = "$service_id" ]] && continue - [[ "$value" = "${service_name}"/* ]] && service_env_list+=(--env-rm "ufw_public_${id}") + [[ "$id" != "$service_id" && "$value" = "${service_name}"/* ]] && service_env_list+=(--env-rm "ufw_public_${id}") + [[ "$id" = "$service_id" ]] || continue + [[ "${value}" = "${env_value}" || "${value}" = "${env_value}"/* ]] || + value_list+=("${value}") done exec 8<&- + value_list=("${env_value}" "${value_list[@]}") + + service_env="ufw_public_${service_id}=$(IFS="${ENV_VALUE_SPLITTER}"; printf '%s' "${value_list[*]}")" + + service_env_list+=(--env-add "${service_env}") docker service update --update-parallelism=0 \ --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ @@ -269,24 +286,57 @@ function ufw-docker--get-env-list() { --format '{{range $k,$v := .Spec.TaskTemplate.ContainerSpec.Env}}{{ $v }}{{"\n"}}{{end}}' | sed -e '/^ufw_public_/!d' \ -e 's/^ufw_public_//' \ - -e 's/=/ /' + -e 's/=/ /' | + while read -r id value; do + tr ',' '\n' <<< "$value" | sed "s/^/${id} /g" + done } function ufw-docker--service-delete() { declare service_name="$1" + declare service_port="${2:-}" + declare port proto + if [[ "${service_port:-}" = */* ]]; then + port="${service_port%/*}" + proto="${service_port#*/}" + elif [[ -n "${service_port:-}" ]]; then + port="$service_port" + proto="$DEFAULT_PROTO" + fi + declare service_id + service_id="$(ufw-docker--get-service-id "${service_name}")" + [[ -z "${service_id:-}" ]] && die "Could not find service \"$service_name\"" + service_name="$(ufw-docker--get-service-name "${service_name}")" + + declare env_value= published_port= target_port= + if [[ -n "${port:-}" ]]; then + exec 9< <(ufw-docker--list-service-ports "$service_name") + while read -u 9 -r published_port target_port; do + if [[ "$target_port" = "${port}/${proto}" ]]; then + env_value="${service_name}/${published_port}/${proto}" + break; + fi + done + exec 9<&- + [[ -n "${env_value:-}" ]] || die "Service $service_name does not publish port $service_port." + else + declare env_value="${service_name}" + fi + + declare id value + declare -a value_list=() exec 8< <(ufw-docker--get-env-list) while read -u 8 -r id value; do - if [[ "$id" = "$service_name" ]] || [[ "$value" = "${service_name}"/* ]]; then - declare service_id="$id" - service_name="${value%%/*}" - declare service_env="ufw_public_${service_id}=${service_name}/deny" - break; - fi + [[ "$id" = "$service_id" ]] || continue + [[ "${value}" = "${env_value}" || "${value}" = "${env_value}"/* ]] || + value_list+=("${value}") done exec 8<&- + value_list=("${env_value}/deny" "${value_list[@]}") - [[ -z "${service_env:-}" ]] && die "Could not find service \"$service_name\"" + declare service_env + service_env="ufw_public_${service_id}=$(IFS="${ENV_VALUE_SPLITTER}"; printf '%s' "${value_list[*]}")" docker service update --update-parallelism=0 \ --env-add ufw_docker_agent_image="${ufw_docker_agent_image}" \ @@ -300,9 +350,6 @@ function ufw-docker--raw-command() { ufw "$@" } -after_rules="/etc/ufw/after.rules" -after6_rules="/etc/ufw/after6.rules" - function ufw-docker--check() { err "\\n########## iptables -n -L DOCKER-USER ##########" iptables -n -L DOCKER-USER @@ -370,7 +417,7 @@ function ufw-docker--check-install() { after_rules_tmp="${after_rules_tmp:-$(mktemp)}" rm-on-exit "$after_rules_tmp" - sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" "$after_rules" > "$after_rules_tmp" + sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" "$after_rules" | tee "$after_rules_tmp" >/dev/null { cat <<-\EOF # BEGIN UFW AND DOCKER @@ -409,7 +456,7 @@ function ufw-docker--check-install() { COMMIT # END UFW AND DOCKER EOF - } >> "${after_rules_tmp}" + } | tee -a "${after_rules_tmp}" >/dev/null diff -u --color=auto "$after_rules" "$after_rules_tmp" } @@ -430,7 +477,7 @@ function ufw-docker--check-install_ipv6() { after6_rules_tmp="${after6_rules_tmp:-$(mktemp)}" rm-on-exit "$after6_rules_tmp" - sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" "$after6_rules" > "$after6_rules_tmp" + sed "/^# BEGIN UFW AND DOCKER/,/^# END UFW AND DOCKER/d" "$after6_rules" | tee "$after6_rules_tmp" >/dev/null { cat <<-\EOF # BEGIN UFW AND DOCKER @@ -465,7 +512,7 @@ function ufw-docker--check-install_ipv6() { COMMIT # END UFW AND DOCKER EOF - } >> "${after6_rules_tmp}" + } | tee -a "${after6_rules_tmp}" diff -u --color=auto "$after6_rules" "$after6_rules_tmp" } @@ -624,7 +671,7 @@ case "$ufw_action" in INSTANCE_PORT="${INSTANCE_PORT%/*}" ;;& delete|list) - "ufw-docker--$ufw_action" "$INSTANCE_NAME\\(/v6\\)\\?" "$INSTANCE_PORT" "$PROTO" "$NETWORK" + "ufw-docker--$ufw_action" "$INSTANCE_NAME" "$INSTANCE_PORT" "$PROTO" "$NETWORK" ;; allow) "ufw-docker--$ufw_action" "$INSTANCE_NAME" "$INSTANCE_PORT" "$PROTO" "$NETWORK"