├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── example-iptables.txt └── src ├── docker-fw.go ├── docker.go ├── graph.go ├── iptables.go ├── lookupCache.go ├── start.go └── state.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/docker-fw 2 | 3 | .gopath/ 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/github.com/fsouza/go-dockerclient"] 2 | path = vendor/github.com/fsouza/go-dockerclient 3 | url = https://github.com/fsouza/go-dockerclient 4 | [submodule "vendor/github.com/pborman/getopt"] 5 | path = vendor/github.com/pborman/getopt 6 | url = https://github.com/pborman/getopt 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bin/docker-fw: 2 | mkdir -p bin .gopath 3 | if [ ! -L .gopath/src ]; then ln -s "$(CURDIR)/vendor" .gopath/src; fi 4 | cd src && GOBIN="$(CURDIR)/bin/" GOPATH="$(CURDIR)/.gopath" go install && mv ../bin/src ../bin/docker-fw 5 | 6 | all: bin/docker-fw errcheck test 7 | 8 | errcheck: 9 | mkdir -p bin .gopath 10 | if [ ! -L .gopath/src ]; then ln -s "$(CURDIR)/vendor" .gopath/src; fi 11 | cd src && GOPATH="$(CURDIR)/.gopath" errcheck 12 | 13 | test: 14 | mkdir -p bin .gopath 15 | if [ ! -L .gopath/src ]; then ln -s "$(CURDIR)/vendor" .gopath/src; fi 16 | cd src && GOPATH="$(CURDIR)/.gopath" go test -v 17 | 18 | .PHONY: all deps test errcheck bin/docker-fw 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Docker-fw 2 | ========= 3 | 4 | docker-fw is a complementary tool for [Docker](https://docker.com/) to manage their iptables firewall rules; it features persistence of rules and dynamic port assignments, in case host or container are restarted. 5 | 6 | docker-fw expects your firewall to be using the ``*filter FORWARD`` chain with a default policy of REJECT/DROP (or an equivalent rule at bottom); this is default behavior starting from Docker version 1.5. 7 | 8 | docker-fw does not work with Docker daemon ``--restart`` options because docker-fw would not be called automatically on container start. However, you can customize initialization of containers on host boot script via ``/etc/rc.local``, for example to loop through existing containers and initialize their firewall rules using ``docker-fw start``. 9 | 10 | It is also possible to use this utility completely manage your internal docker0 bridge traffic between containers, as it will play nicely along with ``--icc=false`` and ``--iptables=true`` Docker daemon options. 11 | 12 | Willing to contribute? Please submit a [pull request](https://github.com/gdm85/docker-fw/pulls) or [create an issue](https://github.com/gdm85/docker-fw/issues/new). 13 | 14 | Iptables workflow explanation 15 | ============================= 16 | 17 | This is how docker-fw expects network flow to happen (e.g. iptables explained in human terms) under a strict whitelisting firewall: 18 | 19 | 1. no link between FORWARD and DOCKER chains for all traffic from any source (rule ``FORWARD -o docker0 -j DOCKER`` added by Docker and removed by ``docker-fw init``) 20 | 2. all internal traffic on FORWARD chain is linked to DOCKER chain (``FORWARD -i docker0 -o docker0 -j DOCKER`` as 1st rule) 21 | 3. existing connections keep being forwarded (rule ``FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT`` added by Docker is not touched) 22 | 4. outgoing connections from all containers are kept being forwarded (``FORWARD -i docker0 ! -o docker0 -j ACCEPT`` added by Docker is not touched) 23 | 5. a DROP rule is appeneded on FORWARD table as exiting rule 24 | 6. custom firewall rules are added before such DROP rule (usually with an insert) and/or to the DOCKER chain itself 25 | 26 | See also [example-iptables.txt](example-iptables.txt). 27 | 28 | License 29 | ======= 30 | 31 | [Author](https://github.com/gdm85) is not officially involved with Docker development, thus this is not an official tool either. 32 | 33 | docker-fw is licensed under GNU GPL version 2, see [LICENSE](LICENSE). 34 | 35 | Building 36 | ======== 37 | 38 | Running the ``make`` command should suffice. The Makefile will use a locally-generated `GOPATH` without populating it with any package; all source code 39 | dependencies are submodules under `vendor/`. 40 | 41 | Actions 42 | ======== 43 | 44 | Init 45 | ---- 46 | 47 | Removes the iptables rule added by docker daemon at startup `-o docker0 -j DOCKER` from ``*filter FORWARD`` chain. 48 | It will fail if docker daemon is not running or if rule does not exist. 49 | 50 | docker-fw init 51 | 52 | Add actions 53 | ----------- 54 | 55 | 'add' is used to add a firewall specification for a container (any network external to Docker circuit, e.g. 192.168.178.0/24 or a public internet address) and targets the FORWARD chain, while 'add-internal'/'add-two-ways' target the INPUT chain. 56 | If a valid container id/name is specified, then its IPv4 will be always aliased by docker-fw. Some special values exist for address specification: 57 | - `.` to reference the container for which rules are being added 58 | - `/` to reference the Docker host (usually 172.17.42.1) 59 | 60 | **NOTE**: referencing the Docker host `/` is mostly intended for the 'add-internal' action; since it is considered a poor practice to create firewall rules to allow traffic that target the docker host 61 | 62 | docker-fw add container-id --source=(1.2.3.4|.|container-id) [--rev-lookup] [--sport=xxxx] [--dest=(1.2.3.4|.|container-id)] [--dport=xxxx] [--protocol=(tcp|udp)] [--filter="-i docker0 -o docker0"] 63 | docker-fw (add-internal|add-two-ways) container-id --source=(1.2.3.4|.|container-id|/) [--rev-lookup] [--sport=xxxx] --dest=(1.2.3.4|.|container-id|/) --dport=xxxx [--protocol=(tcp|udp)] [--filter="-i docker0 -o docker0"] 64 | 65 | Some rules to use 'add', 'add-two-ways', 'add-internal' and 'add-input': 66 | - address specifications (source/destination) can also be in IPv4 subnet notation 67 | - specifying ``--dport`` is mandatory for 'add-internal' action. 68 | - protocol default is 'tcp'. 69 | - at least source or destination must be equivalent to '.' (container for which rule is being specified), but cannot be both. If no destination is specified, '.' is assumed. 70 | - specification of extra iptables filter is optional, and empty by default 71 | - using ``--rev-lookup`` allows to specify a container IPv4 address, that otherwise would be an error (name/id form is preferred) 72 | 73 | 'add-two-ways' requires that source is a container and performs two tasks: 74 | - execute add-internal with the specified rule 75 | - always make sure that the source container will have a /etc/hosts rule for the source container 76 | - the internal rules and the custom hosts will be restored when using ``docker-fw start`` for the container 77 | 78 | These commands can also parse and add multiple rules from a file or stdin (using '-' as filename): 79 | 80 | docker-fw add --from=(filename|-) 81 | docker-fw add-internal --from=(filename|-) 82 | docker-fw add-input --from=(filename|-) 83 | 84 | When using ``--from``, any other parameter (except ``--rev-lookup``) is disallowed. 85 | 86 | Two-ways linking 87 | ---------------- 88 | 89 | An example of how to apply two-ways linking (assumes ``--icc=false`` on your Docker daemon): 90 | ``` 91 | export IMAGE=ubuntu 92 | docker run --detach --name=promoted $IMAGE sleep 1000 93 | docker run --detach --expose=1025 --link promoted:promoted --name=endpoint $IMAGE sleep 1000 94 | 95 | ## enable iptables + hosts via docker-fw 96 | docker-fw add-two-ways endpoint --source promoted --dport 1025 97 | 98 | ## test (it's advised to use 2 terminals for these commands) 99 | docker exec endpoint nc -l 1025 & 100 | 101 | docker exec promoted sh -c "echo 'Hello from promoted container' | nc endpoint 1025" 102 | ``` 103 | 104 | Save-hostconfig 105 | --------------- 106 | 107 | Save host configuration of a running and correctly network-enabled container. 108 | Such configuration will be used when starting the container through docker-fw. 109 | It always happens by default after a successful start. 110 | 111 | See also https://github.com/docker/docker/issues/8723 112 | 113 | docker-fw save-hostconfig container1 [container2] [container3] [...] [containerN] 114 | 115 | Replay 116 | ------ 117 | 118 | Replay all firewall rules; will not add them again if existing on current iptables and will update the IPv4 addresses referenced in source/destination by looking up the aliases (if any specified). 119 | Use ``--dry-run`` to display which stateful changes would be applied, and report exit code zero only if there would be none. 120 | 121 | docker-fw replay [--dry-run] container1 [container2] [container3] [...] [containerN] 122 | 123 | Ls 124 | -- 125 | 126 | List all existing firewall rules for specified container(s); if no container is specified, all containers' rules will be displayed. 127 | 128 | docker-fw ls [container1] [container2] [container3] [...] [containerN] 129 | 130 | Drop 131 | ---- 132 | 133 | Drop all firewall rules for specified container; iptables rules are deleted and the json file that contains them is deleted from the container directory. 134 | 135 | docker-fw drop container1 [container2] [container3] [...] [containerN] 136 | 137 | Allow 138 | ----- 139 | 140 | Allow specified source address (external) as an 'add' command for each of the available published ports of the container. 141 | 142 | docker-fw allow container-id ip-address-1 [ip-address-2] [ip-address-3] [...] [ip-address-N] 143 | 144 | This command is explicitly meant to allow access from external networks to the container's network address. 145 | 146 | Start 147 | ----- 148 | 149 | docker-fw start [--dry-run] [--paused] [--pull-deps] container1 [container2] [container3] [...] [containerN] 150 | 151 | It does the following: 152 | - sort input list of containers second their dependencies 153 | - start each of them sequentially (paused when ``--paused`` is specified) 154 | - execute the equivalent of 'replay' action for each container as it is started 155 | 156 | The option ``--paused`` allows to start containers in paused status (for example in case user doesn't want to allow any activity until all firewall restore operations are completed). 157 | The option ``--pull-deps`` will automatically make dependant (by link relationship) containers part of the selection. 158 | If a container is already started or paused, its state is not changed. 159 | By specifying ``--dry-run`` containers will be displayed in the order they would be started, but their state will not be changed. 160 | 161 | ### Dependencies 162 | Please note that Docker currently (1.8) lacks a correct dependency [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph) when starting containers, thus it does not start them in correct order (unless you use ``--restart=true`` has a hack); unfortunately, nothing is mentioned in [documentation there](https://docs.docker.com/articles/host_integration/) regarding this issue, which is solved as explained above by docker-fw start action (even if you don't use any of the other docker-fw features). 163 | 164 | See also: 165 | * https://github.com/docker/docker/issues/8821 166 | * https://github.com/docker/docker/issues/11777 167 | 168 | Internals 169 | ========= 170 | 171 | docker-fw uses [Docker API](https://docs.docker.com/reference/api/docker_remote_api/) through [go-dockerclient](https://github.com/fsouza/go-dockerclient), and command-line based iptables access; [libiptc](http://tldp.org/HOWTO/Querying-libiptc-HOWTO/) is not being used because its API is not published (and it would be a tad too complex, see also [go-libiptc](https://github.com/gdm85/go-libiptc)). 172 | 173 | Container information is retrieved via API when needed and cached for the duration of the execution of docker-fw. 174 | Any id/name valid for the Docker API can be used with docker-fw. 175 | 176 | Known issues 177 | ============ 178 | 179 | * Has some hardcoded features/settings inherited from Docker defaults (e.g. 172.x.x.x subnet) 180 | * Not thoroughly tested, and no unit tests coverage 181 | * Stores its ``.json`` descriptors in Docker's own containers metadata directory 182 | 183 | All of the above can be addressed with some effort, and probably will (in due time); as always, patches welcome! 184 | 185 | Troubleshooting 186 | =============== 187 | 188 | If you see an error like this when running ``docker-fw init``: 189 | ``` 190 | 2015/01/24 21:01:20 init: Could not find docker-added rule 191 | ``` 192 | 193 | You have two issues: 194 | * you didn't [RTFM](https://en.wikipedia.org/wiki/RTFM) :) 195 | * you are using Docker older than version 1.5 (it didn't have [this PR](https://github.com/docker/docker/pull/7003) merged in its codebase) 196 | -------------------------------------------------------------------------------- /example-iptables.txt: -------------------------------------------------------------------------------- 1 | # Generated by iptables-save v1.4.21 on Thu Mar 5 11:28:01 2015 2 | *nat 3 | :PREROUTING ACCEPT [4526:5946974] 4 | :INPUT ACCEPT [6:450] 5 | :OUTPUT ACCEPT [0:0] 6 | :POSTROUTING ACCEPT [0:0] 7 | :DOCKER - [0:0] 8 | -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER 9 | -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER 10 | -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE 11 | COMMIT 12 | # Completed on Thu Mar 5 11:28:01 2015 13 | # Generated by iptables-save v1.4.21 on Thu Mar 5 11:28:01 2015 14 | *filter 15 | :INPUT ACCEPT [66:8669] 16 | ## in a public access configuration, you probably don't want your host to act as an open router, 17 | ## thus you'd either change the default policy or implement proper filtering 18 | :FORWARD ACCEPT [0:0] 19 | :OUTPUT ACCEPT [45:8433] 20 | :DOCKER - [0:0] 21 | -A FORWARD -i docker0 -o docker0 -j DOCKER 22 | -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 23 | -A FORWARD -i docker0 ! -o docker0 -j ACCEPT 24 | -A FORWARD -i docker0 -o docker0 -j DROP 25 | COMMIT 26 | # Completed on Thu Mar 5 11:28:01 2015 27 | -------------------------------------------------------------------------------- /src/docker-fw.go: -------------------------------------------------------------------------------- 1 | /* 2 | * docker-fw v0.2.4 - a complementary tool for Docker to manage custom 3 | * firewall rules between/towards Docker containers 4 | * Copyright (C) 2014~2016 gdm85 - https://github.com/gdm85/docker-fw/ 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | 21 | package main 22 | 23 | import ( 24 | "bufio" 25 | "errors" 26 | "fmt" 27 | "log" 28 | "os" 29 | "regexp" 30 | "strings" 31 | 32 | "github.com/pborman/getopt" 33 | ) 34 | 35 | const ( 36 | version = "0.2.4" 37 | ADDR_SPEC = "Can be either an IPv4 address, a subnet, one of the special aliases ('.' = container IPv4, '/' = docker host IPv4) or a container id. If an IPv4 address is specified and no subnet, '/32' will be added. Default is '.'" 38 | // directly from Docker 39 | validContainerNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.-]` 40 | ) 41 | 42 | type Action struct { 43 | ContainerId string 44 | VerboseArg, SourceArg, SourcePortArg, DestArg, DestPortArg, ProtoArg, FilterArg, FromArg, ReverseLookupContainerIPv4Arg getopt.Option 45 | CommandSet *getopt.Set 46 | 47 | source, dest, proto, filter string 48 | reverseLookupContainerIPv4 bool 49 | sourcePort, destPort uint16 50 | verbose bool 51 | } 52 | 53 | var ( 54 | containerIdMatch = regexp.MustCompile(`^/?` + validContainerNameChars + `+$`) 55 | ) 56 | 57 | func NewAction(allowParseNames bool) *Action { 58 | var a Action 59 | a.CommandSet = getopt.New() 60 | a.CommandSet.SetProgram("docker-fw (init|start|allow|add|add-input|add-two-ways|add-internal|ls|save-hostconfig|replay|drop) containerId") 61 | a.CommandSet.SetParameters("\n\nSyntax for all add actions:\n\tdocker-fw (add|add-input|add-two-ways|add-internal) ...") 62 | 63 | a.VerboseArg = a.CommandSet.BoolVarLong(&a.verbose, "verbose", 'v', "use more verbose output, prints all iptables operations") 64 | 65 | // define all command line options 66 | a.SourceArg = a.CommandSet.StringVarLong(&a.source, "source", 's', "source-specification*", ".") 67 | a.SourcePortArg = a.CommandSet.Uint16VarLong(&a.sourcePort, "sport", 0, "Source port, optional", "port") 68 | a.DestArg = a.CommandSet.StringVarLong(&a.dest, "dest", 'd', "destination-specification*", ".") 69 | a.DestPortArg = a.CommandSet.Uint16VarLong(&a.destPort, "dport", 0, "Destination port, mandatory only for 'add-input', 'add-two-ways' and 'add-internal' actions", "port") 70 | a.ProtoArg = a.CommandSet.EnumVarLong(&a.proto, "protocol", 'p', []string{"tcp", "udp"}, "The protocol of the packet to check") 71 | a.FilterArg = a.CommandSet.StringVarLong(&a.filter, "filter", 0, "extra iptables conditions") 72 | if allowParseNames { 73 | a.ReverseLookupContainerIPv4Arg = a.CommandSet.BoolVarLong(&a.reverseLookupContainerIPv4, "rev-lookup", 0, "allow specifying addresses in 172.* subnet and map them back to container names") 74 | } 75 | 76 | // explicitly set all option defaults 77 | a.proto = "tcp" 78 | a.source = "." 79 | a.dest = "." 80 | a.sourcePort = 0 81 | a.destPort = 0 82 | a.filter = "" 83 | 84 | return &a 85 | } 86 | 87 | func (a *Action) CreateRule() (*IptablesRule, error) { 88 | return NewIptablesRule(a.ContainerId, a.source, a.sourcePort, a.dest, a.destPort, a.proto, a.filter, a.reverseLookupContainerIPv4) 89 | } 90 | 91 | func (a *Action) Validate(action string) error { 92 | // make source argument mandatory for better readability 93 | // although it could safely default to '.' (due to the check that src != dest), 94 | // it is better to have it explicit for readability 95 | if !a.SourceArg.Seen() { 96 | return errors.New("--source is mandatory") 97 | } 98 | if action == "add-input" || action == "add-internal" || action == "add-two-ways" { 99 | if !a.DestPortArg.Seen() { 100 | return errors.New("--dport is mandatory") 101 | } 102 | } 103 | 104 | //NOTE: enforcement of different source/destination happens in NewIptablesRule() 105 | 106 | if a.SourcePortArg.Seen() && a.sourcePort == 0 { 107 | return errors.New("Invalid source port specified") 108 | } 109 | 110 | if a.DestPortArg.Seen() && a.destPort == 0 { 111 | return errors.New("Invalid destination port specified") 112 | } 113 | 114 | if len(a.dest) == 0 { 115 | return errors.New("Invalid destination specification") 116 | } 117 | 118 | if len(a.source) == 0 { 119 | return errors.New("Invalid source specification") 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func runCommandsFromScanner(scanner *bufio.Scanner, action string) error { 126 | lineNo := 0 127 | for scanner.Scan() { 128 | lineNo++ 129 | 130 | // create a new 'commandLine' for each input line, 131 | // but always use same action for all lines 132 | commandLine := NewAction(false) 133 | // set executable name 134 | newArgs := []string{os.Args[0]} 135 | newArgs = append(newArgs, strings.Split(scanner.Text(), " ")...) 136 | if err := commandLine.Parse(newArgs); err != nil { 137 | return errors.New(fmt.Sprintf("%s: error at line %d: %s", action, lineNo, err)) 138 | } 139 | 140 | err := commandLine.ExecuteAddAction(action) 141 | if err != nil { 142 | return errors.New(fmt.Sprintf("[file] %s: %s", action, err)) 143 | } 144 | } 145 | 146 | if err := scanner.Err(); err != nil { 147 | return err 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func (a *Action) Parse(args []string) error { 154 | return a.CommandSet.Getopt(args, nil) 155 | } 156 | 157 | func (a *Action) Usage() { 158 | fmt.Printf(`docker-fw version %s, Copyright (C) gdm85 https://github.com/gdm85/docker-fw/ 159 | docker-fw comes with ABSOLUTELY NO WARRANTY; for details see LICENSE 160 | This is free software, and you are welcome to redistribute it 161 | under certain conditions`, version) 162 | a.CommandSet.PrintUsage(os.Stdout) 163 | fmt.Printf("\n* = %s\n", ADDR_SPEC) 164 | fmt.Printf("\nSyntax for 'allow' action:\n\tdocker-fw allow address1 [address2] [address3] [...] [addressN]\nA list of IPv4 addresses is accepted\n\n") 165 | fmt.Printf("Syntax for 'ls' action:\n\tdocker-fw ls [container1] [container2] [container3] [...] [containerN]\nA list of 0 or more container IDs/names is accepted\n\n") 166 | fmt.Printf("Syntax for 'drop' action:\n\tdocker-fw drop container1 [container2] [container3] [...] [containerN]\nA list of container IDs/names is accepted\n\n") 167 | fmt.Printf("Syntax for 'save-hostconfig' action:\n\tdocker-fw save-hostconfig container1 [container2] [container3] [...] [containerN]\nA list of container IDs/names is accepted\n\n") 168 | fmt.Printf("Syntax for 'replay' action:\n\tdocker-fw replay [--dry-run] container1 [container2] [container3] [...] [containerN]\nA list of container IDs/names is accepted\n\n") 169 | fmt.Printf("Syntax for 'start' action:\n\tdocker-fw start [--dry-run] [--paused] [--pull-deps] container1 [container2] [container3] [...] [containerN]\n") 170 | fmt.Printf("A list of container IDs/names is accepted; option '--paused' allows to start containers in paused status, option '--pull-deps' allows to pull dependencies in selection, option --dry-run shows container names in the order they would be started without changing their state\n") 171 | } 172 | 173 | func (a *Action) ExecuteAddAction(action string) error { 174 | err := a.Validate(action) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | rule, err := a.CreateRule() 180 | if err != nil { 181 | return err 182 | } 183 | 184 | if action == "add" { 185 | if isDockerIPv4(rule.Source) && isDockerIPv4(rule.Destination) { 186 | return errors.New("Trying to add an external firewall rule for internal Docker traffic") 187 | } 188 | 189 | err = AddFirewallRule(a.ContainerId, rule) 190 | } else if action == "add-input" { 191 | err = AddInputRule(a.ContainerId, rule) 192 | } else if action == "add-internal" { 193 | err = AddInternalRule(a.ContainerId, rule) 194 | } else if action == "add-two-ways" { 195 | err = AddTwoWays(a.ContainerId, rule) 196 | } else { 197 | // only add* actions are supported when importing from file 198 | return errors.New("cannot execute this action: " + action) 199 | } 200 | return err 201 | } 202 | 203 | var verboseOutput bool 204 | 205 | func main() { 206 | // all possible command line arguments 207 | var from string 208 | cliArgs := NewAction(true) 209 | fromArg := cliArgs.CommandSet.StringVarLong(&from, "from", 0, "", "file|-") 210 | 211 | // if no arguments specified, show help and exit with failure 212 | if len(os.Args) == 1 || (len(os.Args) == 2 && (os.Args[1] == "-h" || os.Args[1] == "--help")) { 213 | cliArgs.Usage() 214 | os.Exit(1) 215 | return 216 | } 217 | 218 | action := os.Args[1] 219 | switch action { 220 | case "init": 221 | if len(os.Args) == 3 { 222 | if os.Args[2] == "--verbose" { 223 | verboseOutput = true 224 | } else { 225 | log.Fatal("init action takes no command line arguments (except --verbose)") 226 | os.Exit(1) 227 | return 228 | } 229 | } 230 | if len(os.Args) > 3 { 231 | log.Fatal("init action takes no command line arguments (except --verbose)") 232 | os.Exit(1) 233 | return 234 | } 235 | 236 | err := InitializeFirewall() 237 | if err != nil { 238 | log.Fatalf("%s: %s", action, err) 239 | return 240 | } 241 | 242 | // success 243 | os.Exit(0) 244 | return 245 | case "allow": 246 | if len(os.Args) < 3 { 247 | log.Fatalf("%s: no container id specified", action) 248 | os.Exit(1) 249 | return 250 | } 251 | if len(os.Args) < 4 { 252 | log.Fatalf("%s: no whitelist addresses specified", action) 253 | os.Exit(1) 254 | return 255 | } 256 | // pick container id 257 | containerId := os.Args[2] 258 | 259 | if !containerIdMatch.MatchString(containerId) { 260 | log.Fatalf("not a valid container id: %s", containerId) 261 | return 262 | } 263 | 264 | err := AllowExternal(containerId, os.Args[3:]) 265 | // parse error 266 | if err != nil { 267 | log.Printf("%s: %s", action, err) 268 | os.Exit(2) 269 | return 270 | } 271 | os.Exit(0) 272 | return 273 | case "start": 274 | if len(os.Args) < 3 { 275 | log.Fatalf("%s: no container ids specified", action) 276 | os.Exit(1) 277 | return 278 | } 279 | containerIds := []string{} 280 | paused := false 281 | dryRun := false 282 | pullDeps := false 283 | for _, arg := range os.Args[2:] { 284 | // is the famous '--paused' option? 285 | if strings.HasPrefix(arg, "--") { 286 | switch arg { 287 | case "--paused": 288 | paused = true 289 | break 290 | case "--dry-run": 291 | dryRun = true 292 | break 293 | case "--pull-deps": 294 | pullDeps = true 295 | break 296 | default: 297 | log.Fatalf("%s: unknown option: %s", action, arg) 298 | return 299 | } 300 | 301 | continue 302 | } 303 | 304 | // pick container id 305 | if !containerIdMatch.MatchString(arg) { 306 | log.Fatalf("not a valid container id: %s", arg) 307 | return 308 | } 309 | containerIds = append(containerIds, arg) 310 | } 311 | 312 | exitCode, err := StartContainers(containerIds, paused, pullDeps, dryRun) 313 | // parse error 314 | if err != nil { 315 | log.Printf("%s: %s", action, err) 316 | } 317 | os.Exit(exitCode) 318 | return 319 | case "replay": 320 | if len(os.Args) < 3 { 321 | log.Fatalf("%s: insufficient command line arguments specified", action) 322 | os.Exit(1) 323 | return 324 | } 325 | 326 | dryRun := false 327 | containerIds := []string{} 328 | for _, arg := range os.Args[2:] { 329 | 330 | if arg == "--dry-run" { 331 | dryRun = true 332 | continue 333 | } 334 | 335 | // pick container id 336 | if !containerIdMatch.MatchString(arg) { 337 | log.Fatalf("not a valid container id: %s", arg) 338 | return 339 | } 340 | containerIds = append(containerIds, arg) 341 | } 342 | 343 | if len(containerIds) == 0 { 344 | log.Fatalf("%s: no containers specified", action) 345 | os.Exit(1) 346 | return 347 | } 348 | 349 | exitCode, err := ReplayRules(containerIds, dryRun) 350 | if err != nil { 351 | log.Printf("%s: %s", action, err) 352 | os.Exit(exitCode) 353 | return 354 | } 355 | 356 | os.Exit(exitCode) 357 | return 358 | case "ls": 359 | containerIds := []string{} 360 | for _, arg := range os.Args[2:] { 361 | // pick container id 362 | if !containerIdMatch.MatchString(arg) { 363 | log.Fatalf("not a valid container id: %s", arg) 364 | return 365 | } 366 | containerIds = append(containerIds, arg) 367 | } 368 | 369 | err := ListRules(containerIds) 370 | if err != nil { 371 | log.Printf("%s: %s", action, err) 372 | os.Exit(2) 373 | return 374 | } 375 | 376 | os.Exit(0) 377 | return 378 | case "drop", "save-hostconfig": 379 | if len(os.Args) < 3 { 380 | log.Fatalf("%s: no container ids specified", action) 381 | os.Exit(1) 382 | return 383 | } 384 | containerIds := []string{} 385 | for _, arg := range os.Args[2:] { 386 | // pick container id 387 | if !containerIdMatch.MatchString(arg) { 388 | log.Fatalf("not a valid container id: %s", arg) 389 | return 390 | } 391 | containerIds = append(containerIds, arg) 392 | } 393 | 394 | var err error 395 | switch action { 396 | case "drop": 397 | err = DropRules(containerIds) 398 | case "save-hostconfig": 399 | err = BackupHostConfig(containerIds, true, false) 400 | default: 401 | panic("not yet implemented action: " + action) 402 | } 403 | if err != nil { 404 | log.Printf("%s: %s", action, err) 405 | os.Exit(2) 406 | return 407 | } 408 | 409 | os.Exit(0) 410 | return 411 | case "add-two-ways", "add-internal", "add", "add-input": 412 | if len(os.Args) < 3 { 413 | log.Fatalf("%s: no container id specified", action) 414 | os.Exit(1) 415 | return 416 | } 417 | 418 | // pick container id 419 | containerId := os.Args[2] 420 | cliArgs.ContainerId = containerId 421 | 422 | if !containerIdMatch.MatchString(containerId) { 423 | log.Fatalf("not a valid container id: %s", containerId) 424 | return 425 | } 426 | break 427 | default: 428 | log.Fatalf("Unknown action: %s", action) 429 | return 430 | } 431 | 432 | // parse all except those already manually parsed 433 | newArgs := []string{os.Args[0]} 434 | newArgs = append(newArgs, os.Args[3:]...) 435 | if err := cliArgs.Parse(newArgs); err != nil { 436 | fmt.Fprintln(os.Stderr, err) 437 | cliArgs.Usage() 438 | os.Exit(1) 439 | return 440 | } 441 | 442 | // if a source for a list of actions is not specified, take a shortcut to direct action processing 443 | if !fromArg.Seen() { 444 | err := cliArgs.ExecuteAddAction(action) 445 | if err != nil { 446 | log.Fatalf("%s: %s", action, err) 447 | return 448 | } 449 | 450 | // success 451 | os.Exit(0) 452 | } 453 | 454 | if cliArgs.SourceArg.Seen() || cliArgs.SourcePortArg.Seen() || cliArgs.DestArg.Seen() || cliArgs.DestPortArg.Seen() || cliArgs.ProtoArg.Seen() || cliArgs.FilterArg.Seen() { 455 | log.Fatal("When using --from, only '--rev-lookup' is allowed") 456 | return 457 | } 458 | 459 | // read all commands line by line from stdin 460 | var err error 461 | if from == "-" { 462 | err = runCommandsFromScanner(bufio.NewScanner(os.Stdin), action) 463 | } else { 464 | file, err := os.Open(from) 465 | if err == nil { 466 | err = runCommandsFromScanner(bufio.NewScanner(file), action) 467 | if err != nil { 468 | log.Fatal(err) 469 | } 470 | err = file.Close() 471 | } 472 | } 473 | 474 | if err != nil { 475 | log.Fatal(err) 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /src/docker.go: -------------------------------------------------------------------------------- 1 | /* 2 | * docker-fw v0.2.4 - a complementary tool for Docker to manage custom 3 | * firewall rules between/towards Docker containers 4 | * Copyright (C) 2014~2016 gdm85 - https://github.com/gdm85/docker-fw/ 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package main 21 | 22 | import ( 23 | "bytes" 24 | "errors" 25 | "fmt" 26 | "sort" 27 | "strings" 28 | 29 | "github.com/fsouza/go-dockerclient" 30 | ) 31 | 32 | type ExecResult struct { 33 | Stdout, Stderr string 34 | ExitCode int 35 | } 36 | 37 | var Docker *docker.Client 38 | 39 | func init() { 40 | var err error 41 | Docker, err = docker.NewClient("unix:///var/run/docker.sock") 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | 47 | func areEquivalentArrays(a, b []string) bool { 48 | if a == nil && b == nil { 49 | return true 50 | } 51 | 52 | if len(a) != len(b) { 53 | return false 54 | } 55 | 56 | sort.Strings(a) 57 | sort.Strings(b) 58 | 59 | // compare each element 60 | l := len(a) 61 | for i := 0; i < l; i++ { 62 | if a[i] != b[i] { 63 | return false 64 | } 65 | } 66 | 67 | return true 68 | } 69 | 70 | func arePortBindingsEqual(a, b map[docker.Port][]docker.PortBinding) bool { 71 | if a == nil && b == nil { 72 | return true 73 | } 74 | 75 | if len(a) != len(b) { 76 | return false 77 | } 78 | 79 | // retrieve keys & convert each binding to a string, for ease of comparison 80 | aKeys := []string{} 81 | aValues := map[string][]string{} 82 | for key, value := range a { 83 | aKeys = append(aKeys, string(key)) 84 | 85 | serialized := []string{} 86 | for _, binding := range value { 87 | serialized = append(serialized, binding.HostIP+":"+binding.HostPort) 88 | } 89 | 90 | aValues[string(key)] = serialized 91 | } 92 | 93 | bKeys := []string{} 94 | bValues := map[string][]string{} 95 | for key, value := range b { 96 | bKeys = append(bKeys, string(key)) 97 | 98 | serialized := []string{} 99 | for _, binding := range value { 100 | serialized = append(serialized, binding.HostIP+":"+binding.HostPort) 101 | } 102 | 103 | bValues[string(key)] = serialized 104 | } 105 | 106 | // keys must match 107 | if !areEquivalentArrays(aKeys, bKeys) { 108 | return false 109 | } 110 | 111 | // then traverse through the common keys to check if values match 112 | for key, _ := range a { 113 | if !areEquivalentArrays(aValues[string(key)], bValues[string(key)]) { 114 | return false 115 | } 116 | } 117 | 118 | return true 119 | } 120 | 121 | func asGoodAs(orig *docker.HostConfig, current *docker.HostConfig) bool { 122 | return orig.NetworkMode == current.NetworkMode && 123 | areEquivalentArrays(orig.Links, current.Links) && 124 | areEquivalentArrays(orig.DNS, current.DNS) && 125 | areEquivalentArrays(orig.DNSSearch, current.DNSSearch) && 126 | areEquivalentArrays(orig.ExtraHosts, current.ExtraHosts) && 127 | areEquivalentArrays(orig.VolumesFrom, current.VolumesFrom) && 128 | areEquivalentArrays(orig.Binds, current.Binds) && 129 | areEquivalentArrays(orig.CapAdd, current.CapAdd) && 130 | areEquivalentArrays(orig.CapDrop, current.CapDrop) && 131 | orig.PublishAllPorts == current.PublishAllPorts && 132 | orig.RestartPolicy.Name == current.RestartPolicy.Name && 133 | orig.Privileged == current.Privileged && 134 | arePortBindingsEqual(orig.PortBindings, current.PortBindings) 135 | } 136 | 137 | func containerExec(cid string, cmd []string) (*ExecResult, error) { 138 | config := docker.CreateExecOptions{ 139 | Container: cid, 140 | AttachStdin: false, 141 | AttachStdout: true, 142 | AttachStderr: true, 143 | Tty: false, 144 | Cmd: cmd, 145 | } 146 | execObj, err := Docker.CreateExec(config) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | // Docker.SkipServerVersionCheck = true 152 | var stdout, stderr bytes.Buffer 153 | opts := docker.StartExecOptions{ 154 | OutputStream: &stdout, 155 | ErrorStream: &stderr, 156 | Detach: false, 157 | } 158 | 159 | // start execution & join 160 | err = Docker.StartExec(execObj.ID, opts) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | // inspect to retrieve exit code 166 | inspect, err := Docker.InspectExec(execObj.ID) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | return &ExecResult{ 172 | ExitCode: inspect.ExitCode, 173 | Stdout: stdout.String(), 174 | Stderr: stderr.String(), 175 | }, nil 176 | } 177 | 178 | func containerInject(cid, path, content string) error { 179 | // first truncate the existing hosts file 180 | // 'truncate', like 'cat', are part of coreutils and expected to be found within container 181 | result, err := containerExec(cid, []string{"truncate", "--size=0", "/etc/hosts"}) 182 | if err != nil { 183 | return err 184 | } 185 | if result.ExitCode != 0 { 186 | return errors.New(fmt.Sprintf("failed to truncate hosts inside container: %s", result.Stderr)) 187 | } 188 | 189 | // proceed to append new data 190 | config := docker.CreateExecOptions{ 191 | Container: cid, 192 | AttachStdin: true, 193 | AttachStdout: true, 194 | AttachStderr: true, 195 | Tty: false, 196 | Cmd: []string{"sh", "-c", "cat >> " + path}, 197 | } 198 | execObj, err := Docker.CreateExec(config) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | // Docker.SkipServerVersionCheck = true 204 | var stdout, stderr bytes.Buffer 205 | opts := docker.StartExecOptions{ 206 | InputStream: strings.NewReader(content), 207 | OutputStream: &stdout, 208 | ErrorStream: &stderr, 209 | Detach: false, 210 | } 211 | 212 | // start execution 213 | err = Docker.StartExec(execObj.ID, opts) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | // inspect to retrieve exit code 219 | inspect, err := Docker.InspectExec(execObj.ID) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | if inspect.ExitCode != 0 { 225 | return errors.New(fmt.Sprintf("failed to cat inside container: %s", stderr.String())) 226 | } 227 | 228 | return nil 229 | } 230 | -------------------------------------------------------------------------------- /src/graph.go: -------------------------------------------------------------------------------- 1 | /* 2 | * docker-fw v0.2.4 - a complementary tool for Docker to manage custom 3 | * firewall rules between/towards Docker containers 4 | * Copyright (C) 2014~2016 gdm85 - https://github.com/gdm85/docker-fw/ 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package main 21 | 22 | import ( 23 | "github.com/fsouza/go-dockerclient" 24 | ) 25 | 26 | type Node struct { 27 | ingress int 28 | 29 | ID string // same as Container.ID 30 | Name string // used for debugging/dry-run purposes 31 | children SortableNodeArray // all direct one-way links (slice of container names) 32 | } 33 | 34 | type SortableNodeArray []*Node 35 | 36 | func NewNode(container *docker.Container) *Node { 37 | return &Node{ 38 | ID: container.ID, 39 | Name: container.Name[1:], 40 | children: SortableNodeArray{}, 41 | } 42 | } 43 | 44 | func (node *Node) LinkTo(child *Node) { 45 | node.children = append(node.children, child) 46 | child.ingress++ 47 | } 48 | 49 | /// 50 | /// 'a' node is 'less' than 'b' node if and only if: 51 | /// - 'a' links to 'b' 52 | /// - on any of the nodes visited by link paths starting from 'a', there is 'b', 53 | /// or 'b' is in any of the link paths starting from any nodes visited in such link paths 54 | /// 55 | /// nodes that have no incoming links (or already started nodes) have priority and go on top of the list 56 | /// based on code by bjarneh - https://github.com/bjarneh/godag/blob/master/src/cmplr/dag.go 57 | /// 58 | func (arr SortableNodeArray) TopSort() SortableNodeArray { 59 | zero := SortableNodeArray{} 60 | sorted := SortableNodeArray{} 61 | 62 | for _, v := range arr { 63 | if v.ingress == 0 { 64 | zero = append(zero, v) 65 | } 66 | } 67 | 68 | for len(zero) > 0 { 69 | 70 | node := zero[0] 71 | zero = zero[1:] // Pop 72 | 73 | for _, child := range node.children { 74 | child.ingress-- 75 | if child.ingress == 0 { 76 | zero = append(zero, child) 77 | } 78 | } 79 | sorted = append(sorted, node) 80 | } 81 | 82 | if len(sorted) < len(arr) { 83 | panic("found cycle in DAG") 84 | } 85 | 86 | return sorted 87 | } 88 | -------------------------------------------------------------------------------- /src/iptables.go: -------------------------------------------------------------------------------- 1 | /* 2 | * docker-fw v0.2.4 - a complementary tool for Docker to manage custom 3 | * firewall rules between/towards Docker containers 4 | * Copyright (C) 2014~2016 gdm85 - https://github.com/gdm85/docker-fw/ 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | 21 | package main 22 | 23 | import ( 24 | "encoding/json" 25 | "errors" 26 | "fmt" 27 | "io/ioutil" 28 | "os" 29 | "os/exec" 30 | "regexp" 31 | "strings" 32 | "syscall" 33 | 34 | "github.com/fsouza/go-dockerclient" 35 | ) 36 | 37 | const ( 38 | IPTABLES_BINARY = "iptables" 39 | DOCKER_HOST = "172.17.42.1/32" 40 | DOCKER_CHAIN = "DOCKER" 41 | ) 42 | 43 | type IptablesRule struct { 44 | Source string 45 | SourceAlias string // optional 46 | SourcePort uint16 // optional 47 | Destination string 48 | DestinationAlias string // optional 49 | DestinationPort uint16 // optional 50 | Protocol string 51 | Filter string // optional 52 | } 53 | 54 | type ActiveIptablesRule struct { 55 | IptablesRule 56 | Chain string 57 | JumpTo string 58 | } 59 | 60 | type IptablesRulesCollection struct { 61 | cid string 62 | Rules []*ActiveIptablesRule 63 | } 64 | 65 | var ( 66 | matchIpv4 *regexp.Regexp 67 | ccl *CachedContainerLookup 68 | ) 69 | 70 | func (r *ActiveIptablesRule) Position() int { 71 | if r.Chain == "FORWARD" { 72 | return 2 73 | } else if r.Chain == "INPUT" { 74 | return 1 75 | } else { 76 | panic("Cannot determine position for chain " + r.Chain) 77 | } 78 | } 79 | 80 | func init() { 81 | // test that iptables works 82 | exitCode, stdo, stde, err := iptablesRun("--version", true) 83 | if err != nil { 84 | panic(fmt.Sprintf("iptables: %s", err)) 85 | } 86 | if exitCode != 0 { 87 | fmt.Fprintln(os.Stdout, stdo) 88 | fmt.Fprintln(os.Stderr, stde) 89 | panic("iptables: not available") 90 | } 91 | 92 | matchIpv4, err = regexp.Compile("^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))(/[0-9]{1,2})?$") 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | // initialize cache used for all operations 98 | ccl = &CachedContainerLookup{containers: map[string]*docker.Container{}, networkAddress: map[string]*docker.Container{}} 99 | } 100 | 101 | func isDockerIPv4(ipv4 string) bool { 102 | return strings.HasPrefix(ipv4, "172.") 103 | } 104 | 105 | func iptablesRun(commandLine string, isCheck bool) (int, string, string, error) { 106 | var err error 107 | 108 | commandLine = IPTABLES_BINARY + " " + commandLine 109 | cmd := exec.Command("sh", "-c", commandLine) 110 | cmd.Env = os.Environ() 111 | cmd.Dir, err = os.Getwd() 112 | if err != nil { 113 | return 1, "","",err 114 | } 115 | 116 | stdout, err := cmd.StdoutPipe() 117 | if err != nil { 118 | return 1, "","",err 119 | } 120 | stderr, err := cmd.StderrPipe() 121 | if err != nil { 122 | return 1, "","",err 123 | } 124 | 125 | if verboseOutput { 126 | if isCheck { 127 | fmt.Printf("docker-fw CHECK: %s\n", commandLine) 128 | } else { 129 | fmt.Printf("docker-fw: %s\n", commandLine) 130 | } 131 | } 132 | err = cmd.Start() 133 | if err != nil { 134 | return 1, "","",err 135 | } 136 | 137 | var bytes []byte 138 | if bytes, err = ioutil.ReadAll(stdout); err != nil { 139 | return 1, "","",err 140 | } 141 | stdo := string(bytes) 142 | 143 | if bytes, err = ioutil.ReadAll(stderr); err != nil { 144 | return 1, "","",err 145 | } 146 | stde := string(bytes) 147 | 148 | var exitCode int 149 | if err := cmd.Wait(); err != nil { 150 | if exitError, ok := err.(*exec.ExitError); ok { 151 | if status, ok := exitError.Sys().(syscall.WaitStatus); ok { 152 | exitCode = status.ExitStatus() 153 | } else { 154 | panic("cannot read exit status") 155 | } 156 | } else { 157 | panic(err) 158 | } 159 | } 160 | 161 | return exitCode, stdo, stde, nil 162 | } 163 | 164 | func InitializeFirewall() error { 165 | // check if daemon is running 166 | err := Docker.Ping() 167 | if err != nil { 168 | return err 169 | } 170 | 171 | // this Docker-added rule must be disposed, see https://github.com/docker/docker/issues/6034#issuecomment-58742268 172 | rule := "FORWARD -o docker0 -j " + DOCKER_CHAIN 173 | exists, err := RuleExists(rule) 174 | if err != nil { 175 | return err 176 | } 177 | if exists { 178 | err := internalDelete(rule, false) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | // insert new rule for internal docker traffic on top 184 | err = internalInsert(1, "FORWARD -i docker0 -o docker0 -j DOCKER") 185 | if err != nil { 186 | return err 187 | } 188 | } else { 189 | return errors.New("Could not find docker-added rule") 190 | } 191 | 192 | //TODO: check that our inserted rule is still on top 193 | // possibly extend this check everywhere iptables is touched 194 | 195 | return nil 196 | } 197 | 198 | func NewIptablesRule(cid string, source string, sourcePort uint16, dest string, destPort uint16, proto, filter string, reverseLookupContainerIPv4 bool) (*IptablesRule, error) { 199 | container, err := ccl.LookupOnlineContainer(cid) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | rule := IptablesRule{} 205 | 206 | rule.Source, rule.SourceAlias, err = ccl.ParseAddress(source, container, reverseLookupContainerIPv4) 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | rule.Destination, rule.DestinationAlias, err = ccl.ParseAddress(dest, container, reverseLookupContainerIPv4) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | // enforce a valid flow specification 217 | if rule.Source == rule.Destination { 218 | return nil, errors.New("cannot add rule with same source and destination") 219 | } 220 | 221 | if rule.SourceAlias != "." && rule.DestinationAlias != "." { 222 | return nil, errors.New("either source or destination must be the container itself") 223 | } 224 | 225 | rule.SourcePort = sourcePort 226 | rule.DestinationPort = destPort 227 | rule.Protocol = proto 228 | rule.Filter = filter 229 | 230 | return &rule, nil 231 | } 232 | 233 | // corresponding to a subcommand 234 | // function to allow incoming traffic for a specific container 235 | func AllowExternal(cid string, whitelist4 []string) error { 236 | container, err := ccl.LookupOnlineContainer(cid) 237 | if err != nil { 238 | return err 239 | } 240 | 241 | containerIpv4 := container.NetworkSettings.IPAddress + "/32" 242 | 243 | for _, port := range container.NetworkSettings.PortMappingAPI() { 244 | // skip this port, it has not been published 245 | if port.PrivatePort == 0 { 246 | continue 247 | } 248 | 249 | if port.Type != "tcp" && port.Type != "udp" { 250 | return errors.New(fmt.Sprintf("Unrecognized protocol '%s' for port %d of container %s", port.Type, port.PrivatePort, cid)) 251 | } 252 | if port.IP != "0.0.0.0" { 253 | return errors.New(fmt.Sprintf("Unrecognized host ip '%s' for binding of port %d (container %s)", port.IP, port.PrivatePort, cid)) 254 | } 255 | 256 | // create a rule for each whitelisted external IPv4 257 | for _, wIpv4 := range whitelist4 { 258 | wIpv4 = strings.Trim(wIpv4, " ") 259 | 260 | // always make IPv4 specific, unless a subnet is specified 261 | if !strings.Contains(wIpv4, "/") { 262 | wIpv4 += "/32" 263 | } 264 | 265 | rule := IptablesRule{ 266 | Source: wIpv4, Destination: containerIpv4, Protocol: port.Type, DestinationPort: uint16(port.PrivatePort), 267 | DestinationAlias: ".", 268 | Filter: "! -i docker0 -o docker0", 269 | } 270 | 271 | err := addFirewallRule(container, &rule) 272 | if err != nil { 273 | return err 274 | } 275 | } 276 | } 277 | 278 | return nil 279 | } 280 | 281 | // format in docker-fw style 282 | func (rule *IptablesRule) FormatAsFwAction() string { 283 | s := fmt.Sprintf("-s %s -d %s -p %s", rule.SourceAliasOrAddress(), rule.DestinationAliasOrAddress(), rule.Protocol) 284 | if rule.Filter != "" { 285 | s += fmt.Sprintf(" --filter '%s'", rule.Filter) 286 | } 287 | if rule.DestinationPort != 0 { 288 | s += fmt.Sprintf(" --dport %d", rule.DestinationPort) 289 | } 290 | if rule.SourcePort != 0 { 291 | s += fmt.Sprintf(" --sport %d", rule.SourcePort) 292 | } 293 | 294 | return s 295 | } 296 | 297 | func (rule *IptablesRule) Format() string { 298 | s := fmt.Sprintf("-s %s -d %s %s -p %s -m %s", rule.Source, rule.Destination, rule.Filter, rule.Protocol, rule.Protocol) 299 | if rule.DestinationPort != 0 { 300 | s += fmt.Sprintf(" --dport %d", rule.DestinationPort) 301 | } 302 | if rule.SourcePort != 0 { 303 | s += fmt.Sprintf(" --sport %d", rule.SourcePort) 304 | } 305 | 306 | return s 307 | } 308 | 309 | func (rule *ActiveIptablesRule) Format() string { 310 | return fmt.Sprintf("%s %s -j %s", rule.Chain, rule.IptablesRule.Format(), rule.JumpTo) 311 | } 312 | 313 | // guess the action that was used to create this rule 314 | // NOTE: rules create through 'allow' will not return back an 'allow' action 315 | func (rule *ActiveIptablesRule) ExtrapolateAction() string { 316 | if rule.Chain == "INPUT" && rule.JumpTo == "ACCEPT" { 317 | return "add-input" 318 | } 319 | if rule.Chain == DOCKER_CHAIN && rule.JumpTo == "ACCEPT" { 320 | return "add-internal" 321 | } 322 | if rule.Chain == "FORWARD" && rule.JumpTo == DOCKER_CHAIN { 323 | return "add" 324 | } 325 | panic("not yet implemented: proper de-serialization of rule " + rule.Format()) 326 | } 327 | 328 | func (rule *ActiveIptablesRule) FormatAsFwCommand(target string) string { 329 | return fmt.Sprintf("%s %s %s", rule.ExtrapolateAction(), target, rule.IptablesRule.FormatAsFwAction()) 330 | } 331 | 332 | func (rule *IptablesRule) SourceAliasOrAddress() string { 333 | if rule.SourceAlias != "" { 334 | return rule.SourceAlias 335 | } 336 | 337 | return rule.Source 338 | } 339 | 340 | func (rule *IptablesRule) DestinationAliasOrAddress() string { 341 | if rule.DestinationAlias != "" { 342 | return rule.DestinationAlias 343 | } 344 | 345 | return rule.Destination 346 | } 347 | 348 | // used for comparison of rules 349 | func (rule *IptablesRule) Aliases() string { 350 | s := "" 351 | if rule.DestinationAlias != "" { 352 | s += fmt.Sprintf("%s=%s\n", rule.DestinationAlias, rule.Destination) 353 | } 354 | if rule.SourceAlias != "" { 355 | s += fmt.Sprintf("%s=%s\n", rule.SourceAlias, rule.Source) 356 | } 357 | return s 358 | } 359 | 360 | // corresponding to a subcommand ('add') 361 | func AddFirewallRule(cid string, iptRule *IptablesRule) error { 362 | container, err := ccl.LookupOnlineContainer(cid) 363 | if err != nil { 364 | return err 365 | } 366 | 367 | return addFirewallRule(container, iptRule) 368 | } 369 | 370 | func addFirewallRule(container *docker.Container, iptRule *IptablesRule) error { 371 | addedRule := ActiveIptablesRule{Chain: "FORWARD", JumpTo: DOCKER_CHAIN} 372 | addedRule.IptablesRule = *iptRule 373 | 374 | // insert always on top 375 | // NOTE: the catchall "-o docker0 -j DOCKER" must *not* exist in table 376 | err := internalInsert(addedRule.Position(), addedRule.Format()) 377 | if err != nil { 378 | return err 379 | } 380 | 381 | return recordRule(container, &addedRule) 382 | } 383 | 384 | // corresponding to a subcommand (add-input) 385 | func AddInputRule(cid string, iptRule *IptablesRule) error { 386 | container, err := ccl.LookupOnlineContainer(cid) 387 | if err != nil { 388 | return err 389 | } 390 | 391 | addedRule := ActiveIptablesRule{Chain: "INPUT", JumpTo: "ACCEPT"} 392 | addedRule.IptablesRule = *iptRule 393 | 394 | err = internalInsert(addedRule.Position(), addedRule.Format()) 395 | if err != nil { 396 | return err 397 | } 398 | 399 | return recordRule(container, &addedRule) 400 | } 401 | 402 | // corresponding to action add-two-ways 403 | func AddTwoWays(cid string, iptRule *IptablesRule) error { 404 | // create or update the two-ways hook for source 405 | if iptRule.SourceAlias == "" { 406 | return errors.New("Source must be a container id/name") 407 | } 408 | err := updateCustomHosts(iptRule.SourceAlias, cid) 409 | if err != nil { 410 | return err 411 | } 412 | 413 | // this is necessary because of --icc=false 414 | err = AddInternalRule(cid, iptRule) 415 | if err != nil { 416 | return err 417 | } 418 | return nil 419 | } 420 | 421 | // corresponding to a subcommand (add-internal) 422 | func AddInternalRule(cid string, iptRule *IptablesRule) error { 423 | container, err := ccl.LookupOnlineContainer(cid) 424 | if err != nil { 425 | return err 426 | } 427 | 428 | addedRule := ActiveIptablesRule{Chain: DOCKER_CHAIN, JumpTo: "ACCEPT"} 429 | addedRule.IptablesRule = *iptRule 430 | 431 | err = internalAppend(cid, addedRule.Format()) 432 | if err != nil { 433 | return err 434 | } 435 | 436 | return recordRule(container, &addedRule) 437 | } 438 | 439 | func (c *IptablesRulesCollection) Append(iptRule *ActiveIptablesRule) { 440 | c.Rules = append(c.Rules, iptRule) 441 | } 442 | 443 | func (c *IptablesRulesCollection) fileName() string { 444 | return fmt.Sprintf("/var/lib/docker/containers/%s/extraRules.json", c.cid) 445 | } 446 | 447 | func (c *IptablesRulesCollection) Remove() error { 448 | return os.Remove(c.fileName()) 449 | } 450 | 451 | func (c *IptablesRulesCollection) Save() error { 452 | bytes, err := json.Marshal(&c) 453 | if err != nil { 454 | return err 455 | } 456 | err = ioutil.WriteFile(c.fileName(), bytes, 0666) 457 | return err 458 | } 459 | 460 | func DropRules(containerIds []string) error { 461 | for _, cid := range containerIds { 462 | container, err := ccl.LookupContainer(cid) 463 | if err != nil { 464 | return err 465 | } 466 | 467 | c, err := LoadRules(container) 468 | if err != nil { 469 | return err 470 | } 471 | 472 | //NOTE: will not delete a JSON representing an empty array 473 | if len(c.Rules) == 0 { 474 | return nil 475 | } 476 | 477 | for _, r := range c.Rules { 478 | // attempt to delete, do not make a permanent failure 479 | _ = internalDelete(r.Format(), true) 480 | } 481 | 482 | err = c.Remove() 483 | if err != nil { 484 | return err 485 | } 486 | } 487 | return nil 488 | } 489 | 490 | // store iptables rule in a JSON descriptor 491 | func recordRule(container *docker.Container, iptRule *ActiveIptablesRule) error { 492 | c, err := LoadRules(container) 493 | if err != nil { 494 | return err 495 | } 496 | 497 | // check if rule is already there 498 | for _, r := range c.Rules { 499 | if r.Format() == iptRule.Format() && r.Aliases() == iptRule.Aliases() { 500 | // already tracked, skip 501 | fmt.Printf("docker-fw: rule '%s' already tracked\n", r.Format()) 502 | return nil 503 | } 504 | } 505 | 506 | // add the new rule 507 | c.Append(iptRule) 508 | 509 | return c.Save() 510 | } 511 | 512 | func HasAnyRule() (bool, error) { 513 | return false, nil 514 | } 515 | 516 | // check if rule exists 517 | func RuleExists(rule string) (bool, error) { 518 | exitCode, stdo, stde, err := iptablesRun("--wait -C "+rule, true) 519 | if err != nil { 520 | return false, err 521 | } 522 | if exitCode == 1 { 523 | return false, nil 524 | } 525 | if exitCode == 0 { 526 | return true, nil 527 | } 528 | // unexpected exit code 529 | fmt.Fprintln(os.Stdout, stdo) 530 | fmt.Fprintln(os.Stderr, stde) 531 | return false, errors.New("cannot determine if rule exists") 532 | } 533 | 534 | func internalAppend(containerId, rule string) error { 535 | exists, err := RuleExists(rule) 536 | if err != nil { 537 | return err 538 | } 539 | if exists { 540 | fmt.Printf("docker-fw: iptables(%s): rule '%s' already exists, not appending\n", containerId, rule) 541 | return nil 542 | } 543 | 544 | parts := strings.SplitN(rule, " ", 2) 545 | // now append rule 546 | exitCode, stdo, stde, err := iptablesRun(fmt.Sprintf("--wait -A %s %s", parts[0], parts[1]), false) 547 | if err != nil { 548 | panic(fmt.Sprintf("iptables(%s): %s", containerId, err)) 549 | } 550 | if exitCode != 0 { 551 | fmt.Fprintln(os.Stdout, stdo) 552 | fmt.Fprintln(os.Stderr, stde) 553 | return errors.New(fmt.Sprintf("iptables(%s): cannot append rule '%s'", containerId, rule)) 554 | } 555 | 556 | return nil 557 | } 558 | 559 | func internalInsert(pos int, rule string) error { 560 | exists, err := RuleExists(rule) 561 | if err != nil { 562 | return err 563 | } 564 | if exists { 565 | fmt.Printf("docker-fw: iptables: rule '%s' already exists, not inserting\n", rule) 566 | return nil 567 | } 568 | 569 | parts := strings.SplitN(rule, " ", 2) 570 | // now insert rule 571 | exitCode, stdo, stde, err := iptablesRun(fmt.Sprintf("--wait -I %s %d %s", parts[0], pos, parts[1]), false) 572 | if err != nil { 573 | return err 574 | } 575 | if exitCode != 0 { 576 | fmt.Fprintln(os.Stdout, stdo) 577 | fmt.Fprintln(os.Stderr, stde) 578 | return errors.New("cannot insert iptables rule") 579 | } 580 | 581 | return nil 582 | } 583 | 584 | func internalDelete(rule string, quiet bool) error { 585 | // now insert rule 586 | exitCode, stdo, stde, err := iptablesRun("--wait -D "+rule, false) 587 | if err != nil { 588 | // unexpected failure while running external command 589 | return err 590 | } 591 | if exitCode != 0 { 592 | if !quiet { 593 | fmt.Fprintln(os.Stdout, stdo) 594 | fmt.Fprintln(os.Stderr, stde) 595 | } 596 | return errors.New("cannot delete iptables rule") 597 | } 598 | 599 | return nil 600 | } 601 | 602 | // execute again all rules stored for specified container 603 | func ReplayRules(containerIds []string, dryRun bool) (int, error) { 604 | hasChanges := false 605 | for _, cidx := range containerIds { 606 | container, err := ccl.LookupOnlineContainer(cidx) 607 | if err != nil { 608 | return 1, err 609 | } 610 | 611 | c, err := LoadRules(container) 612 | if err != nil { 613 | return 2, err 614 | } 615 | 616 | changed := false 617 | for _, r := range c.Rules { 618 | oldRule := r.Format() 619 | 620 | // de-alias source 621 | if r.SourceAlias != "" { 622 | ipv4, _, err := ccl.ParseAddress(r.SourceAlias, container, false) 623 | if err != nil { 624 | return 3, err 625 | } 626 | 627 | if r.Source != ipv4 { 628 | changed = true 629 | r.Source = ipv4 630 | } 631 | } 632 | 633 | // de-alias destination 634 | if r.DestinationAlias != "" { 635 | ipv4, _, err := ccl.ParseAddress(r.DestinationAlias, container, false) 636 | if err != nil { 637 | return 4, err 638 | } 639 | 640 | if r.Destination != ipv4 { 641 | changed = true 642 | r.Destination = ipv4 643 | } 644 | } 645 | 646 | // create the rule that it is necessary to have 647 | rule := r.Format() 648 | 649 | // skip deleting/re-adding if rule is not any different than previous 650 | if rule != oldRule { 651 | // first, (attempt to) remove old rule 652 | if dryRun { 653 | exists, err := RuleExists(oldRule) 654 | if err != nil { 655 | return 5, err 656 | } 657 | if exists { 658 | fmt.Printf("docker-fw: iptables(%s): would delete rule '%s'\n", container.Name[1:], oldRule) 659 | hasChanges = true 660 | } 661 | } else { 662 | _ = internalDelete(oldRule, true) 663 | } 664 | } 665 | 666 | // check if new rule is already there 667 | 668 | exists, err := RuleExists(rule) 669 | if err != nil { 670 | return 6, err 671 | } 672 | if exists { 673 | //fmt.Printf("iptables(%s): rule '%s' does not exist\n", container.Name[1:], rule) 674 | 675 | // insert or append, depending on destination chain 676 | if r.Chain == DOCKER_CHAIN { 677 | if dryRun { 678 | fmt.Printf("docker-fw: iptables(%s): would append rule '%s'\n", container.Name, rule) 679 | hasChanges = true 680 | } else { 681 | err := internalAppend(container.Name, rule) 682 | if err != nil { 683 | return 5, err 684 | } 685 | } 686 | } else { 687 | if dryRun { 688 | fmt.Printf("docker-fw: iptables(%s): would insert rule '%s'\n", container.Name, rule) 689 | hasChanges = true 690 | } else { 691 | err := internalInsert(r.Position(), rule) 692 | if err != nil { 693 | return 6, err 694 | } 695 | } 696 | } 697 | } 698 | } 699 | 700 | // used for dry-run exit code, report non-zero if anything would change 701 | if changed { 702 | hasChanges = true 703 | } 704 | 705 | // if there was any change, store them again 706 | if !dryRun && changed { 707 | err := c.Save() 708 | if err != nil { 709 | return 7, err 710 | } 711 | } 712 | } 713 | 714 | if dryRun { 715 | if hasChanges { 716 | return 1, nil 717 | } 718 | } 719 | 720 | // exit with 0 since all operations were successful 721 | return 0, nil 722 | } 723 | 724 | func ListRules(containerIds []string) error { 725 | containers := []*docker.Container{} 726 | if len(containerIds) == 0 { 727 | err := ccl.LoadAllContainers() 728 | if err != nil { 729 | return err 730 | } 731 | 732 | containers = ccl.GetAllContainers() 733 | } else { 734 | for _, cid := range containerIds { 735 | container, err := ccl.LookupContainer(cid) 736 | if err != nil { 737 | return err 738 | } 739 | 740 | containers = append(containers, container) 741 | } 742 | } 743 | 744 | // loop through each container and display their ready-to-use add* actions 745 | for _, container := range containers { 746 | collection, err := LoadRules(container) 747 | if err != nil { 748 | return err 749 | } 750 | 751 | if len(collection.Rules) == 0 { 752 | continue 753 | } 754 | 755 | for _, rule := range collection.Rules { 756 | fmt.Printf("%s\n", rule.FormatAsFwCommand(container.Name[1:])) 757 | } 758 | } 759 | 760 | return nil 761 | } 762 | -------------------------------------------------------------------------------- /src/lookupCache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * docker-fw v0.2.4 - a complementary tool for Docker to manage custom 3 | * firewall rules between/towards Docker containers 4 | * Copyright (C) 2014~2016 gdm85 - https://github.com/gdm85/docker-fw/ 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | 21 | package main 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "strings" 27 | 28 | "github.com/fsouza/go-dockerclient" 29 | ) 30 | 31 | type CachedContainerLookup struct { 32 | containers map[string]*docker.Container 33 | 34 | // lookup by network address 35 | networkAddress map[string]*docker.Container 36 | 37 | // used only once to pre-fill cache with all existing containers 38 | loadedAll bool 39 | } 40 | 41 | func (ccl *CachedContainerLookup) GetAllContainers() []*docker.Container { 42 | lookupByPtr := map[*docker.Container]bool{} 43 | for _, container := range ccl.containers { 44 | // overwrite without fear, as no multiple container pointers are at any time being used 45 | // and this prevents duplicates here 46 | lookupByPtr[container] = true 47 | } 48 | 49 | // get the values 50 | containers := []*docker.Container{} 51 | for p, _ := range lookupByPtr { 52 | containers = append(containers, p) 53 | } 54 | 55 | return containers 56 | } 57 | 58 | func (ccl *CachedContainerLookup) lookupInternal(cid string, mustBeOnline bool) (*docker.Container, error) { 59 | if len(cid) == 0 { 60 | panic("empty container id passed to lookupInternal") 61 | } 62 | 63 | if container, ok := ccl.containers[cid]; !ok { 64 | if ccl.loadedAll { 65 | return nil, fmt.Errorf("container '%s' not found", cid) 66 | } 67 | 68 | err := ccl.fullRefreshContainer(cid, mustBeOnline) 69 | if err != nil { 70 | return nil, err 71 | } 72 | } else { 73 | // always perform check if container is online, also when returning a cached result 74 | if mustBeOnline { 75 | if container.NetworkSettings.IPAddress == "" { 76 | return nil, fmt.Errorf("container '%s' does not have a valid IPv4 address", container.ID) 77 | } 78 | } 79 | } 80 | 81 | return ccl.containers[cid], nil 82 | } 83 | 84 | func (ccl *CachedContainerLookup) fullRefreshContainer(id string, mustBeOnline bool) error { 85 | // pull new inspect data from API 86 | container, err := Docker.InspectContainer(id) 87 | if err != nil { 88 | return fmt.Errorf("InspectContainer('%s'): %s", id, err) 89 | } 90 | 91 | ccl.containers[container.ID] = container 92 | 93 | // add also non-standard alias or name 94 | if id != container.ID && id != container.Name { 95 | ccl.containers[id] = container 96 | } 97 | 98 | if mustBeOnline { 99 | containerIpv4 := container.NetworkSettings.IPAddress 100 | if containerIpv4 == "" { 101 | return errors.New(fmt.Sprintf("Container %s does not have a valid IPv4 address", id)) 102 | } 103 | 104 | //NOTE: status will necessarily be desynchronized from what container is doing meanwhile program runs 105 | // thus program should update 'networkAddress' lookup in case of status manipulation actions (e.g. 'start') 106 | ccl.networkAddress[containerIpv4] = container 107 | } 108 | ccl.containers[container.Name[1:]] = container 109 | 110 | return nil 111 | } 112 | 113 | func (ccl *CachedContainerLookup) RefreshContainer(cid string, mustBeOnline bool) error { 114 | // update the entry (forced, no cache applies) 115 | err := ccl.fullRefreshContainer(cid, mustBeOnline) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | // find all containers that are referenced with a different version of the id 121 | var toReMap []string 122 | for id, container := range ccl.containers { 123 | // this is a special alias e.g. shorter id or whatever else resolves to the container through API 124 | if container.ID == id { 125 | toReMap = append(toReMap, id) 126 | continue 127 | } 128 | } 129 | 130 | // remap using the most updated version that was retrieved 131 | for _, customId := range toReMap { 132 | ccl.containers[customId] = ccl.containers[ccl.containers[customId].ID] 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (ccl *CachedContainerLookup) LoadAllContainers() error { 139 | if ccl.loadedAll { 140 | return nil 141 | } 142 | 143 | containers, err := Docker.ListContainers(docker.ListContainersOptions{All: true}) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | // map all containers by their ID & name 149 | // will overwrite previous entries 150 | for _, containerSummary := range containers { 151 | err := ccl.fullRefreshContainer(containerSummary.ID, false) 152 | if err != nil { 153 | return err 154 | } 155 | } 156 | 157 | // find all containers that are referenced with a different version of the id 158 | var toReMap []string 159 | for id, container := range ccl.containers { 160 | // this is a special alias e.g. shorter id or whatever else resolves to the container through API 161 | if id != container.ID && id != container.Name[1:] { 162 | toReMap = append(toReMap, id) 163 | continue 164 | } 165 | } 166 | 167 | // remap using the most updated version that was retrieved 168 | for _, customId := range toReMap { 169 | ccl.containers[customId] = ccl.containers[ccl.containers[customId].ID] 170 | } 171 | 172 | // prevent loading any other entry for the whole program execution 173 | ccl.loadedAll = true 174 | 175 | return nil 176 | } 177 | 178 | func (ccl *CachedContainerLookup) LookupOnlineContainer(cid string) (*docker.Container, error) { 179 | return ccl.lookupInternal(cid, true) 180 | } 181 | 182 | // same as Lookup(), but does not check that container is up and running 183 | func (ccl *CachedContainerLookup) LookupContainer(cid string) (*docker.Container, error) { 184 | return ccl.lookupInternal(cid, false) 185 | } 186 | 187 | func (ccl *CachedContainerLookup) FindContainerByNetworkAddress(ipv4 string) (*docker.Container, error) { 188 | if !ccl.loadedAll { 189 | panic("Cannot lookup by network address if all entries have not been loaded") 190 | } 191 | 192 | container, ok := ccl.networkAddress[ipv4] 193 | if !ok { 194 | return nil, errors.New("address does not point to any container: " + ipv4) 195 | } 196 | 197 | return container, nil 198 | } 199 | 200 | func applySelfReduction(foundContainer *docker.Container, self *docker.Container) string { 201 | if foundContainer == self { 202 | return "." 203 | } 204 | return foundContainer.Name[1:] 205 | } 206 | 207 | // first return value is ipv4 208 | // second return value is alias (names preferred over IDs) 209 | func (ccl *CachedContainerLookup) ParseAddress(addressOrAlias string, self *docker.Container, parseContainerNames bool) (string, string, error) { 210 | switch addressOrAlias { 211 | case ".": 212 | return self.NetworkSettings.IPAddress + "/32", addressOrAlias, nil 213 | case "/": 214 | fallthrough 215 | case DOCKER_HOST: 216 | return DOCKER_HOST, "/", nil 217 | } 218 | // match an IPv4 with optional subnet 219 | res := matchIpv4.FindStringSubmatch(addressOrAlias) 220 | if len(res) != 0 { 221 | ipv4 := addressOrAlias 222 | if res[4] == "" { 223 | // add default subnet 224 | ipv4 += "/32" 225 | } 226 | 227 | // disallow specifying IPs in Docker subnet (unless specifically allowed) 228 | if isDockerIPv4(ipv4) && strings.HasSuffix(ipv4, "/32") { 229 | if !parseContainerNames { 230 | return "", "", errors.New("trying to use Docker IPv4, use an alias instead") 231 | } 232 | 233 | // load all containers - will use a cache 234 | err := ccl.LoadAllContainers() 235 | if err != nil { 236 | return "", "", err 237 | } 238 | 239 | container, err := ccl.FindContainerByNetworkAddress(ipv4[:strings.Index(ipv4, "/")]) 240 | if err != nil { 241 | return "", "", err 242 | } 243 | 244 | // return the identified container name 245 | return ipv4, applySelfReduction(container, self), nil 246 | } 247 | 248 | // an ipv4 notation address, either single IPv4 or a subnet, not from a Docker container 249 | return ipv4, "", nil 250 | } else { 251 | // not an ipv4, try to match to a container name/id 252 | container, err := ccl.LookupOnlineContainer(addressOrAlias) 253 | if err != nil { 254 | return "", "", err 255 | } 256 | 257 | // resolved container id ipv4 and id itself 258 | return container.NetworkSettings.IPAddress + "/32", applySelfReduction(container, self), nil 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/start.go: -------------------------------------------------------------------------------- 1 | /* 2 | * docker-fw v0.2.4 - a complementary tool for Docker to manage custom 3 | * firewall rules between/towards Docker containers 4 | * Copyright (C) 2014~2016 gdm85 - https://github.com/gdm85/docker-fw/ 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | 21 | package main 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "log" 27 | "strings" 28 | 29 | "github.com/fsouza/go-dockerclient" 30 | ) 31 | 32 | func arrayContains(haystack []*docker.Container, needle *docker.Container) bool { 33 | for _, b := range haystack { 34 | // we are not comparing the pointer itself because a dynamic update to stored container reference is potentially possible 35 | if b.ID == needle.ID { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | 42 | // this fix is necessary for an undocumented bug: you cannot feed back to API what you got it from regarding Links 43 | func fixHostConfig(name string, orig *docker.HostConfig) { 44 | // normalize 45 | if orig.RestartPolicy.Name == "" { 46 | orig.RestartPolicy = docker.NeverRestart() 47 | } 48 | 49 | newLinks := []string{} 50 | 51 | // now add links 52 | for _, link := range orig.Links { 53 | parts := strings.SplitN(link, ":", 2) 54 | // remove prefix from second part and leading slash from first part 55 | if parts[0][0] != '/' || parts[1][0] != '/' { 56 | // something has changed in API, likely inconsistency fixed upstream 57 | panic("unexpected format of links") 58 | } 59 | parts[0] = parts[0][1:] 60 | parts[1] = parts[1][(len(name) + 1):] 61 | newLinks = append(newLinks, fmt.Sprintf("%s:%s", parts[0], parts[1])) 62 | } 63 | 64 | // replace new links 65 | orig.Links = newLinks 66 | } 67 | 68 | func startAndSave(container *docker.Container) error { 69 | hostConfig, err := fetchSavedHostConfig(container.ID) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | if hostConfig == nil { 75 | log.Printf("WARNING: no saved HostConfig found for container '%s'", container.Name[1:]) 76 | 77 | // use the blank one 78 | hostConfig = container.HostConfig 79 | } 80 | 81 | fixHostConfig(container.Name, hostConfig) 82 | 83 | // use last known host configuration 84 | err = Docker.StartContainer(container.ID, hostConfig) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // this will also enforce container to be online 90 | err = ccl.RefreshContainer(container.ID, true) 91 | 92 | return err 93 | } 94 | 95 | func StartContainers(containerIds []string, startPaused, pullDeps, dryRun bool) (int, error) { 96 | normalizedIds, err := internalStartContainers(containerIds, startPaused, pullDeps, dryRun) 97 | if err != nil { 98 | return 127, err 99 | } 100 | 101 | // restore custom hosts modifications 102 | for _, id := range normalizedIds { 103 | err = reapplyCustomHosts(id) 104 | if err != nil { 105 | return 128, err 106 | } 107 | } 108 | 109 | // if no containers, do nothing 110 | if len(normalizedIds) == 0 { 111 | return 0, nil 112 | } 113 | 114 | // at the end, always run the 'replay' action 115 | return ReplayRules(normalizedIds, dryRun) 116 | } 117 | 118 | // 1) build a graph of container dependencies 119 | // 2) start them from lowest to highest dependency count 120 | // 3) for each container start, pause them (if asked to) 121 | // 4) when all containers have been started, run the 'replay' action for them 122 | func internalStartContainers(containerIds []string, startPaused, pullDeps, dryRun bool) ([]string, error) { 123 | // first normalize all container ids to the proper 'ID' property given through inspect 124 | // this is necessary because we won't allow to start dependant containers if not specified 125 | var containers []*docker.Container 126 | normalizedIds := []string{} 127 | for _, cid := range containerIds { 128 | container, err := ccl.LookupContainer(cid) 129 | if err != nil { 130 | return normalizedIds, err 131 | } 132 | 133 | containers = append(containers, container) 134 | normalizedIds = append(normalizedIds, container.ID) 135 | } 136 | 137 | // build the sortable graph of nodes and their link dependencies 138 | lookup := map[string]*Node{} 139 | for _, container := range containers { 140 | 141 | // prepare container node itself 142 | node, ok := lookup[container.ID] 143 | if !ok { 144 | node = NewNode(container) 145 | lookup[container.ID] = node 146 | } 147 | 148 | for _, link := range container.HostConfig.Links { 149 | parts := strings.SplitN(link, ":", 2) 150 | 151 | // identify the target container 152 | linkName := parts[0][1:] 153 | linkContainer, err := ccl.LookupContainer(linkName) 154 | if err != nil { 155 | return normalizedIds, err 156 | } 157 | 158 | // error if a container is missing from selection and no --pull-deps was specified 159 | if !pullDeps { 160 | if !arrayContains(containers, linkContainer) { 161 | return normalizedIds, errors.New(fmt.Sprintf("container '%s' is not specified in list and no --pull-deps specified", linkName)) 162 | } 163 | } 164 | 165 | linkNode, ok := lookup[linkContainer.ID] 166 | if !ok { 167 | linkNode = NewNode(linkContainer) 168 | 169 | lookup[linkContainer.ID] = linkNode 170 | } 171 | 172 | // now create association 173 | linkNode.LinkTo(node) 174 | } 175 | 176 | // now also check dependencies created by volumes 177 | for _, volumesProvider := range container.HostConfig.VolumesFrom { 178 | 179 | // identify the provider container 180 | volsContainer, err := ccl.LookupContainer(volumesProvider) 181 | if err != nil { 182 | return normalizedIds, err 183 | } 184 | 185 | // error if a container is missing from selection and no --pull-deps was specified 186 | if !pullDeps { 187 | if !arrayContains(containers, volsContainer) { 188 | return normalizedIds, errors.New(fmt.Sprintf("container '%s' (volumes provider) is not specified in list and no --pull-deps specified", volsContainer.Name[1:])) 189 | } 190 | } 191 | 192 | volsNode, ok := lookup[volsContainer.ID] 193 | if !ok { 194 | volsNode = NewNode(volsContainer) 195 | 196 | lookup[volsContainer.ID] = volsNode 197 | } 198 | 199 | // now create association 200 | volsNode.LinkTo(node) 201 | } 202 | } 203 | 204 | // convert the map to a flat array 205 | var allNodes SortableNodeArray 206 | for _, v := range lookup { 207 | allNodes = append(allNodes, v) 208 | } 209 | 210 | // apply topological sort 211 | allNodes = allNodes.TopSort() 212 | 213 | for _, node := range allNodes { 214 | // print container names as they are started, Docker-style 215 | fmt.Println(node.Name) 216 | 217 | if dryRun { 218 | continue 219 | } 220 | 221 | // always get latest version, since state might have changed 222 | container, err := ccl.LookupContainer(node.ID) 223 | if err != nil { 224 | return normalizedIds, err 225 | } 226 | 227 | changedState := false 228 | // start container 229 | if !container.State.Running { 230 | err := startAndSave(container) 231 | if err != nil { 232 | return normalizedIds, err 233 | } 234 | changedState = true 235 | 236 | //NOTE: container's paused status has not changed because of start 237 | } 238 | 239 | if startPaused && !container.State.Paused { 240 | //NOTE: container might already have been paused in command above 241 | err := Docker.PauseContainer(container.ID) 242 | if err != nil { 243 | return normalizedIds, err 244 | } 245 | changedState = true 246 | } 247 | 248 | if changedState { 249 | // always get latest version, since state might have changed 250 | // this will also enforce container to be online 251 | err = ccl.RefreshContainer(container.ID, true) 252 | if err != nil { 253 | return normalizedIds, err 254 | } 255 | } 256 | } 257 | 258 | if !dryRun { 259 | // attempt to save again network rules 260 | // NOTE: will fail if any change is detected 261 | err := BackupHostConfig(normalizedIds, true, true) 262 | if err != nil { 263 | return normalizedIds, err 264 | } 265 | } 266 | 267 | return normalizedIds, nil 268 | } 269 | -------------------------------------------------------------------------------- /src/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | * docker-fw v0.2.4 - a complementary tool for Docker to manage custom 3 | * firewall rules between/towards Docker containers 4 | * Copyright (C) 2014~2016 gdm85 - https://github.com/gdm85/docker-fw/ 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | 21 | package main 22 | 23 | import ( 24 | "encoding/json" 25 | "errors" 26 | "fmt" 27 | "io/ioutil" 28 | "log" 29 | "os" 30 | "strings" 31 | 32 | "github.com/fsouza/go-dockerclient" 33 | ) 34 | 35 | func getBackupHostConfigFileName(cid string) string { 36 | return fmt.Sprintf("/var/lib/docker/containers/%s/backupHostConfig.json", cid) 37 | } 38 | 39 | //NOTE: container must be running in order for this to be working 40 | func BackupHostConfig(containerIds []string, mergeNetworkSettings, failOnChange bool) error { 41 | for _, userCid := range containerIds { 42 | container, err := ccl.LookupOnlineContainer(userCid) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if !container.State.Running { 48 | return errors.New(fmt.Sprintf("Container %s is not running", container.Name[1:])) 49 | } 50 | 51 | // validate that nothing relevant has changed 52 | if failOnChange { 53 | origHostConfig, err := fetchSavedHostConfig(container.ID) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if origHostConfig != nil { 59 | // normalize 60 | if origHostConfig.RestartPolicy.Name == "" { 61 | origHostConfig.RestartPolicy = docker.NeverRestart() 62 | } 63 | 64 | // proceed to validate that nothing relevant changed since last execution time 65 | //NOTE: this might easily be an insufficient test when new options are added upstream 66 | if !asGoodAs(origHostConfig, container.HostConfig) { 67 | return errors.New(fmt.Sprintf("Container %s has inconsistently changed host configuration", container.Name[1:])) 68 | } 69 | } 70 | } 71 | 72 | err = backupHostConfig(container, mergeNetworkSettings) 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func backupHostConfig(container *docker.Container, mergeNetworkSettings bool) error { 82 | var origPortBindings map[docker.Port][]docker.PortBinding 83 | if mergeNetworkSettings { 84 | origPortBindings = container.HostConfig.PortBindings 85 | 86 | container.HostConfig.PortBindings = container.NetworkSettings.Ports 87 | } 88 | 89 | bytes, err := json.Marshal(container.HostConfig) 90 | if mergeNetworkSettings { 91 | container.HostConfig.PortBindings = origPortBindings 92 | } 93 | if err != nil { 94 | return err 95 | } 96 | err = ioutil.WriteFile(getBackupHostConfigFileName(container.ID), bytes, 0666) 97 | return err 98 | } 99 | 100 | func fetchSavedHostConfigAsBytes(id string) ([]byte, error) { 101 | fileName := getBackupHostConfigFileName(id) 102 | 103 | _, err := os.Stat(fileName) 104 | if err != nil { 105 | if !os.IsNotExist(err) { 106 | return nil, err 107 | } 108 | 109 | // nothing found, and no error either 110 | return nil, nil 111 | } 112 | 113 | // read only when existing 114 | bytes, err := ioutil.ReadFile(fileName) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | return bytes, nil 120 | } 121 | 122 | func fetchSavedHostConfig(id string) (*docker.HostConfig, error) { 123 | hostConfig := docker.HostConfig{} 124 | bytes, err := fetchSavedHostConfigAsBytes(id) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | // nothing found 130 | if bytes == nil { 131 | return nil, nil 132 | } 133 | 134 | err = json.Unmarshal(bytes, &hostConfig) 135 | if err != nil { 136 | log.Printf("Could not unmarshal host config '%s'", string(bytes)) 137 | return nil, err 138 | } 139 | 140 | return &hostConfig, nil 141 | } 142 | 143 | // read existing rules (if any) 144 | func LoadRules(container *docker.Container) (*IptablesRulesCollection, error) { 145 | c := IptablesRulesCollection{cid: container.ID} 146 | 147 | _, err := os.Stat(c.fileName()) 148 | if err != nil { 149 | if !os.IsNotExist(err) { 150 | return nil, err 151 | } 152 | 153 | // file does not exist, no problem, allow fallback to 'return nil' 154 | 155 | } else { 156 | // read only when existing 157 | bytes, err := ioutil.ReadFile(c.fileName()) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | err = json.Unmarshal(bytes, &c) 163 | if err != nil { 164 | log.Printf("Could not unmarshal iptables rules '%s'", string(bytes)) 165 | return nil, err 166 | } 167 | } 168 | 169 | return &c, nil 170 | } 171 | 172 | func getCustomHostsFileName(c *docker.Container) string { 173 | return fmt.Sprintf("/var/lib/docker/containers/%s/customHosts.json", c.ID) 174 | } 175 | 176 | func LoadCustomHosts(container *docker.Container) ([]string, error) { 177 | _, err := os.Stat(getCustomHostsFileName(container)) 178 | if err != nil { 179 | if !os.IsNotExist(err) { 180 | return nil, err 181 | } 182 | 183 | // file does not exist, no problem 184 | return []string{}, nil 185 | } 186 | // read only when existing 187 | bytes, err := ioutil.ReadFile(getCustomHostsFileName(container)) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | ch := []string{} 193 | err = json.Unmarshal(bytes, &ch) 194 | if err != nil { 195 | log.Printf("Could not unmarshal custom hosts '%s'", string(bytes)) 196 | return nil, err 197 | } 198 | return ch, nil 199 | } 200 | 201 | func saveCustomHosts(c *docker.Container, ch []string) error { 202 | bytes, err := json.Marshal(&ch) 203 | if err != nil { 204 | return err 205 | } 206 | err = ioutil.WriteFile(getCustomHostsFileName(c), bytes, 0666) 207 | return err 208 | } 209 | 210 | func inArray(a []string, needle string) bool { 211 | for _, e := range a { 212 | if e == needle { 213 | return true 214 | } 215 | } 216 | return false 217 | } 218 | 219 | func updateCustomHosts(target, b string) error { 220 | container, err := ccl.LookupOnlineContainer(target) 221 | if err != nil { 222 | return err 223 | } 224 | ch, err := LoadCustomHosts(container) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | bContainer, err := ccl.LookupOnlineContainer(b) 230 | if err != nil { 231 | return err 232 | } 233 | 234 | found := inArray(ch, bContainer.Name[1:]) 235 | 236 | if !found { 237 | ch = append(ch, bContainer.Name[1:]) 238 | } 239 | 240 | // in any case, update the live hosts file of target 241 | err = updateHosts(container, ch) 242 | if err != nil { 243 | return err 244 | } 245 | 246 | // save only if it was not already there 247 | if !found { 248 | err = saveCustomHosts(container, ch) 249 | if err != nil { 250 | return err 251 | } 252 | } 253 | 254 | return nil 255 | } 256 | 257 | func reapplyCustomHosts(target string) error { 258 | container, err := ccl.LookupOnlineContainer(target) 259 | if err != nil { 260 | return err 261 | } 262 | ch, err := LoadCustomHosts(container) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | if len(ch) == 0 { 268 | return nil 269 | } 270 | 271 | // update the live hosts file of target 272 | err = updateHosts(container, ch) 273 | if err != nil { 274 | return err 275 | } 276 | 277 | return nil 278 | } 279 | 280 | func restorePaused(c *docker.Container, paused bool, origErr error) error { 281 | if paused { 282 | err := Docker.PauseContainer(c.ID) 283 | if err != nil { 284 | return fmt.Errorf("%s\nadditionally, an error while re-pausing container: %s", origErr, err) 285 | } 286 | } 287 | return origErr 288 | } 289 | 290 | func updateHosts(c *docker.Container, ch []string) error { 291 | // In order to exec successfully within the container, we must unpause it if it were paused 292 | // although nsenter has not such limitation, for some reason it is enforced by Docker, 293 | // thus here docker-fw complies by first unpausing the container and then re-pausing it. 294 | // The net result is that - whatsoever your experiments tell you - you should never relay on two-ways containers 295 | // being reachable during any of your initialization in CMD/ENTRYPOINT commands. 296 | // This could be better handled by directly modifying the hosts file even before container is started, 297 | // but it would be using undocumented features. 298 | wasPaused := false 299 | if c.State.Paused { 300 | err := Docker.UnpauseContainer(c.ID) 301 | if err != nil { 302 | return err 303 | } 304 | wasPaused = true 305 | } 306 | 307 | result, err := containerExec(c.ID, []string{"cat", "/etc/hosts"}) 308 | if err != nil { 309 | return restorePaused(c, wasPaused, err) 310 | } 311 | 312 | if result.ExitCode != 0 { 313 | err := errors.New(fmt.Sprintf("Could not read /etc/hosts in container '%s': %s", c.Name[1:], result.Stderr)) 314 | return restorePaused(c, wasPaused, err) 315 | } 316 | 317 | // read existing hosts 318 | hasHostsChanges := false 319 | rewrittenLines := []string{} 320 | okContainers := []string{} 321 | for _, line := range strings.Split(result.Stdout, "\n") { 322 | line = strings.TrimSpace(line) 323 | 324 | if len(line) == 0 || line[0] == '#' { 325 | rewrittenLines = append(rewrittenLines, line) 326 | continue 327 | } 328 | 329 | // get all fields, although most of them will have only 2 330 | fields := strings.Fields(line) 331 | 332 | // scan for matches with specified custom hosts 333 | removeFields := []string{} 334 | for _, cid := range ch { 335 | container, err := ccl.LookupOnlineContainer(cid) 336 | if err != nil { 337 | return restorePaused(c, wasPaused, err) 338 | } 339 | for _, field := range fields[1:] { 340 | if field == container.Name[1:] { 341 | if fields[0] != container.NetworkSettings.IPAddress { 342 | // needs an update, IPv4 changed 343 | removeFields = append(removeFields, field) 344 | break 345 | } else { 346 | // if a container is not in this array it will always trigger addition of a new /etc/hosts line 347 | if !inArray(okContainers, field) { 348 | okContainers = append(okContainers, field) 349 | } 350 | } 351 | } 352 | } 353 | } 354 | 355 | if len(removeFields) > 0 { 356 | // filter out all fields to be removed 357 | newFields := []string{fields[0]} 358 | for _, field := range fields[1:] { 359 | if !inArray(removeFields, field) { 360 | newFields = append(newFields, field) 361 | } 362 | } 363 | 364 | // if there is only 1 field (IP), discard, otherwise add the new line 365 | // containers without a match will be added later anyway 366 | if len(newFields) == 1 { 367 | continue 368 | } 369 | 370 | fmt.Printf("docker-fw: add-two-ways: updated hosts line for (%s) in container '%s'\n", strings.Join(removeFields, ", "), c.Name[1:]) 371 | rewrittenLines = append(rewrittenLines, strings.Join(newFields, "\t")) 372 | hasHostsChanges = true 373 | } else { 374 | // preserve original line, since no change happened 375 | rewrittenLines = append(rewrittenLines, line) 376 | } 377 | } 378 | 379 | // add new hosts lines 380 | for _, host := range ch { 381 | container, err := ccl.LookupOnlineContainer(host) 382 | if err != nil { 383 | return restorePaused(c, wasPaused, err) 384 | } 385 | if !inArray(okContainers, container.Name[1:]) { 386 | rewrittenLines = append(rewrittenLines, fmt.Sprintf("%s\t%s", container.NetworkSettings.IPAddress, container.Name[1:])) 387 | fmt.Printf("docker-fw: add-two-ways: added hosts line for '%s' in container '%s'\n", container.Name[1:], c.Name[1:]) 388 | hasHostsChanges = true 389 | } 390 | } 391 | 392 | // write new hosts file (as needed) 393 | if hasHostsChanges { 394 | err := containerInject(c.ID, "/etc/hosts", strings.Join(rewrittenLines, "\n")+"\n") 395 | if err != nil { 396 | return restorePaused(c, wasPaused, err) 397 | } 398 | } 399 | 400 | return restorePaused(c, wasPaused, nil) 401 | } 402 | --------------------------------------------------------------------------------