├── .dockerignore ├── .gitignore ├── .gitmodules ├── .release-it.json ├── Dockerfile ├── LICENSE ├── README.md ├── cmd └── docker-ipv6nat │ └── main.go ├── docker-ipv6nat-compat ├── firewall.go ├── manager.go ├── push.sh ├── state.go └── watcher.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/github.com/docker/docker"] 2 | path = vendor/github.com/docker/docker 3 | url = https://github.com/docker/docker 4 | [submodule "vendor/github.com/docker/go-units"] 5 | path = vendor/github.com/docker/go-units 6 | url = https://github.com/docker/go-units 7 | [submodule "vendor/github.com/fsouza/go-dockerclient"] 8 | path = vendor/github.com/fsouza/go-dockerclient 9 | url = https://github.com/fsouza/go-dockerclient 10 | [submodule "vendor/github.com/coreos/go-iptables"] 11 | path = vendor/github.com/coreos/go-iptables 12 | url = https://github.com/coreos/go-iptables 13 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "before:bump": "sed -E -i '' 's/^const buildVersion = .*$/const buildVersion = \"${version}\"/' cmd/docker-ipv6nat/main.go", 4 | "after:bump": "./push.sh ${version}", 5 | "after:release": "rm -Rf dist/" 6 | }, 7 | "git": { 8 | "commitMessage": "Release v${version}", 9 | "tagName": "v${version}", 10 | "tagAnnotation": "docker-ipv6nat v${version}" 11 | }, 12 | "github": { 13 | "release": true, 14 | "releaseName": "${version}", 15 | "draft": true, 16 | "assets": [ "dist/*" ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.16.6-alpine3.14 AS build 2 | ARG TARGETPLATFORM 3 | WORKDIR /go/src/github.com/robbertkl/docker-ipv6nat 4 | COPY . . 5 | RUN [ "$TARGETPLATFORM" = "linux/amd64" ] && echo GOOS=linux GOARCH=amd64 > .env || true 6 | RUN [ "$TARGETPLATFORM" = "linux/arm64" ] && echo GOOS=linux GOARCH=arm64 > .env || true 7 | RUN [ "$TARGETPLATFORM" = "linux/arm/v6" ] && echo GOOS=linux GOARCH=arm GOARM=6 > .env || true 8 | RUN [ "$TARGETPLATFORM" = "linux/arm/v7" ] && echo GOOS=linux GOARCH=arm GOARM=7 > .env || true 9 | ENV CGO_ENABLED=0 10 | RUN go env -w GO111MODULE=auto 11 | RUN env $(cat .env | xargs) go build -o /docker-ipv6nat.$(echo "$TARGETPLATFORM" | sed -E 's/(^linux|\/)//g') ./cmd/docker-ipv6nat 12 | 13 | FROM alpine:3.14 AS release 14 | RUN apk add --no-cache ip6tables 15 | COPY --from=build /docker-ipv6nat.* /docker-ipv6nat 16 | COPY docker-ipv6nat-compat / 17 | ENTRYPOINT ["/docker-ipv6nat-compat"] 18 | CMD ["--retry"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Robbert Klarenbeek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-ipv6nat 2 | 3 | This project mimics the way Docker does NAT for IPv4 and applies it to IPv6. Jump to [Usage](#usage) to get started right away. 4 | 5 | ## Why would I need this? 6 | 7 | Unfortunately, initially Docker was not created with IPv6 in mind. 8 | It was added later and, while it has come a long way, is still not as usable as one would want. 9 | Much discussion is still going on as to how IPv6 should be used in a containerized world; see the various GitHub issues linked below. 10 | 11 | Currently, you can let Docker give each container an IPv6 address from your (public) pool, but this has disadvantages: 12 | 13 | * Giving each container a publicly routable address means all ports (even unexposed / unpublished ports) are suddenly reachable by everyone, if no additional filtering is done ([docker/docker#21614](https://github.com/docker/docker/issues/21614)) 14 | * By default, each container gets a random IPv6, making it impossible to do properly do DNS; the alternative is to assign a specific IPv6 address to each container, still an administrative hassle ([docker/docker#13481](https://github.com/docker/docker/issues/13481)) 15 | * Published ports won't work on IPv6, unless you have the userland proxy enabled (which, for now, is enabled by default in Docker) 16 | * The userland proxy, however, seems to be on its way out ([docker/docker#14856](https://github.com/docker/docker/issues/14856)) and has various issues, like: 17 | * It can use a lot of RAM ([docker/docker#11185](https://github.com/docker/docker/issues/11185)) 18 | * Source IP addresses are rewritten, making it completely unusable for many purposes, e.g. mail servers ([docker/docker#17666](https://github.com/docker/docker/issues/17666), [docker/libnetwork#1099](https://github.com/docker/libnetwork/issues/1099)) 19 | 20 | Special mention of [@JonasT](https://github.com/JonasT) who submitted the majority of the above issues, pointing out some of the practical issues when using IPv6 with Docker. 21 | 22 | So basically, IPv6 for Docker can (depending on your setup) be pretty unusable ([docker/docker#13481](https://github.com/docker/docker/issues/13481)) and completely inconsistent with the way how IPv4 works ([docker/docker#21951](https://github.com/docker/docker/issues/21951)). 23 | Docker images are mostly designed with IPv4 NAT in mind, having NAT provide a layer of security allowing only published ports through, and letting container linking or user-defined networks provide inter-container communication. 24 | This does not go hand in hand with the way Docker IPv6 works, requiring image maintainers to rethink/adapt their images with IPv6 in mind. 25 | 26 | ## Welcome IPv6 NAT 27 | 28 | So what does this repo do? It attempts to resolve all of the above issues by managing `ip6tables` to setup IPv6 NAT for your containers, similar to how it's done by the Docker daemon for IPv4. 29 | 30 | * A ULA range ([RFC 4193](https://tools.ietf.org/html/rfc4193)) is used for containers; this automatically means the containers will NOT be publicly routable 31 | * Published ports are forwarded to the corresponding containers, similar to IPv4 32 | * The original IPv6 source addresses are maintained, again, just like with IPv4 33 | * Userland proxy can be turned off and IPv6 will still work 34 | 35 | This makes a transition to IPv6 completely painless, without needing to make changes to your images. 36 | 37 | Please note: 38 | 39 | * The Docker network API is required, so at least Docker 1.9.0 40 | * It triggers only on ULA ranges (so within `fc00::/7`), e.g. `fd00:dead:beef::/48` 41 | * Only networks with driver `bridge` are supported; this includes Docker's default network ("bridge"), as well as user-defined bridge networks 42 | 43 | ## NAT on IPv6, are you insane? 44 | 45 | First of all, thank you for questioning my sanity! 46 | I'm aware NAT on IPv6 is almost always a no-go, since the huge number of available addresses removes the need for it. 47 | However, considering all the above issues related to IPv6 are fixed with IPv6 NAT, I thought: why not? 48 | The concepts of working with Docker images/containers rely heavily on IPv4 NAT, so if this makes IPv6 with Docker usable in the same way, be happy. 49 | I'm in no way "pro IPv6 NAT" in the general case; I'm just "pro working shit". 50 | 51 | Probably IPv6 NAT will never make it into Docker, just because it's not "the right way". 52 | This is fine; when a better alternative is found, I'd be happy to use it and get rid of this repo. 53 | However, since the IPv6 support just isn't there yet, and discussions are still ongoing, this repo can be used in the meantime. 54 | 55 | Still think IPv6 NAT is a bad idea? That's fine, you're absolutely free to NOT use this repo. 56 | 57 | ## Usage 58 | 59 | ### Docker Container 60 | 61 | The recommended way is to run the Docker image: 62 | 63 | ``` 64 | docker run -d --name ipv6nat --privileged --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -v /lib/modules:/lib/modules:ro robbertkl/ipv6nat 65 | ``` 66 | 67 | The flags `--privileged` and `--network host` are necessary because docker-ipv6nat manages the hosts IPv6 firewall using ip6tables. 68 | 69 | To limit runtime privileges as a security precaution, the `--privileged` flag can be replaced with `--cap-add NET_ADMIN --cap-add SYS_MODULE`. 70 | 71 | If you're a security fan (it's not bad), you can drop all capabilities `--cap-drop ALL` and leave only `--cap-add NET_ADMIN --cap-add NET_RAW --cap-add SYS_MODULE`. 72 | About it you can read in a good [article](https://www.redhat.com/en/blog/secure-your-containers-one-weird-trick) from RedHat. 73 | 74 | You may omit the `-v /lib/modules:/lib/modules:ro` bind mount and `--cap-add SYS_MODULE` if your distro already loaded `ip6_tables` kernel module on boot. If you have a systemd-based distro, you can ensure that on next boot by `echo ip6_tables >/etc/modules-load.d/ipv6nat.conf`, see [modules-load.d(5)](https://www.freedesktop.org/software/systemd/man/modules-load.d.html). 75 | 76 | By default, the docker-ipv6nat command runs with the `--retry` flag. See the "Usage Flags" section below for more options. Add them to the end of the `docker run` command. 77 | 78 | ### AUR Package 79 | 80 | If you are running ArchLinux, you can install the latest version by getting the package from the [AUR](https://aur.archlinux.org/packages/docker-ipv6nat/). 81 | 82 | ```bash 83 | trizen -S docker-ipv6nat 84 | ``` 85 | 86 | For running docker-ipv6nat on system starup, you can simply enable (and start) the systemd service: 87 | 88 | ```bash 89 | systemctl enable docker-ipv6nat.service 90 | systemctl start docker-ipv6nat.service 91 | ``` 92 | 93 | ### Standalone Binary 94 | 95 | Alternatively, you can download the latest release from the [release page](https://github.com/robbertkl/docker-ipv6nat/releases) and run it directly on your host. 96 | See below for usage flags. 97 | 98 | ### Usage Flags 99 | 100 | Output from `docker-ipv6nat --help`: 101 | 102 | ``` 103 | Usage: docker-ipv6 [options] 104 | 105 | Automatically configure IPv6 NAT for running docker containers 106 | 107 | Options: 108 | -cleanup 109 | remove rules when shutting down 110 | -debug 111 | log ruleset changes to stdout 112 | -retry 113 | keep retrying to reconnect after a disconnect 114 | -version 115 | show version 116 | 117 | Environment Variables: 118 | DOCKER_HOST - default value for -endpoint 119 | DOCKER_CERT_PATH - directory path containing key.pem, cert.pem and ca.pem 120 | DOCKER_TLS_VERIFY - enable client TLS verification 121 | ``` 122 | 123 | ## Docker IPv6 configuration 124 | 125 | Instructions below show ways to enable IPv6 and are not specific to docker-ipv6nat. 126 | Just make sure to use a ULA range in order for docker-ipv6nat to pick them up. 127 | 128 | ### Option A: default bridge network 129 | 130 | To use IPv6, make sure your Docker daemon is started with `--ipv6` and specifies a ULA range with `--fixed-cidr-v6` (e.g. `--fixed-cidr-v6 fd00:dead:beef::/48`). 131 | 132 | ### Option B: user-defined network 133 | 134 | To try it out without messing with your Docker daemon flags, or if you're already using user-defined networks, you can create a IPv6-enabled network with: 135 | 136 | ``` 137 | docker network create --ipv6 --subnet fd00:dead:beef::/48 mynetwork 138 | ``` 139 | 140 | Then start all of your other containers with `--network mynetwork`. Please note the `robbertkl/ipv6nat` container still needs to run with `--network host` to access the host firewall. 141 | 142 | Docker-ipv6nat respects all supported `com.docker.network.bridge.*` options (pass them with `-o`) and adds 1 additional option: 143 | 144 | * `com.docker.network.bridge.host_binding_ipv6`: Default IPv6 address when binding container ports (do not include subnet/prefixlen; defaults to `::`, i.e. all IPv6 addresses) 145 | 146 | Please note this option can only be set on user-defined networks, as the default bridge network is controlled by the Docker daemon. 147 | 148 | ## Swarm mode support 149 | 150 | As mentioned above, docker-ipv6nat ip6tables changes affects only `bridge` type networks, so `overlay` networks are out of the window. Despite of that fact, in order to NAT outgoing traffic from a container to the outside world we can use the swarm `docker_gwbridge` which is a `bridge` network that every container in your swarm will get a 'leg' in. 151 | 152 | When you run `docker swarm init` a default bridge for the swarm is created named `docker_gwbridge` which is equivalent to `docker0` for standalone docker engines. the thing is that it's configured by default to prevent ip_forwarding 153 | 154 | So the workaround is to create the `docker_gwbridge` with ip_forwarding enabled before you run the `docker swarm init` 155 | in this way we managed to access the outside world components and yet keep the container within the swarm overlay network to enjoy all the benefits of swarm. 156 | 157 | Example: 158 | ```bash 159 | docker network create \ 160 | --ipv6 \ 161 | --subnet 172.20.0.0/20 \ 162 | --gateway 172.20.0.1 \ 163 | --gateway fd00:3984:3989::1 \ 164 | --subnet fd00:3984:3989::/64 \ 165 | --opt com.docker.network.bridge.name=docker_gwbridge \ 166 | --opt com.docker.network.bridge.enable_icc=true \ 167 | --opt com.docker.network.bridge.enable_ip_forwarding=true \ 168 | --opt com.docker.network.bridge.enable_ip_masquerade=true \ 169 | docker_gwbridge 170 | ``` 171 | 172 | ## Troubleshooting 173 | 174 | If you can see the added ip6tables rules, but it's still not working, it might be that forwarding is not enabled for IPv6. 175 | This is usually the case if you're using router advertisements (e.g. having `net.ipv6.conf.eth0.accept_ra=1`). 176 | Enabling forwarding in such a case will break router advertisements. To overcome this, use the following in your `/etc/sysctl.conf`: 177 | 178 | ``` 179 | net.ipv6.conf.eth0.accept_ra = 2 180 | net.ipv6.conf.all.forwarding = 1 181 | net.ipv6.conf.default.forwarding = 1 182 | ``` 183 | 184 | The special value of 2 will allow accepting router advertisements even if forwarding is enabled. 185 | 186 | Setting the `-debug` flag for docker-ipv6nat will log all ruleset changes to stdout so you can check your logs how docker-ipv6nat is modifing your ip6tables rulesets. 187 | 188 | ## Authors 189 | 190 | * Robbert Klarenbeek, 191 | 192 | Big thanks to all GitHub contributors for testing, reporting issues and PRs! 193 | 194 | Special thanks to Elias Werberich [@bephinix](https://github.com/bephinix) for his many contributions, mainly keeping up with recent docker / libnetwork changes and porting them to docker-ipv6nat! 195 | 196 | ## License 197 | 198 | This repo is published under the [MIT License](http://www.opensource.org/licenses/mit-license.php). 199 | -------------------------------------------------------------------------------- /cmd/docker-ipv6nat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/fsouza/go-dockerclient" 10 | "github.com/robbertkl/docker-ipv6nat" 11 | ) 12 | 13 | const buildVersion = "0.4.4" 14 | 15 | var ( 16 | cleanup bool 17 | retry bool 18 | userlandProxy bool 19 | version bool 20 | debug bool 21 | ) 22 | 23 | func usage() { 24 | fmt.Fprintln(os.Stderr, `Usage: docker-ipv6 [options] 25 | 26 | Automatically configure IPv6 NAT for running docker containers 27 | 28 | Options:`) 29 | flag.PrintDefaults() 30 | 31 | fmt.Fprintln(os.Stderr, ` 32 | Environment Variables: 33 | DOCKER_HOST - default value for -endpoint 34 | DOCKER_CERT_PATH - directory path containing key.pem, cert.pem and ca.pem 35 | DOCKER_TLS_VERIFY - enable client TLS verification 36 | `) 37 | 38 | fmt.Fprintln(os.Stderr, `For more information, see https://github.com/robbertkl/docker-ipv6nat`) 39 | } 40 | 41 | func initFlags() { 42 | flag.BoolVar(&cleanup, "cleanup", false, "remove rules when shutting down") 43 | flag.BoolVar(&retry, "retry", false, "keep retrying to reconnect after a disconnect") 44 | flag.BoolVar(&version, "version", false, "show version") 45 | flag.BoolVar(&debug, "debug", false, "log ruleset changes to stdout") 46 | 47 | flag.Usage = usage 48 | flag.Parse() 49 | } 50 | 51 | func main() { 52 | initFlags() 53 | 54 | if version { 55 | fmt.Println(buildVersion) 56 | return 57 | } 58 | 59 | if flag.NArg() > 0 { 60 | usage() 61 | os.Exit(1) 62 | } 63 | 64 | if err := run(); err != nil { 65 | log.Fatalf("%v", err) 66 | } 67 | } 68 | 69 | func run() error { 70 | if debug { 71 | log.Println("docker-ipv6nat is running in debug mode") 72 | } 73 | 74 | client, err := docker.NewClientFromEnv() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | state, err := dockeripv6nat.NewState(debug) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | if cleanup { 85 | defer func() { 86 | if err := state.Cleanup(); err != nil { 87 | log.Printf("%v", err) 88 | } 89 | }() 90 | } 91 | 92 | watcher := dockeripv6nat.NewWatcher(client, state, retry) 93 | if err := watcher.Watch(); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /docker-ipv6nat-compat: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BINARY="xtables-legacy-multi" 4 | if iptables-nft -L 2> /dev/null | grep -q "Chain DOCKER " 5 | then 6 | BINARY="xtables-nft-multi" 7 | fi 8 | 9 | ln -nfs "${BINARY}" /sbin/iptables 10 | ln -nfs "${BINARY}" /sbin/iptables-save 11 | ln -nfs "${BINARY}" /sbin/iptables-restore 12 | ln -nfs "${BINARY}" /sbin/ip6tables 13 | ln -nfs "${BINARY}" /sbin/ip6tables-save 14 | ln -nfs "${BINARY}" /sbin/ip6tables-restore 15 | hash -r 16 | 17 | exec /docker-ipv6nat "${@}" 18 | -------------------------------------------------------------------------------- /firewall.go: -------------------------------------------------------------------------------- 1 | package dockeripv6nat 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/coreos/go-iptables/iptables" 8 | ) 9 | 10 | // Table describes an ip(6)tables table 11 | type Table string 12 | 13 | // All ip(6)tables tables we use 14 | const ( 15 | TableFilter = "filter" 16 | TableNat = "nat" 17 | ) 18 | 19 | // Chain describes an ip(6)tables chain 20 | type Chain string 21 | 22 | // All ip(6)tables chains we use 23 | const ( 24 | ChainInput = "INPUT" 25 | ChainOutput = "OUTPUT" 26 | ChainPrerouting = "PREROUTING" 27 | ChainPostrouting = "POSTROUTING" 28 | ChainForward = "FORWARD" 29 | ChainDockerUser = "DOCKER-USER" 30 | ChainDocker = "DOCKER" 31 | ChainDockerIsolation1 = "DOCKER-ISOLATION-STAGE-1" 32 | ChainDockerIsolation2 = "DOCKER-ISOLATION-STAGE-2" 33 | ) 34 | 35 | // TableChain references a combination of an ip(6)tables table and chain 36 | type TableChain struct { 37 | table Table 38 | chain Chain 39 | } 40 | 41 | // Rule represents a unique firewall rule 42 | type Rule struct { 43 | tc TableChain 44 | spec []string 45 | prepend bool 46 | } 47 | 48 | // NewRule constructs a new (non prepended) Rule 49 | func NewRule(table Table, chain Chain, spec ...string) *Rule { 50 | return &Rule{ 51 | tc: TableChain{table, chain}, 52 | spec: spec, 53 | prepend: false, 54 | } 55 | } 56 | 57 | // NewPrependRule constructs a new Rule with prepend set to true 58 | func NewPrependRule(table Table, chain Chain, spec ...string) *Rule { 59 | return &Rule{ 60 | tc: TableChain{table, chain}, 61 | spec: spec, 62 | prepend: true, 63 | } 64 | } 65 | 66 | func (r *Rule) hash() string { 67 | return strings.Join(r.spec, "#") 68 | } 69 | 70 | // Equal compares 2 Rules 71 | func (r *Rule) Equal(other *Rule) bool { 72 | if r.tc != other.tc { 73 | return false 74 | } 75 | 76 | if len(r.spec) != len(other.spec) { 77 | return false 78 | } 79 | 80 | for index := range r.spec { 81 | if r.spec[index] != other.spec[index] { 82 | return false 83 | } 84 | } 85 | 86 | return true 87 | } 88 | 89 | // Ruleset contains a list of unique rules 90 | type Ruleset []*Rule 91 | 92 | // Contains checks if a Rule is part of the Ruleset 93 | func (s *Ruleset) Contains(r *Rule) bool { 94 | for _, sr := range *s { 95 | if r.Equal(sr) { 96 | return true 97 | } 98 | } 99 | 100 | return false 101 | } 102 | 103 | // Diff returns a new Ruleset with only the rules that are not part of other 104 | func (s *Ruleset) Diff(other *Ruleset) *Ruleset { 105 | if len(*other) == 0 { 106 | return s 107 | } 108 | 109 | diffed := make(Ruleset, 0, len(*s)) 110 | for _, r := range *s { 111 | if !other.Contains(r) { 112 | diffed = append(diffed, r) 113 | } 114 | } 115 | 116 | return &diffed 117 | } 118 | 119 | // Firewall keeps track of the active rules, in order to perform proper appends/prepends 120 | type Firewall struct { 121 | ipt *iptables.IPTables 122 | activeRules map[TableChain]map[string]bool 123 | debug bool 124 | userChainJumpRule *Rule 125 | } 126 | 127 | // NewFirewall constructs a new Firewall 128 | func NewFirewall(debug bool) (*Firewall, error) { 129 | ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | return &Firewall{ 135 | ipt: ipt, 136 | activeRules: make(map[TableChain]map[string]bool), 137 | debug: debug, 138 | userChainJumpRule: NewRule(TableFilter, ChainForward, "-j", ChainDockerUser), 139 | }, nil 140 | } 141 | 142 | func (fw *Firewall) activateRule(r *Rule) { 143 | if _, exists := fw.activeRules[r.tc]; !exists { 144 | fw.activeRules[r.tc] = make(map[string]bool) 145 | } 146 | fw.activeRules[r.tc][r.hash()] = true 147 | } 148 | 149 | func (fw *Firewall) deactivateRule(r *Rule) { 150 | delete(fw.activeRules[r.tc], r.hash()) 151 | } 152 | 153 | // EnsureTableChains creates (and clears!) the given TableChains 154 | func (fw *Firewall) EnsureTableChains(tableChains []TableChain) error { 155 | for _, tc := range tableChains { 156 | if err := fw.ipt.ClearChain(string(tc.table), string(tc.chain)); err != nil { 157 | return err 158 | } 159 | delete(fw.activeRules, tc) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // RemoveTableChains deletes the given TableChains 166 | func (fw *Firewall) RemoveTableChains(tableChains []TableChain) error { 167 | for _, tc := range tableChains { 168 | fw.ipt.ClearChain(string(tc.table), string(tc.chain)) 169 | fw.ipt.DeleteChain(string(tc.table), string(tc.chain)) 170 | delete(fw.activeRules, tc) 171 | } 172 | 173 | return nil 174 | } 175 | 176 | // EnsureRules makes sure the Rules in the given Ruleset exist or it creates them 177 | func (fw *Firewall) EnsureRules(rules *Ruleset) error { 178 | // A regular loop to append only the non-prepend rules 179 | for _, rule := range *rules { 180 | if rule.prepend { 181 | continue 182 | } 183 | 184 | exists, err := fw.ipt.Exists(string(rule.tc.table), string(rule.tc.chain), rule.spec...) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | if !exists { 190 | if err := fw.ipt.Insert(string(rule.tc.table), string(rule.tc.chain), len(fw.activeRules[rule.tc])+1, rule.spec...); err != nil { 191 | return err 192 | } 193 | if fw.debug { 194 | log.Println("rule added: -t", string(rule.tc.table), "-I", string(rule.tc.chain), len(fw.activeRules[rule.tc])+1, strings.Join(rule.spec, " ")) 195 | } 196 | } 197 | fw.activateRule(rule) 198 | } 199 | 200 | // Loop in reverse to insert the prepend rules to the start of the chain 201 | for index := len(*rules) - 1; index >= 0; index-- { 202 | rule := (*rules)[index] 203 | if !rule.prepend { 204 | continue 205 | } 206 | 207 | exists, err := fw.ipt.Exists(string(rule.tc.table), string(rule.tc.chain), rule.spec...) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | if !exists { 213 | if err := fw.ipt.Insert(string(rule.tc.table), string(rule.tc.chain), 1, rule.spec...); err != nil { 214 | return err 215 | } 216 | if fw.debug { 217 | log.Println("rule added: -t", string(rule.tc.table), "-I", string(rule.tc.chain), 1, strings.Join(rule.spec, " ")) 218 | } 219 | } 220 | fw.activateRule(rule) 221 | } 222 | 223 | return nil 224 | } 225 | 226 | // RemoveRules makes sure the Rules in the given Ruleset don't exist or removes them 227 | func (fw *Firewall) RemoveRules(rules *Ruleset) error { 228 | for _, rule := range *rules { 229 | if rule.Equal(fw.userChainJumpRule) { 230 | continue 231 | } 232 | 233 | exists, err := fw.ipt.Exists(string(rule.tc.table), string(rule.tc.chain), rule.spec...) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | if exists { 239 | if err := fw.ipt.Delete(string(rule.tc.table), string(rule.tc.chain), rule.spec...); err != nil { 240 | return err 241 | } 242 | if fw.debug { 243 | log.Println("rule removed: -t", string(rule.tc.table), "-D", string(rule.tc.chain), strings.Join(rule.spec, " ")) 244 | } 245 | } 246 | fw.deactivateRule(rule) 247 | } 248 | 249 | return nil 250 | } 251 | 252 | // EnsureUserFilterChain makes sure the DOCKER-USER chain exists, without clearing it 253 | func (fw *Firewall) EnsureUserFilterChain() error { 254 | chains, err := fw.ipt.ListChains(TableFilter) 255 | if err != nil { 256 | return err 257 | } 258 | 259 | exists := false 260 | for _, chain := range chains { 261 | if chain == ChainDockerUser { 262 | exists = true 263 | } 264 | } 265 | 266 | if !exists { 267 | if err = fw.ipt.NewChain(TableFilter, ChainDockerUser); err != nil { 268 | return err 269 | } 270 | } 271 | 272 | if err = fw.ipt.AppendUnique(TableFilter, ChainDockerUser, "-j", "RETURN"); err != nil { 273 | return err 274 | } 275 | 276 | exists, err = fw.ipt.Exists(TableFilter, ChainForward, "-j", ChainDockerUser) 277 | if err != nil { 278 | return err 279 | } 280 | 281 | if exists { 282 | err = fw.ipt.Delete(TableFilter, ChainForward, "-j", ChainDockerUser) 283 | } 284 | return err 285 | } 286 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package dockeripv6nat 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "strconv" 7 | 8 | "github.com/coreos/go-iptables/iptables" 9 | ) 10 | 11 | type managedNetwork struct { 12 | id string 13 | bridge string 14 | subnet net.IPNet 15 | icc bool 16 | masquerade bool 17 | internal bool 18 | binding net.IP 19 | } 20 | 21 | type managedContainer struct { 22 | id string 23 | bridge string 24 | address net.IP 25 | ports []managedPort 26 | } 27 | 28 | type managedPort struct { 29 | port uint16 30 | proto string 31 | hostAddress net.IP 32 | hostPort uint16 33 | } 34 | 35 | // Manager controls the firewall by managing rules for Docker networks and containers 36 | type Manager struct { 37 | fw *Firewall 38 | hairpinMode bool 39 | } 40 | 41 | // NewManager constructs a new Manager 42 | func NewManager(debug bool) (*Manager, error) { 43 | hairpinMode, err := detectHairpinMode() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | fw, err := NewFirewall(debug) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | if err := fw.EnsureUserFilterChain(); err != nil { 54 | return nil, err 55 | } 56 | 57 | if err := fw.EnsureTableChains(getCustomTableChains()); err != nil { 58 | return nil, err 59 | } 60 | 61 | if err := fw.EnsureRules(getBaseRules(hairpinMode)); err != nil { 62 | return nil, err 63 | } 64 | 65 | return &Manager{ 66 | fw: fw, 67 | hairpinMode: hairpinMode, 68 | }, nil 69 | } 70 | 71 | func detectHairpinMode() (bool, error) { 72 | // Use the IPv4 firewall to detect if the docker daemon is started with --userland-proxy=false. 73 | 74 | ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) 75 | if err != nil { 76 | return false, err 77 | } 78 | 79 | hairpinModeOffRulespec := []string{ 80 | "!", "-d", "127.0.0.0/8", 81 | "-m", "addrtype", 82 | "--dst-type", "LOCAL", 83 | "-j", "DOCKER", 84 | } 85 | 86 | hairpinModeOnRulespec := hairpinModeOffRulespec[3:] 87 | 88 | hairpinModeOn, err := ipt.Exists(TableNat, ChainOutput, hairpinModeOnRulespec...) 89 | if err != nil { 90 | return false, err 91 | } else if hairpinModeOn { 92 | return true, nil 93 | } 94 | 95 | hairpinModeOff, err := ipt.Exists(TableNat, ChainOutput, hairpinModeOffRulespec...) 96 | if err != nil { 97 | return false, err 98 | } else if hairpinModeOff { 99 | return false, nil 100 | } 101 | 102 | // Old iptables misinterprets prefix matches in new iptables 103 | hairpinModeOffRulespec[2] = "127.0.0.0/32" 104 | 105 | hairpinModeOff, err = ipt.Exists(TableNat, ChainOutput, hairpinModeOffRulespec...) 106 | if err != nil { 107 | return false, err 108 | } else if hairpinModeOff { 109 | return false, nil 110 | } 111 | 112 | return false, errors.New("unable to detect hairpin mode (is the docker daemon running?)") 113 | } 114 | 115 | // Cleanup removes the base rules and table-chains (per-network / per-container rules should already be removed) 116 | func (m *Manager) Cleanup() error { 117 | if err := m.fw.RemoveRules(getBaseRules(m.hairpinMode)); err != nil { 118 | return err 119 | } 120 | 121 | if err := m.fw.RemoveTableChains(getCustomTableChains()); err != nil { 122 | return err 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // ReplaceNetwork applies relative rule changes for a network 129 | func (m *Manager) ReplaceNetwork(oldNetwork, newNetwork *managedNetwork) error { 130 | return m.applyRules(getRulesForNetwork(oldNetwork, m.hairpinMode), getRulesForNetwork(newNetwork, m.hairpinMode)) 131 | } 132 | 133 | // ReplaceContainer applies relative rule changes for a container 134 | func (m *Manager) ReplaceContainer(oldContainer, newContainer *managedContainer) error { 135 | return m.applyRules(getRulesForContainer(oldContainer, m.hairpinMode), getRulesForContainer(newContainer, m.hairpinMode)) 136 | } 137 | 138 | func (m *Manager) applyRules(oldRules, newRules *Ruleset) error { 139 | oldRules = oldRules.Diff(newRules) 140 | 141 | if err := m.fw.EnsureRules(newRules); err != nil { 142 | return err 143 | } 144 | 145 | if err := m.fw.RemoveRules(oldRules); err != nil { 146 | return err 147 | } 148 | 149 | return nil 150 | } 151 | 152 | func getCustomTableChains() []TableChain { 153 | return []TableChain{ 154 | {TableFilter, ChainDocker}, 155 | {TableFilter, ChainDockerIsolation1}, 156 | {TableFilter, ChainDockerIsolation2}, 157 | {TableNat, ChainDocker}, 158 | } 159 | } 160 | 161 | func getBaseRules(hairpinMode bool) *Ruleset { 162 | outputRule := NewRule(TableNat, ChainOutput, 163 | "-m", "addrtype", 164 | "--dst-type", "LOCAL", 165 | "-j", ChainDocker) 166 | 167 | if !hairpinMode { 168 | outputRule.spec = append(outputRule.spec, "!", "-d", "::1") 169 | } 170 | 171 | return &Ruleset{ 172 | NewPrependRule(TableFilter, ChainForward, 173 | "-j", ChainDockerUser), 174 | NewPrependRule(TableFilter, ChainForward, 175 | "-j", ChainDockerIsolation1), 176 | NewRule(TableFilter, ChainDockerIsolation1, 177 | "-j", "RETURN"), 178 | NewRule(TableFilter, ChainDockerIsolation2, 179 | "-j", "RETURN"), 180 | NewRule(TableNat, ChainPrerouting, 181 | "-m", "addrtype", 182 | "--dst-type", "LOCAL", 183 | "-j", ChainDocker), 184 | outputRule, 185 | } 186 | } 187 | 188 | func getRulesForNetwork(network *managedNetwork, hairpinMode bool) *Ruleset { 189 | if network == nil { 190 | return &Ruleset{} 191 | } 192 | 193 | iccAction := "ACCEPT" 194 | if !network.icc { 195 | iccAction = "DROP" 196 | } 197 | 198 | if network.internal { 199 | return &Ruleset{ 200 | // internal: drop traffic to docker network from foreign subnet 201 | // notice: rule is different from IPv4 counterpart because NDP should not be blocked 202 | NewPrependRule(TableFilter, ChainDockerIsolation1, 203 | "!", "-i", network.bridge, 204 | "-o", network.bridge, 205 | "-j", "DROP"), 206 | // internal: drop traffic from docker network to foreign subnet 207 | // notice: rule is different from IPv4 counterpart because NDP should not be blocked 208 | NewPrependRule(TableFilter, ChainDockerIsolation1, 209 | "!", "-o", network.bridge, 210 | "-i", network.bridge, 211 | "-j", "DROP"), 212 | // ICC 213 | NewRule(TableFilter, ChainForward, 214 | "-i", network.bridge, 215 | "-o", network.bridge, 216 | "-j", iccAction), 217 | } 218 | } 219 | 220 | rs := Ruleset{ 221 | // not internal: catch if packet wants to leave docker network (stage 1) 222 | NewPrependRule(TableFilter, ChainDockerIsolation1, 223 | "-i", network.bridge, 224 | "!", "-o", network.bridge, 225 | "-j", ChainDockerIsolation2), 226 | // not internal: if packet wants to enter another docker network, drop it (stage 2) 227 | NewPrependRule(TableFilter, ChainDockerIsolation2, 228 | "-o", network.bridge, 229 | "-j", "DROP"), 230 | // not internal: check ingoing traffic to docker network for new connections in additional chain 231 | NewRule(TableFilter, ChainForward, 232 | "-o", network.bridge, 233 | "-j", ChainDocker), 234 | // not internal: allow ingoing traffic to docker network for established connections 235 | NewRule(TableFilter, ChainForward, 236 | "-o", network.bridge, 237 | "-m", "conntrack", 238 | "--ctstate", "RELATED,ESTABLISHED", 239 | "-j", "ACCEPT"), 240 | // not internal: allow outgoing traffic from docker network 241 | NewRule(TableFilter, ChainForward, 242 | "-i", network.bridge, 243 | "!", "-o", network.bridge, 244 | "-j", "ACCEPT"), 245 | // ICC 246 | NewRule(TableFilter, ChainForward, 247 | "-i", network.bridge, 248 | "-o", network.bridge, 249 | "-j", iccAction), 250 | // masquerade packets if they enter the docker network 251 | NewPrependRule(TableNat, ChainPostrouting, 252 | "-o", network.bridge, 253 | "-m", "addrtype", 254 | "--dst-type", "LOCAL", 255 | "-j", "MASQUERADE"), 256 | } 257 | 258 | if network.masquerade { 259 | rs = append(rs, 260 | // masquerade packets if they leave the docker network 261 | NewPrependRule(TableNat, ChainPostrouting, 262 | "-s", network.subnet.String(), 263 | "!", "-o", network.bridge, 264 | "-j", "MASQUERADE"), 265 | ) 266 | } 267 | 268 | if !hairpinMode { 269 | rs = append(rs, NewPrependRule(TableNat, ChainDocker, 270 | "-i", network.bridge, 271 | "-j", "RETURN")) 272 | } 273 | 274 | return &rs 275 | } 276 | 277 | func getRulesForContainer(container *managedContainer, hairpinMode bool) *Ruleset { 278 | if container == nil { 279 | return &Ruleset{} 280 | } 281 | 282 | rs := make(Ruleset, 0, len(container.ports)*3) 283 | for _, port := range container.ports { 284 | rs = append(rs, *getRulesForPort(&port, container, hairpinMode)...) 285 | } 286 | 287 | return &rs 288 | } 289 | 290 | func getRulesForPort(port *managedPort, container *managedContainer, hairpinMode bool) *Ruleset { 291 | containerPortString := strconv.Itoa(int(port.port)) 292 | hostPortString := strconv.Itoa(int(port.hostPort)) 293 | hostAddressString := "0/0" 294 | if !port.hostAddress.IsUnspecified() { 295 | hostAddressString = port.hostAddress.String() 296 | } 297 | 298 | dnatRule := NewRule(TableNat, ChainDocker, 299 | "-d", hostAddressString, 300 | "-p", port.proto, 301 | "-m", port.proto, 302 | "--dport", hostPortString, 303 | "-j", "DNAT", 304 | "--to-destination", net.JoinHostPort(container.address.String(), containerPortString)) 305 | 306 | if !hairpinMode { 307 | dnatRule.spec = append(dnatRule.spec, "!", "-i", container.bridge) 308 | } 309 | 310 | return &Ruleset{ 311 | NewRule(TableFilter, ChainDocker, 312 | "-d", container.address.String(), 313 | "!", "-i", container.bridge, 314 | "-o", container.bridge, 315 | "-p", port.proto, 316 | "-m", port.proto, 317 | "--dport", containerPortString, 318 | "-j", "ACCEPT"), 319 | NewRule(TableNat, ChainPostrouting, 320 | "-s", container.address.String(), 321 | "-d", container.address.String(), 322 | "-p", port.proto, 323 | "-m", port.proto, 324 | "--dport", containerPortString, 325 | "-j", "MASQUERADE"), 326 | dnatRule, 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BUILDER=ipv6nat-builder 4 | DIST_DIR=dist 5 | VERSION="${1}" 6 | 7 | if [ -z "${VERSION}" ] 8 | then 9 | echo "Usage: ${0} X.X.X" 10 | exit 1 11 | fi 12 | 13 | export DOCKER_HOST= 14 | export DOCKER_CONTEXT=default 15 | 16 | cd `dirname "${0}"` 17 | 18 | set -e 19 | set -x 20 | 21 | docker buildx rm "${BUILDER}" || true 22 | docker buildx create --name "${BUILDER}" --use 23 | docker buildx build \ 24 | --platform "linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7" \ 25 | --pull \ 26 | --push \ 27 | --tag "robbertkl/ipv6nat:${VERSION}" \ 28 | --tag "robbertkl/ipv6nat:latest" \ 29 | . 30 | 31 | BUILDER_CONTAINER="buildx_buildkit_${BUILDER}0" 32 | docker exec "${BUILDER_CONTAINER}" sh -c \ 33 | 'mkdir /dist; mv /var/lib/buildkit/runc-overlayfs/snapshots/snapshots/*/fs/docker-ipv6nat.* /dist' 34 | rm -Rf "${DIST_DIR}/" 35 | docker cp "${BUILDER_CONTAINER}:/dist" "${DIST_DIR}" 36 | 37 | docker buildx use default 38 | docker buildx rm "${BUILDER}" 39 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package dockeripv6nat 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "strconv" 7 | 8 | "github.com/fsouza/go-dockerclient" 9 | ) 10 | 11 | // State keeps track of the current Docker containers and networks to apply relative updates to the manager 12 | type State struct { 13 | manager *Manager 14 | networks map[string]*managedNetwork 15 | containers map[string]*managedContainer 16 | } 17 | 18 | // fc00::/7, Unique Local IPv6 Unicast Addresses, see RFC 4193 19 | var ulaCIDR = net.IPNet{ 20 | IP: net.ParseIP("fc00::"), 21 | Mask: net.CIDRMask(7, 128), 22 | } 23 | 24 | // NewState constructs a new state 25 | func NewState(debug bool) (*State, error) { 26 | manager, err := NewManager(debug) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &State{ 32 | manager: manager, 33 | networks: make(map[string]*managedNetwork), 34 | containers: make(map[string]*managedContainer), 35 | }, nil 36 | } 37 | 38 | // Cleanup resets the state 39 | func (s *State) Cleanup() error { 40 | s.RemoveMissingContainers([]string{}) 41 | s.RemoveMissingNetworks([]string{}) 42 | 43 | if err := s.manager.Cleanup(); err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | // RemoveMissingNetworks removes any of the given networks, if they don't exist 51 | func (s *State) RemoveMissingNetworks(networkIDs []string) error { 52 | for id := range s.networks { 53 | if !contains(networkIDs, id) { 54 | if err := s.UpdateNetwork(id, nil); err != nil { 55 | return err 56 | } 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // RemoveMissingContainers removes any of the given containers if they don't exist 64 | func (s *State) RemoveMissingContainers(containerIDs []string) error { 65 | for id := range s.containers { 66 | if !contains(containerIDs, id) { 67 | if err := s.UpdateContainer(id, nil); err != nil { 68 | return err 69 | } 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // UpdateNetwork applies a network, which can add, remove or update it 77 | func (s *State) UpdateNetwork(id string, network *docker.Network) error { 78 | oldNetwork := s.networks[id] 79 | newNetwork := s.parseNetwork(network) 80 | 81 | if oldNetwork != nil || newNetwork != nil { 82 | if err := s.manager.ReplaceNetwork(oldNetwork, newNetwork); err != nil { 83 | return err 84 | } 85 | } 86 | 87 | if newNetwork == nil { 88 | delete(s.networks, id) 89 | } else { 90 | s.networks[id] = newNetwork 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // UpdateContainer applies a container, which can add, remove or update it 97 | func (s *State) UpdateContainer(id string, container *docker.Container) error { 98 | oldContainer := s.containers[id] 99 | newContainer := s.parseContainer(container) 100 | 101 | if oldContainer != nil || newContainer != nil { 102 | if err := s.manager.ReplaceContainer(oldContainer, newContainer); err != nil { 103 | return err 104 | } 105 | } 106 | 107 | if newContainer == nil { 108 | delete(s.containers, id) 109 | } else { 110 | s.containers[id] = newContainer 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (s *State) parseNetwork(network *docker.Network) *managedNetwork { 117 | if network == nil { 118 | return nil 119 | } 120 | 121 | // Don't check network.EnableIPv6, since this will be false before Docker 1.11.0, even if we have IPv6 subnets. 122 | 123 | if network.Driver != "bridge" { 124 | return nil 125 | } 126 | 127 | n := managedNetwork{ 128 | id: network.ID, 129 | bridge: "br-" + network.ID[:12], 130 | icc: true, 131 | masquerade: true, 132 | internal: network.Internal, 133 | binding: net.ParseIP("::"), 134 | } 135 | 136 | for _, config := range network.IPAM.Config { 137 | _, subnet, err := net.ParseCIDR(config.Subnet) 138 | if err != nil { 139 | continue 140 | } 141 | if ulaCIDR.Contains(subnet.IP) { 142 | n.subnet = *subnet 143 | break 144 | } 145 | } 146 | 147 | if n.subnet.IP == nil { 148 | return nil 149 | } 150 | 151 | for key, value := range network.Options { 152 | switch key { 153 | case "com.docker.network.bridge.name": 154 | n.bridge = value 155 | case "com.docker.network.bridge.enable_icc": 156 | b, err := strconv.ParseBool(value) 157 | if err != nil { 158 | log.Printf("invalid value for com.docker.network.bridge.enable_icc (network %s)", network.ID) 159 | break 160 | } 161 | n.icc = b 162 | case "com.docker.network.bridge.enable_ip_masquerade": 163 | b, err := strconv.ParseBool(value) 164 | if err != nil { 165 | log.Printf("invalid value for com.docker.network.bridge.enable_ip_masquerade (network %s)", network.ID) 166 | break 167 | } 168 | n.masquerade = b 169 | case "com.docker.network.bridge.host_binding_ipv6": 170 | ip := net.ParseIP(value) 171 | if ip == nil || ip.To4() != nil { 172 | log.Printf("invalid value for com.docker.network.bridge.host_binding_ipv6 (network %s)", network.ID) 173 | break 174 | } 175 | n.binding = ip 176 | } 177 | } 178 | 179 | return &n 180 | } 181 | 182 | func (s *State) findFirstKnownNetwork(networks map[string]docker.ContainerNetwork) (*managedNetwork, net.IP) { 183 | for _, network := range networks { 184 | ip := net.ParseIP(network.GlobalIPv6Address) 185 | if !ulaCIDR.Contains(ip) { 186 | continue 187 | } 188 | 189 | n, found := s.networks[network.NetworkID] 190 | if !found || n.internal { 191 | continue 192 | } 193 | 194 | return n, ip 195 | } 196 | 197 | return nil, nil 198 | } 199 | 200 | func (s *State) getKnownNetworks() []*managedNetwork { 201 | networks := make([]*managedNetwork, len(s.networks)) 202 | index := 0 203 | for _, network := range s.networks { 204 | networks[index] = network 205 | index++ 206 | } 207 | 208 | return networks 209 | } 210 | 211 | func (s *State) parseContainer(container *docker.Container) *managedContainer { 212 | if container == nil { 213 | return nil 214 | } 215 | 216 | network, containerAddress := s.findFirstKnownNetwork(container.NetworkSettings.Networks) 217 | if network == nil { 218 | return nil 219 | } 220 | 221 | if network.internal { 222 | return nil 223 | } 224 | 225 | ports := make([]managedPort, 0) 226 | for port, bindings := range container.HostConfig.PortBindings { 227 | proto := port.Proto() 228 | containerPort, err := parsePort(port.Port()) 229 | if err != nil { 230 | log.Printf("invalid port %s for container %s", port.Port(), container.ID) 231 | continue 232 | } 233 | 234 | for _, binding := range bindings { 235 | hostAddress := network.binding 236 | 237 | if binding.HostIP != "" && binding.HostIP != "0.0.0.0" { 238 | ip := net.ParseIP(binding.HostIP) 239 | if ip == nil || ip.To4() != nil { 240 | // Skip bindings to IPv4. 241 | continue 242 | } 243 | 244 | hostAddress = ip 245 | } 246 | 247 | hostPort, err := parsePort(binding.HostPort) 248 | if err != nil { 249 | log.Printf("invalid port %s for container %s", binding.HostPort, container.ID) 250 | continue 251 | } 252 | 253 | ports = append(ports, managedPort{ 254 | port: containerPort, 255 | proto: proto, 256 | hostAddress: hostAddress, 257 | hostPort: hostPort, 258 | }) 259 | } 260 | } 261 | 262 | if len(ports) == 0 { 263 | return nil 264 | } 265 | 266 | return &managedContainer{ 267 | id: container.ID, 268 | address: containerAddress, 269 | bridge: network.bridge, 270 | ports: ports, 271 | } 272 | } 273 | 274 | func parsePort(rawPort string) (uint16, error) { 275 | port, err := strconv.ParseUint(rawPort, 10, 16) 276 | if err != nil { 277 | return 0, err 278 | } 279 | return uint16(port), nil 280 | } 281 | 282 | func contains(haystack []string, needle string) bool { 283 | for _, item := range haystack { 284 | if item == needle { 285 | return true 286 | } 287 | } 288 | 289 | return false 290 | } 291 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | package dockeripv6nat 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/fsouza/go-dockerclient" 12 | ) 13 | 14 | // RecoverableError wraps an error to signal the application does not need to crash 15 | type RecoverableError struct { 16 | err error 17 | } 18 | 19 | func (re *RecoverableError) Error() string { 20 | return re.err.Error() 21 | } 22 | 23 | // retryInterval is the number of seconds to wait after connection failure 24 | const retryInterval = 10 25 | 26 | // Watcher processes Docker events and applies them to the state 27 | type Watcher struct { 28 | client *docker.Client 29 | state *State 30 | eventChannel chan *docker.APIEvents 31 | signalChannel chan os.Signal 32 | retry bool 33 | } 34 | 35 | // NewWatcher constructs a new watcher 36 | func NewWatcher(client *docker.Client, state *State, retry bool) *Watcher { 37 | return &Watcher{ 38 | client: client, 39 | state: state, 40 | retry: retry, 41 | } 42 | } 43 | 44 | // Watch starts watching for new Docker events to process 45 | func (w *Watcher) Watch() error { 46 | w.signalChannel = make(chan os.Signal, 1) 47 | signal.Notify(w.signalChannel, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL) 48 | defer signal.Stop(w.signalChannel) 49 | 50 | done := false 51 | for !done { 52 | if w.eventChannel == nil { 53 | if err := w.attemptRecovery(w.setupListener()); err != nil { 54 | return err 55 | } 56 | } 57 | 58 | var err error 59 | done, err = w.processOnce() 60 | if err := w.attemptRecovery(err); err != nil { 61 | return err 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (w *Watcher) attemptRecovery(err error) error { 69 | if err == nil { 70 | return nil 71 | } 72 | 73 | if errRecoverable, match := err.(*RecoverableError); match && w.retry { 74 | if w.eventChannel != nil { 75 | w.client.RemoveEventListener(w.eventChannel) 76 | w.eventChannel = nil 77 | } 78 | log.Printf("%v", errRecoverable.err) 79 | return nil 80 | } 81 | 82 | return err 83 | } 84 | 85 | func (w *Watcher) setupListener() error { 86 | // Always try a ping first 87 | if err := w.client.Ping(); err != nil { 88 | return &RecoverableError{err} 89 | } 90 | 91 | w.eventChannel = make(chan *docker.APIEvents, 1024) 92 | if err := w.client.AddEventListener(w.eventChannel); err != nil { 93 | return &RecoverableError{err} 94 | } 95 | 96 | if err := w.regenerate(); err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (w *Watcher) processOnce() (bool, error) { 104 | select { 105 | case <-time.After(retryInterval * time.Second): 106 | if w.eventChannel != nil { 107 | if err := w.client.Ping(); err != nil { 108 | return false, &RecoverableError{err} 109 | } 110 | } 111 | case event, ok := <-w.eventChannel: 112 | if !ok { 113 | return false, &RecoverableError{errors.New("docker daemon connection interrupted")} 114 | } 115 | if err := w.handleEvent(event); err != nil { 116 | // Wrap in a RecoverableError so that a regenerate will be initiated. 117 | return false, &RecoverableError{err} 118 | } 119 | case sig := <-w.signalChannel: 120 | if sig == syscall.SIGHUP { 121 | // Return a RecoverableError so that a regenerate will be initiated. 122 | return false, &RecoverableError{errors.New("received SIGHUP")} 123 | } 124 | return true, nil 125 | } 126 | 127 | return false, nil 128 | } 129 | 130 | func (w *Watcher) regenerate() error { 131 | networks, err := w.client.ListNetworks() 132 | if err != nil { 133 | return &RecoverableError{err} 134 | } 135 | 136 | networkIDs := make([]string, len(networks)) 137 | for index, network := range networks { 138 | networkIDs[index] = network.ID 139 | if err := w.state.UpdateNetwork(network.ID, &network); err != nil { 140 | return err 141 | } 142 | } 143 | 144 | apiContainers, err := w.client.ListContainers(docker.ListContainersOptions{}) 145 | if err != nil { 146 | return &RecoverableError{err} 147 | } 148 | 149 | containerIDs := make([]string, len(apiContainers)) 150 | for index, apiContainer := range apiContainers { 151 | containerIDs[index] = apiContainer.ID 152 | container, err := w.client.InspectContainer(apiContainer.ID) 153 | if err != nil { 154 | if _, match := err.(*docker.NoSuchContainer); match { 155 | container = nil 156 | } else { 157 | return &RecoverableError{err} 158 | } 159 | } 160 | if err := w.state.UpdateContainer(apiContainer.ID, container); err != nil { 161 | return err 162 | } 163 | } 164 | 165 | if err := w.state.RemoveMissingContainers(containerIDs); err != nil { 166 | return err 167 | } 168 | 169 | if err := w.state.RemoveMissingNetworks(networkIDs); err != nil { 170 | return err 171 | } 172 | 173 | return nil 174 | } 175 | 176 | func (w *Watcher) handleEvent(event *docker.APIEvents) error { 177 | if event.Type != "network" { 178 | return nil 179 | } 180 | 181 | networkID := event.Actor.ID 182 | 183 | switch event.Action { 184 | case "create": 185 | network, err := w.client.NetworkInfo(networkID) 186 | if err != nil { 187 | if _, match := err.(*docker.NoSuchNetwork); match { 188 | network = nil 189 | } else { 190 | return &RecoverableError{err} 191 | } 192 | } 193 | if err := w.state.UpdateNetwork(networkID, network); err != nil { 194 | return err 195 | } 196 | case "destroy": 197 | if err := w.state.UpdateNetwork(networkID, nil); err != nil { 198 | return err 199 | } 200 | case "connect", "disconnect": 201 | containerID := event.Actor.Attributes["container"] 202 | container, err := w.client.InspectContainer(containerID) 203 | if err != nil { 204 | if _, match := err.(*docker.NoSuchContainer); match { 205 | container = nil 206 | } else { 207 | return &RecoverableError{err} 208 | } 209 | } 210 | if err := w.state.UpdateContainer(containerID, container); err != nil { 211 | return err 212 | } 213 | } 214 | 215 | return nil 216 | } 217 | --------------------------------------------------------------------------------