├── .github └── workflows │ └── go.yml ├── .gitignore ├── DCO ├── LICENSE ├── NOTICE ├── README.md ├── build ├── code-of-conduct.md ├── go.mod ├── iptables ├── iptables.go ├── iptables_test.go └── lock.go └── test /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # Maintained in https://github.com/coreos/repo-templates 2 | # Do not edit downstream. 3 | 4 | name: Go 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | permissions: 11 | contents: read 12 | 13 | # don't waste job slots on superseded code 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | test: 20 | name: Test 21 | strategy: 22 | matrix: 23 | go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Set up Go 1.x 27 | uses: actions/setup-go@v4 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | - name: Check out repository 31 | uses: actions/checkout@v3 32 | - name: Install Go dependencies 33 | run: go get golang.org/x/tools/cmd/cover 34 | - name: Check modules 35 | run: go mod verify 36 | - name: Build 37 | run: ./build 38 | - name: Test 39 | run: SUDO_PERMITTED=1 ./test 40 | - name: Run linter 41 | uses: golangci/golangci-lint-action@v3 42 | with: 43 | version: v1.61.0 44 | args: -E=gofmt --timeout=30m0s 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | CoreOS Project 2 | Copyright 2018 CoreOS, Inc 3 | 4 | This product includes software developed at CoreOS, Inc. 5 | (http://www.coreos.com/). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-iptables 2 | 3 | [![GoDoc](https://godoc.org/github.com/coreos/go-iptables/iptables?status.svg)](https://godoc.org/github.com/coreos/go-iptables/iptables) 4 | [![Build status](https://github.com/coreos/go-iptables/actions/workflows/go.yml/badge.svg)](https://github.com/coreos/go-iptables/actions/workflows/go.yml) 5 | 6 | Go bindings for iptables utility. 7 | 8 | In-kernel netfilter does not have a good userspace API. The tables are manipulated via setsockopt that sets/replaces the entire table. Changes to existing table need to be resolved by userspace code which is difficult and error-prone. Netfilter developers heavily advocate using iptables utlity for programmatic manipulation. 9 | 10 | go-iptables wraps invocation of iptables utility with functions to append and delete rules; create, clear and delete chains. 11 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go build ./... 4 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | ## CoreOS Community Code of Conduct 2 | 3 | ### Contributor Code of Conduct 4 | 5 | As contributors and maintainers of this project, and in the interest of 6 | fostering an open and welcoming community, we pledge to respect all people who 7 | contribute through reporting issues, posting feature requests, updating 8 | documentation, submitting pull requests or patches, and other activities. 9 | 10 | We are committed to making participation in this project a harassment-free 11 | experience for everyone, regardless of level of experience, gender, gender 12 | identity and expression, sexual orientation, disability, personal appearance, 13 | body size, race, ethnicity, age, religion, or nationality. 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery 18 | * Personal attacks 19 | * Trolling or insulting/derogatory comments 20 | * Public or private harassment 21 | * Publishing others' private information, such as physical or electronic addresses, without explicit permission 22 | * Other unethical or unprofessional conduct. 23 | 24 | Project maintainers have the right and responsibility to remove, edit, or 25 | reject comments, commits, code, wiki edits, issues, and other contributions 26 | that are not aligned to this Code of Conduct. By adopting this Code of Conduct, 27 | project maintainers commit themselves to fairly and consistently applying these 28 | principles to every aspect of managing this project. Project maintainers who do 29 | not follow or enforce the Code of Conduct may be permanently removed from the 30 | project team. 31 | 32 | This code of conduct applies both within project spaces and in public spaces 33 | when an individual is representing the project or its community. 34 | 35 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 36 | reported by contacting a project maintainer, Brandon Philips 37 | , and/or Rithu John . 38 | 39 | This Code of Conduct is adapted from the Contributor Covenant 40 | (http://contributor-covenant.org), version 1.2.0, available at 41 | http://contributor-covenant.org/version/1/2/0/ 42 | 43 | ### CoreOS Events Code of Conduct 44 | 45 | CoreOS events are working conferences intended for professional networking and 46 | collaboration in the CoreOS community. Attendees are expected to behave 47 | according to professional standards and in accordance with their employer’s 48 | policies on appropriate workplace behavior. 49 | 50 | While at CoreOS events or related social networking opportunities, attendees 51 | should not engage in discriminatory or offensive speech or actions including 52 | but not limited to gender, sexuality, race, age, disability, or religion. 53 | Speakers should be especially aware of these concerns. 54 | 55 | CoreOS does not condone any statements by speakers contrary to these standards. 56 | CoreOS reserves the right to deny entrance and/or eject from an event (without 57 | refund) any individual found to be engaging in discriminatory or offensive 58 | speech or actions. 59 | 60 | Please bring any concerns to the immediate attention of designated on-site 61 | staff, Brandon Philips , and/or Rithu John . 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coreos/go-iptables 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /iptables/iptables.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 CoreOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package iptables 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "net" 22 | "os/exec" 23 | "regexp" 24 | "strconv" 25 | "strings" 26 | "syscall" 27 | ) 28 | 29 | // Adds the output of stderr to exec.ExitError 30 | type Error struct { 31 | exec.ExitError 32 | cmd exec.Cmd 33 | msg string 34 | exitStatus *int //for overriding 35 | } 36 | 37 | func (e *Error) ExitStatus() int { 38 | if e.exitStatus != nil { 39 | return *e.exitStatus 40 | } 41 | return e.Sys().(syscall.WaitStatus).ExitStatus() 42 | } 43 | 44 | func (e *Error) Error() string { 45 | return fmt.Sprintf("running %v: exit status %v: %v", e.cmd.Args, e.ExitStatus(), e.msg) 46 | } 47 | 48 | var isNotExistPatterns = []string{ 49 | "Bad rule (does a matching rule exist in that chain?).\n", 50 | "No chain/target/match by that name.\n", 51 | "No such file or directory", 52 | "does not exist", 53 | } 54 | 55 | // IsNotExist returns true if the error is due to the chain or rule not existing 56 | func (e *Error) IsNotExist() bool { 57 | for _, str := range isNotExistPatterns { 58 | if strings.Contains(e.msg, str) { 59 | return true 60 | } 61 | } 62 | return false 63 | } 64 | 65 | // Protocol to differentiate between IPv4 and IPv6 66 | type Protocol byte 67 | 68 | const ( 69 | ProtocolIPv4 Protocol = iota 70 | ProtocolIPv6 71 | ) 72 | 73 | type IPTables struct { 74 | path string 75 | proto Protocol 76 | hasCheck bool 77 | hasWait bool 78 | waitSupportSecond bool 79 | hasRandomFully bool 80 | v1 int 81 | v2 int 82 | v3 int 83 | mode string // the underlying iptables operating mode, e.g. nf_tables 84 | timeout int // time to wait for the iptables lock, default waits forever 85 | } 86 | 87 | // Stat represents a structured statistic entry. 88 | type Stat struct { 89 | Packets uint64 `json:"pkts"` 90 | Bytes uint64 `json:"bytes"` 91 | Target string `json:"target"` 92 | Protocol string `json:"prot"` 93 | Opt string `json:"opt"` 94 | Input string `json:"in"` 95 | Output string `json:"out"` 96 | Source *net.IPNet `json:"source"` 97 | Destination *net.IPNet `json:"destination"` 98 | Options string `json:"options"` 99 | } 100 | 101 | type option func(*IPTables) 102 | 103 | func IPFamily(proto Protocol) option { 104 | return func(ipt *IPTables) { 105 | ipt.proto = proto 106 | } 107 | } 108 | 109 | func Timeout(timeout int) option { 110 | return func(ipt *IPTables) { 111 | ipt.timeout = timeout 112 | } 113 | } 114 | 115 | func Path(path string) option { 116 | return func(ipt *IPTables) { 117 | ipt.path = path 118 | } 119 | } 120 | 121 | // New creates a new IPTables configured with the options passed as parameters. 122 | // Supported parameters are: 123 | // 124 | // IPFamily(Protocol) 125 | // Timeout(int) 126 | // Path(string) 127 | // 128 | // For backwards compatibility, by default New uses IPv4 and timeout 0. 129 | // i.e. you can create an IPv6 IPTables using a timeout of 5 seconds passing 130 | // the IPFamily and Timeout options as follow: 131 | // 132 | // ip6t := New(IPFamily(ProtocolIPv6), Timeout(5)) 133 | func New(opts ...option) (*IPTables, error) { 134 | 135 | ipt := &IPTables{ 136 | proto: ProtocolIPv4, 137 | timeout: 0, 138 | path: "", 139 | } 140 | 141 | for _, opt := range opts { 142 | opt(ipt) 143 | } 144 | 145 | // if path wasn't preset through New(Path()), autodiscover it 146 | cmd := "" 147 | if ipt.path == "" { 148 | cmd = getIptablesCommand(ipt.proto) 149 | } else { 150 | cmd = ipt.path 151 | } 152 | path, err := exec.LookPath(cmd) 153 | if err != nil { 154 | return nil, err 155 | } 156 | ipt.path = path 157 | 158 | vstring, err := getIptablesVersionString(path) 159 | if err != nil { 160 | return nil, fmt.Errorf("could not get iptables version: %v", err) 161 | } 162 | v1, v2, v3, mode, err := extractIptablesVersion(vstring) 163 | if err != nil { 164 | return nil, fmt.Errorf("failed to extract iptables version from [%s]: %v", vstring, err) 165 | } 166 | ipt.v1 = v1 167 | ipt.v2 = v2 168 | ipt.v3 = v3 169 | ipt.mode = mode 170 | 171 | checkPresent, waitPresent, waitSupportSecond, randomFullyPresent := getIptablesCommandSupport(v1, v2, v3) 172 | ipt.hasCheck = checkPresent 173 | ipt.hasWait = waitPresent 174 | ipt.waitSupportSecond = waitSupportSecond 175 | ipt.hasRandomFully = randomFullyPresent 176 | 177 | return ipt, nil 178 | } 179 | 180 | // New creates a new IPTables for the given proto. 181 | // The proto will determine which command is used, either "iptables" or "ip6tables". 182 | func NewWithProtocol(proto Protocol) (*IPTables, error) { 183 | return New(IPFamily(proto), Timeout(0)) 184 | } 185 | 186 | // Proto returns the protocol used by this IPTables. 187 | func (ipt *IPTables) Proto() Protocol { 188 | return ipt.proto 189 | } 190 | 191 | // Exists checks if given rulespec in specified table/chain exists 192 | func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) { 193 | if !ipt.hasCheck { 194 | return ipt.existsForOldIptables(table, chain, rulespec) 195 | 196 | } 197 | cmd := append([]string{"-t", table, "-C", chain}, rulespec...) 198 | err := ipt.run(cmd...) 199 | eerr, eok := err.(*Error) 200 | switch { 201 | case err == nil: 202 | return true, nil 203 | case eok && eerr.ExitStatus() == 1: 204 | return false, nil 205 | default: 206 | return false, err 207 | } 208 | } 209 | 210 | // Insert inserts rulespec to specified table/chain (in specified pos) 211 | func (ipt *IPTables) Insert(table, chain string, pos int, rulespec ...string) error { 212 | cmd := append([]string{"-t", table, "-I", chain, strconv.Itoa(pos)}, rulespec...) 213 | return ipt.run(cmd...) 214 | } 215 | 216 | // Replace replaces rulespec to specified table/chain (in specified pos) 217 | func (ipt *IPTables) Replace(table, chain string, pos int, rulespec ...string) error { 218 | cmd := append([]string{"-t", table, "-R", chain, strconv.Itoa(pos)}, rulespec...) 219 | return ipt.run(cmd...) 220 | } 221 | 222 | // InsertUnique acts like Insert except that it won't insert a duplicate (no matter the position in the chain) 223 | func (ipt *IPTables) InsertUnique(table, chain string, pos int, rulespec ...string) error { 224 | exists, err := ipt.Exists(table, chain, rulespec...) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | if !exists { 230 | return ipt.Insert(table, chain, pos, rulespec...) 231 | } 232 | 233 | return nil 234 | } 235 | 236 | // Append appends rulespec to specified table/chain 237 | func (ipt *IPTables) Append(table, chain string, rulespec ...string) error { 238 | cmd := append([]string{"-t", table, "-A", chain}, rulespec...) 239 | return ipt.run(cmd...) 240 | } 241 | 242 | // AppendUnique acts like Append except that it won't add a duplicate 243 | func (ipt *IPTables) AppendUnique(table, chain string, rulespec ...string) error { 244 | exists, err := ipt.Exists(table, chain, rulespec...) 245 | if err != nil { 246 | return err 247 | } 248 | 249 | if !exists { 250 | return ipt.Append(table, chain, rulespec...) 251 | } 252 | 253 | return nil 254 | } 255 | 256 | // Delete removes rulespec in specified table/chain 257 | func (ipt *IPTables) Delete(table, chain string, rulespec ...string) error { 258 | cmd := append([]string{"-t", table, "-D", chain}, rulespec...) 259 | return ipt.run(cmd...) 260 | } 261 | 262 | func (ipt *IPTables) DeleteIfExists(table, chain string, rulespec ...string) error { 263 | exists, err := ipt.Exists(table, chain, rulespec...) 264 | if err == nil && exists { 265 | err = ipt.Delete(table, chain, rulespec...) 266 | } 267 | return err 268 | } 269 | 270 | // DeleteById deletes the rule with the specified ID in the given table and chain. 271 | func (ipt *IPTables) DeleteById(table, chain string, id int) error { 272 | cmd := []string{"-t", table, "-D", chain, strconv.Itoa(id)} 273 | return ipt.run(cmd...) 274 | } 275 | 276 | // List rules in specified table/chain 277 | func (ipt *IPTables) ListById(table, chain string, id int) (string, error) { 278 | args := []string{"-t", table, "-S", chain, strconv.Itoa(id)} 279 | rule, err := ipt.executeList(args) 280 | if err != nil { 281 | return "", err 282 | } 283 | return rule[0], nil 284 | } 285 | 286 | // List rules in specified table/chain 287 | func (ipt *IPTables) List(table, chain string) ([]string, error) { 288 | args := []string{"-t", table, "-S", chain} 289 | return ipt.executeList(args) 290 | } 291 | 292 | // List rules (with counters) in specified table/chain 293 | func (ipt *IPTables) ListWithCounters(table, chain string) ([]string, error) { 294 | args := []string{"-t", table, "-v", "-S", chain} 295 | return ipt.executeList(args) 296 | } 297 | 298 | // ListChains returns a slice containing the name of each chain in the specified table. 299 | func (ipt *IPTables) ListChains(table string) ([]string, error) { 300 | args := []string{"-t", table, "-S"} 301 | 302 | result, err := ipt.executeList(args) 303 | if err != nil { 304 | return nil, err 305 | } 306 | 307 | // Iterate over rules to find all default (-P) and user-specified (-N) chains. 308 | // Chains definition always come before rules. 309 | // Format is the following: 310 | // -P OUTPUT ACCEPT 311 | // -N Custom 312 | var chains []string 313 | for _, val := range result { 314 | if strings.HasPrefix(val, "-P") || strings.HasPrefix(val, "-N") { 315 | chains = append(chains, strings.Fields(val)[1]) 316 | } else { 317 | break 318 | } 319 | } 320 | return chains, nil 321 | } 322 | 323 | // '-S' is fine with non existing rule index as long as the chain exists 324 | // therefore pass index 1 to reduce overhead for large chains 325 | func (ipt *IPTables) ChainExists(table, chain string) (bool, error) { 326 | err := ipt.run("-t", table, "-S", chain, "1") 327 | eerr, eok := err.(*Error) 328 | switch { 329 | case err == nil: 330 | return true, nil 331 | case eok && eerr.ExitStatus() == 1: 332 | return false, nil 333 | default: 334 | return false, err 335 | } 336 | } 337 | 338 | // Stats lists rules including the byte and packet counts 339 | func (ipt *IPTables) Stats(table, chain string) ([][]string, error) { 340 | args := []string{"-t", table, "-L", chain, "-n", "-v", "-x"} 341 | lines, err := ipt.executeList(args) 342 | if err != nil { 343 | return nil, err 344 | } 345 | 346 | appendSubnet := func(addr string) string { 347 | if strings.IndexByte(addr, byte('/')) < 0 { 348 | if strings.IndexByte(addr, '.') < 0 { 349 | return addr + "/128" 350 | } 351 | return addr + "/32" 352 | } 353 | return addr 354 | } 355 | 356 | ipv6 := ipt.proto == ProtocolIPv6 357 | 358 | // Skip the warning if exist 359 | if strings.HasPrefix(lines[0], "#") { 360 | lines = lines[1:] 361 | } 362 | 363 | rows := [][]string{} 364 | for i, line := range lines { 365 | // Skip over chain name and field header 366 | if i < 2 { 367 | continue 368 | } 369 | 370 | // Fields: 371 | // 0=pkts 1=bytes 2=target 3=prot 4=opt 5=in 6=out 7=source 8=destination 9=options 372 | line = strings.TrimSpace(line) 373 | fields := strings.Fields(line) 374 | 375 | // The ip6tables verbose output cannot be naively split due to the default "opt" 376 | // field containing 2 single spaces. 377 | if ipv6 { 378 | // Check if field 6 is "opt" or "source" address 379 | dest := fields[6] 380 | ip, _, _ := net.ParseCIDR(dest) 381 | if ip == nil { 382 | ip = net.ParseIP(dest) 383 | } 384 | 385 | // If we detected a CIDR or IP, the "opt" field is empty.. insert it. 386 | if ip != nil { 387 | f := []string{} 388 | f = append(f, fields[:4]...) 389 | f = append(f, " ") // Empty "opt" field for ip6tables 390 | f = append(f, fields[4:]...) 391 | fields = f 392 | } 393 | } 394 | 395 | // Adjust "source" and "destination" to include netmask, to match regular 396 | // List output 397 | fields[7] = appendSubnet(fields[7]) 398 | fields[8] = appendSubnet(fields[8]) 399 | 400 | // Combine "options" fields 9... into a single space-delimited field. 401 | options := fields[9:] 402 | fields = fields[:9] 403 | fields = append(fields, strings.Join(options, " ")) 404 | rows = append(rows, fields) 405 | } 406 | return rows, nil 407 | } 408 | 409 | // ParseStat parses a single statistic row into a Stat struct. The input should 410 | // be a string slice that is returned from calling the Stat method. 411 | func (ipt *IPTables) ParseStat(stat []string) (parsed Stat, err error) { 412 | // For forward-compatibility, expect at least 10 fields in the stat 413 | if len(stat) < 10 { 414 | return parsed, fmt.Errorf("stat contained fewer fields than expected") 415 | } 416 | 417 | // Convert the fields that are not plain strings 418 | parsed.Packets, err = strconv.ParseUint(stat[0], 0, 64) 419 | if err != nil { 420 | return parsed, fmt.Errorf(err.Error(), "could not parse packets") 421 | } 422 | parsed.Bytes, err = strconv.ParseUint(stat[1], 0, 64) 423 | if err != nil { 424 | return parsed, fmt.Errorf(err.Error(), "could not parse bytes") 425 | } 426 | _, parsed.Source, err = net.ParseCIDR(stat[7]) 427 | if err != nil { 428 | return parsed, fmt.Errorf(err.Error(), "could not parse source") 429 | } 430 | _, parsed.Destination, err = net.ParseCIDR(stat[8]) 431 | if err != nil { 432 | return parsed, fmt.Errorf(err.Error(), "could not parse destination") 433 | } 434 | 435 | // Put the fields that are strings 436 | parsed.Target = stat[2] 437 | parsed.Protocol = stat[3] 438 | parsed.Opt = stat[4] 439 | parsed.Input = stat[5] 440 | parsed.Output = stat[6] 441 | parsed.Options = stat[9] 442 | 443 | return parsed, nil 444 | } 445 | 446 | // StructuredStats returns statistics as structured data which may be further 447 | // parsed and marshaled. 448 | func (ipt *IPTables) StructuredStats(table, chain string) ([]Stat, error) { 449 | rawStats, err := ipt.Stats(table, chain) 450 | if err != nil { 451 | return nil, err 452 | } 453 | 454 | structStats := []Stat{} 455 | for _, rawStat := range rawStats { 456 | stat, err := ipt.ParseStat(rawStat) 457 | if err != nil { 458 | return nil, err 459 | } 460 | structStats = append(structStats, stat) 461 | } 462 | 463 | return structStats, nil 464 | } 465 | 466 | func (ipt *IPTables) executeList(args []string) ([]string, error) { 467 | var stdout bytes.Buffer 468 | if err := ipt.runWithOutput(args, &stdout); err != nil { 469 | return nil, err 470 | } 471 | 472 | rules := strings.Split(stdout.String(), "\n") 473 | 474 | // strip trailing newline 475 | if len(rules) > 0 && rules[len(rules)-1] == "" { 476 | rules = rules[:len(rules)-1] 477 | } 478 | 479 | for i, rule := range rules { 480 | rules[i] = filterRuleOutput(rule) 481 | } 482 | 483 | return rules, nil 484 | } 485 | 486 | // NewChain creates a new chain in the specified table. 487 | // If the chain already exists, it will result in an error. 488 | func (ipt *IPTables) NewChain(table, chain string) error { 489 | return ipt.run("-t", table, "-N", chain) 490 | } 491 | 492 | const existsErr = 1 493 | 494 | // ClearChain flushed (deletes all rules) in the specified table/chain. 495 | // If the chain does not exist, a new one will be created 496 | func (ipt *IPTables) ClearChain(table, chain string) error { 497 | err := ipt.NewChain(table, chain) 498 | 499 | eerr, eok := err.(*Error) 500 | switch { 501 | case err == nil: 502 | return nil 503 | case eok && eerr.ExitStatus() == existsErr: 504 | // chain already exists. Flush (clear) it. 505 | return ipt.run("-t", table, "-F", chain) 506 | default: 507 | return err 508 | } 509 | } 510 | 511 | // RenameChain renames the old chain to the new one. 512 | func (ipt *IPTables) RenameChain(table, oldChain, newChain string) error { 513 | return ipt.run("-t", table, "-E", oldChain, newChain) 514 | } 515 | 516 | // DeleteChain deletes the chain in the specified table. 517 | // The chain must be empty 518 | func (ipt *IPTables) DeleteChain(table, chain string) error { 519 | return ipt.run("-t", table, "-X", chain) 520 | } 521 | 522 | func (ipt *IPTables) ClearAndDeleteChain(table, chain string) error { 523 | exists, err := ipt.ChainExists(table, chain) 524 | if err != nil || !exists { 525 | return err 526 | } 527 | err = ipt.run("-t", table, "-F", chain) 528 | if err == nil { 529 | err = ipt.run("-t", table, "-X", chain) 530 | } 531 | return err 532 | } 533 | 534 | func (ipt *IPTables) ClearAll() error { 535 | return ipt.run("-F") 536 | } 537 | 538 | func (ipt *IPTables) DeleteAll() error { 539 | return ipt.run("-X") 540 | } 541 | 542 | // ChangePolicy changes policy on chain to target 543 | func (ipt *IPTables) ChangePolicy(table, chain, target string) error { 544 | return ipt.run("-t", table, "-P", chain, target) 545 | } 546 | 547 | // Check if the underlying iptables command supports the --random-fully flag 548 | func (ipt *IPTables) HasRandomFully() bool { 549 | return ipt.hasRandomFully 550 | } 551 | 552 | // Return version components of the underlying iptables command 553 | func (ipt *IPTables) GetIptablesVersion() (int, int, int) { 554 | return ipt.v1, ipt.v2, ipt.v3 555 | } 556 | 557 | // run runs an iptables command with the given arguments, ignoring 558 | // any stdout output 559 | func (ipt *IPTables) run(args ...string) error { 560 | return ipt.runWithOutput(args, nil) 561 | } 562 | 563 | // runWithOutput runs an iptables command with the given arguments, 564 | // writing any stdout output to the given writer 565 | func (ipt *IPTables) runWithOutput(args []string, stdout io.Writer) error { 566 | args = append([]string{ipt.path}, args...) 567 | if ipt.hasWait { 568 | args = append(args, "--wait") 569 | if ipt.timeout != 0 && ipt.waitSupportSecond { 570 | args = append(args, strconv.Itoa(ipt.timeout)) 571 | } 572 | } else { 573 | fmu, err := newXtablesFileLock() 574 | if err != nil { 575 | return err 576 | } 577 | ul, err := fmu.tryLock() 578 | if err != nil { 579 | syscall.Close(fmu.fd) 580 | return err 581 | } 582 | defer func() { 583 | _ = ul.Unlock() 584 | }() 585 | } 586 | 587 | var stderr bytes.Buffer 588 | cmd := exec.Cmd{ 589 | Path: ipt.path, 590 | Args: args, 591 | Stdout: stdout, 592 | Stderr: &stderr, 593 | } 594 | 595 | if err := cmd.Run(); err != nil { 596 | switch e := err.(type) { 597 | case *exec.ExitError: 598 | return &Error{*e, cmd, stderr.String(), nil} 599 | default: 600 | return err 601 | } 602 | } 603 | 604 | return nil 605 | } 606 | 607 | // getIptablesCommand returns the correct command for the given protocol, either "iptables" or "ip6tables". 608 | func getIptablesCommand(proto Protocol) string { 609 | if proto == ProtocolIPv6 { 610 | return "ip6tables" 611 | } else { 612 | return "iptables" 613 | } 614 | } 615 | 616 | // Checks if iptables has the "-C" and "--wait" flag 617 | func getIptablesCommandSupport(v1 int, v2 int, v3 int) (bool, bool, bool, bool) { 618 | return iptablesHasCheckCommand(v1, v2, v3), iptablesHasWaitCommand(v1, v2, v3), iptablesWaitSupportSecond(v1, v2, v3), iptablesHasRandomFully(v1, v2, v3) 619 | } 620 | 621 | // getIptablesVersion returns the first three components of the iptables version 622 | // and the operating mode (e.g. nf_tables or legacy) 623 | // e.g. "iptables v1.3.66" would return (1, 3, 66, legacy, nil) 624 | func extractIptablesVersion(str string) (int, int, int, string, error) { 625 | versionMatcher := regexp.MustCompile(`v([0-9]+)\.([0-9]+)\.([0-9]+)(?:\s+\((\w+))?`) 626 | result := versionMatcher.FindStringSubmatch(str) 627 | if result == nil { 628 | return 0, 0, 0, "", fmt.Errorf("no iptables version found in string: %s", str) 629 | } 630 | 631 | v1, err := strconv.Atoi(result[1]) 632 | if err != nil { 633 | return 0, 0, 0, "", err 634 | } 635 | 636 | v2, err := strconv.Atoi(result[2]) 637 | if err != nil { 638 | return 0, 0, 0, "", err 639 | } 640 | 641 | v3, err := strconv.Atoi(result[3]) 642 | if err != nil { 643 | return 0, 0, 0, "", err 644 | } 645 | 646 | mode := "legacy" 647 | if result[4] != "" { 648 | mode = result[4] 649 | } 650 | return v1, v2, v3, mode, nil 651 | } 652 | 653 | // Runs "iptables --version" to get the version string 654 | func getIptablesVersionString(path string) (string, error) { 655 | cmd := exec.Command(path, "--version") 656 | var out bytes.Buffer 657 | cmd.Stdout = &out 658 | err := cmd.Run() 659 | if err != nil { 660 | return "", err 661 | } 662 | return out.String(), nil 663 | } 664 | 665 | // Checks if an iptables version is after 1.4.11, when --check was added 666 | func iptablesHasCheckCommand(v1 int, v2 int, v3 int) bool { 667 | if v1 > 1 { 668 | return true 669 | } 670 | if v1 == 1 && v2 > 4 { 671 | return true 672 | } 673 | if v1 == 1 && v2 == 4 && v3 >= 11 { 674 | return true 675 | } 676 | return false 677 | } 678 | 679 | // Checks if an iptables version is after 1.4.20, when --wait was added 680 | func iptablesHasWaitCommand(v1 int, v2 int, v3 int) bool { 681 | if v1 > 1 { 682 | return true 683 | } 684 | if v1 == 1 && v2 > 4 { 685 | return true 686 | } 687 | if v1 == 1 && v2 == 4 && v3 >= 20 { 688 | return true 689 | } 690 | return false 691 | } 692 | 693 | // Checks if an iptablse version is after 1.6.0, when --wait support second 694 | func iptablesWaitSupportSecond(v1 int, v2 int, v3 int) bool { 695 | if v1 > 1 { 696 | return true 697 | } 698 | if v1 == 1 && v2 >= 6 { 699 | return true 700 | } 701 | return false 702 | } 703 | 704 | // Checks if an iptables version is after 1.6.2, when --random-fully was added 705 | func iptablesHasRandomFully(v1 int, v2 int, v3 int) bool { 706 | if v1 > 1 { 707 | return true 708 | } 709 | if v1 == 1 && v2 > 6 { 710 | return true 711 | } 712 | if v1 == 1 && v2 == 6 && v3 >= 2 { 713 | return true 714 | } 715 | return false 716 | } 717 | 718 | // Checks if a rule specification exists for a table 719 | func (ipt *IPTables) existsForOldIptables(table, chain string, rulespec []string) (bool, error) { 720 | rs := strings.Join(append([]string{"-A", chain}, rulespec...), " ") 721 | args := []string{"-t", table, "-S"} 722 | var stdout bytes.Buffer 723 | err := ipt.runWithOutput(args, &stdout) 724 | if err != nil { 725 | return false, err 726 | } 727 | return strings.Contains(stdout.String(), rs), nil 728 | } 729 | 730 | // counterRegex is the regex used to detect nftables counter format 731 | var counterRegex = regexp.MustCompile(`^\[([0-9]+):([0-9]+)\] `) 732 | 733 | // filterRuleOutput works around some inconsistencies in output. 734 | // For example, when iptables is in legacy vs. nftables mode, it produces 735 | // different results. 736 | func filterRuleOutput(rule string) string { 737 | out := rule 738 | 739 | // work around an output difference in nftables mode where counters 740 | // are output in iptables-save format, rather than iptables -S format 741 | // The string begins with "[0:0]" 742 | // 743 | // Fixes #49 744 | if groups := counterRegex.FindStringSubmatch(out); groups != nil { 745 | // drop the brackets 746 | out = out[len(groups[0]):] 747 | out = fmt.Sprintf("%s -c %s %s", out, groups[1], groups[2]) 748 | } 749 | 750 | return out 751 | } 752 | -------------------------------------------------------------------------------- /iptables/iptables_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 CoreOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package iptables 16 | 17 | import ( 18 | "crypto/rand" 19 | "fmt" 20 | "math/big" 21 | "net" 22 | "os" 23 | "reflect" 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | func TestProto(t *testing.T) { 29 | ipt, err := New() 30 | if err != nil { 31 | t.Fatalf("New failed: %v", err) 32 | } 33 | if ipt.Proto() != ProtocolIPv4 { 34 | t.Fatalf("Expected default protocol IPv4, got %v", ipt.Proto()) 35 | } 36 | 37 | ip4t, err := NewWithProtocol(ProtocolIPv4) 38 | if err != nil { 39 | t.Fatalf("NewWithProtocol(ProtocolIPv4) failed: %v", err) 40 | } 41 | if ip4t.Proto() != ProtocolIPv4 { 42 | t.Fatalf("Expected protocol IPv4, got %v", ip4t.Proto()) 43 | } 44 | 45 | ip6t, err := NewWithProtocol(ProtocolIPv6) 46 | if err != nil { 47 | t.Fatalf("NewWithProtocol(ProtocolIPv6) failed: %v", err) 48 | } 49 | if ip6t.Proto() != ProtocolIPv6 { 50 | t.Fatalf("Expected protocol IPv6, got %v", ip6t.Proto()) 51 | } 52 | } 53 | 54 | func TestTimeout(t *testing.T) { 55 | ipt, err := New() 56 | if err != nil { 57 | t.Fatalf("New failed: %v", err) 58 | } 59 | if ipt.timeout != 0 { 60 | t.Fatalf("Expected timeout 0 (wait forever), got %v", ipt.timeout) 61 | } 62 | 63 | ipt2, err := New(Timeout(5)) 64 | if err != nil { 65 | t.Fatalf("New failed: %v", err) 66 | } 67 | if ipt2.timeout != 5 { 68 | t.Fatalf("Expected timeout 5, got %v", ipt.timeout) 69 | } 70 | 71 | } 72 | 73 | // force usage of -legacy or -nft commands and check that they're detected correctly 74 | func TestLegacyDetection(t *testing.T) { 75 | testCases := []struct { 76 | in string 77 | mode string 78 | err bool 79 | }{ 80 | { 81 | "iptables-legacy", 82 | "legacy", 83 | false, 84 | }, 85 | { 86 | "ip6tables-legacy", 87 | "legacy", 88 | false, 89 | }, 90 | { 91 | "iptables-nft", 92 | "nf_tables", 93 | false, 94 | }, 95 | { 96 | "ip6tables-nft", 97 | "nf_tables", 98 | false, 99 | }, 100 | } 101 | 102 | for i, tt := range testCases { 103 | t.Run(fmt.Sprint(i), func(t *testing.T) { 104 | ipt, err := New(Path(tt.in)) 105 | if err == nil && tt.err { 106 | t.Fatal("expected err, got none") 107 | } else if err != nil && !tt.err { 108 | t.Fatalf("unexpected err %s", err) 109 | } 110 | 111 | if !strings.Contains(ipt.path, tt.in) { 112 | t.Fatalf("Expected path %s in %s", tt.in, ipt.path) 113 | } 114 | if ipt.mode != tt.mode { 115 | t.Fatalf("Expected %s iptables, but got %s", tt.mode, ipt.mode) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func randChain(t *testing.T) string { 122 | n, err := rand.Int(rand.Reader, big.NewInt(1000000)) 123 | if err != nil { 124 | t.Fatalf("Failed to generate random chain name: %v", err) 125 | } 126 | 127 | return "TEST-" + n.String() 128 | } 129 | 130 | func contains(list []string, value string) bool { 131 | for _, val := range list { 132 | if val == value { 133 | return true 134 | } 135 | } 136 | return false 137 | } 138 | 139 | // mustTestableIptables returns a list of ip(6)tables handles with various 140 | // features enabled & disabled, to test compatibility. 141 | // We used to test noWait as well, but that was removed as of iptables v1.6.0 142 | func mustTestableIptables() []*IPTables { 143 | ipt, err := New() 144 | if err != nil { 145 | panic(fmt.Sprintf("New failed: %v", err)) 146 | } 147 | ip6t, err := NewWithProtocol(ProtocolIPv6) 148 | if err != nil { 149 | panic(fmt.Sprintf("NewWithProtocol(ProtocolIPv6) failed: %v", err)) 150 | } 151 | ipts := []*IPTables{ipt, ip6t} 152 | 153 | // ensure we check one variant without built-in checking 154 | if ipt.hasCheck { 155 | i := *ipt 156 | i.hasCheck = false 157 | ipts = append(ipts, &i) 158 | 159 | i6 := *ip6t 160 | i6.hasCheck = false 161 | ipts = append(ipts, &i6) 162 | } else { 163 | panic("iptables on this machine is too old -- missing -C") 164 | } 165 | return ipts 166 | } 167 | 168 | func TestChain(t *testing.T) { 169 | for i, ipt := range mustTestableIptables() { 170 | t.Run(fmt.Sprint(i), func(t *testing.T) { 171 | runChainTests(t, ipt) 172 | }) 173 | } 174 | } 175 | 176 | func runChainTests(t *testing.T, ipt *IPTables) { 177 | t.Logf("testing %s (hasWait=%t, hasCheck=%t)", ipt.path, ipt.hasWait, ipt.hasCheck) 178 | 179 | chain := randChain(t) 180 | 181 | // Saving the list of chains before executing tests 182 | originalListChain, err := ipt.ListChains("filter") 183 | if err != nil { 184 | t.Fatalf("ListChains of Initial failed: %v", err) 185 | } 186 | 187 | // chain shouldn't exist, this will create new 188 | err = ipt.ClearChain("filter", chain) 189 | if err != nil { 190 | t.Fatalf("ClearChain (of missing) failed: %v", err) 191 | } 192 | 193 | // chain should be in listChain 194 | listChain, err := ipt.ListChains("filter") 195 | if err != nil { 196 | t.Fatalf("ListChains failed: %v", err) 197 | } 198 | if !contains(listChain, chain) { 199 | t.Fatalf("ListChains doesn't contain the new chain %v", chain) 200 | } 201 | 202 | // ChainExists should find it, too 203 | exists, err := ipt.ChainExists("filter", chain) 204 | if err != nil { 205 | t.Fatalf("ChainExists for existing chain failed: %v", err) 206 | } else if !exists { 207 | t.Fatalf("ChainExists doesn't find existing chain") 208 | } 209 | 210 | // chain now exists 211 | err = ipt.ClearChain("filter", chain) 212 | if err != nil { 213 | t.Fatalf("ClearChain (of empty) failed: %v", err) 214 | } 215 | 216 | // put a simple rule in 217 | err = ipt.Append("filter", chain, "-s", "0/0", "-j", "ACCEPT") 218 | if err != nil { 219 | t.Fatalf("Append failed: %v", err) 220 | } 221 | 222 | // can't delete non-empty chain 223 | err = ipt.DeleteChain("filter", chain) 224 | if err == nil { 225 | t.Fatalf("DeleteChain of non-empty chain did not fail") 226 | } 227 | e, ok := err.(*Error) 228 | if ok && e.IsNotExist() { 229 | t.Fatal("DeleteChain of non-empty chain returned IsNotExist") 230 | } 231 | 232 | err = ipt.ClearChain("filter", chain) 233 | if err != nil { 234 | t.Fatalf("ClearChain (of non-empty) failed: %v", err) 235 | } 236 | 237 | // rename the chain 238 | newChain := randChain(t) 239 | err = ipt.RenameChain("filter", chain, newChain) 240 | if err != nil { 241 | t.Fatalf("RenameChain failed: %v", err) 242 | } 243 | 244 | // chain empty, should be ok 245 | err = ipt.DeleteChain("filter", newChain) 246 | if err != nil { 247 | t.Fatalf("DeleteChain of empty chain failed: %v", err) 248 | } 249 | 250 | // check that chain is fully gone and that state similar to initial one 251 | listChain, err = ipt.ListChains("filter") 252 | if err != nil { 253 | t.Fatalf("ListChains failed: %v", err) 254 | } 255 | if !reflect.DeepEqual(originalListChain, listChain) { 256 | t.Fatalf("ListChains mismatch: \ngot %#v \nneed %#v", originalListChain, listChain) 257 | } 258 | 259 | // ChainExists must not find it anymore 260 | exists, err = ipt.ChainExists("filter", chain) 261 | if err != nil { 262 | t.Fatalf("ChainExists for non-existing chain failed: %v", err) 263 | } else if exists { 264 | t.Fatalf("ChainExists finds non-existing chain") 265 | } 266 | 267 | // test ClearAndDelete 268 | err = ipt.NewChain("filter", chain) 269 | if err != nil { 270 | t.Fatalf("NewChain failed: %v", err) 271 | } 272 | err = ipt.Append("filter", chain, "-j", "ACCEPT") 273 | if err != nil { 274 | t.Fatalf("Append failed: %v", err) 275 | } 276 | err = ipt.ClearAndDeleteChain("filter", chain) 277 | if err != nil { 278 | t.Fatalf("ClearAndDelete failed: %v", err) 279 | } 280 | exists, err = ipt.ChainExists("filter", chain) 281 | if err != nil { 282 | t.Fatalf("ChainExists failed: %v", err) 283 | } 284 | if exists { 285 | t.Fatalf("ClearAndDelete didn't delete the chain") 286 | } 287 | err = ipt.ClearAndDeleteChain("filter", chain) 288 | if err != nil { 289 | t.Fatalf("ClearAndDelete failed for non-existing chain: %v", err) 290 | } 291 | } 292 | 293 | func TestRules(t *testing.T) { 294 | for i, ipt := range mustTestableIptables() { 295 | t.Run(fmt.Sprint(i), func(t *testing.T) { 296 | runRulesTests(t, ipt) 297 | }) 298 | } 299 | } 300 | 301 | func runRulesTests(t *testing.T, ipt *IPTables) { 302 | t.Logf("testing %s (hasWait=%t, hasCheck=%t)", getIptablesCommand(ipt.Proto()), ipt.hasWait, ipt.hasCheck) 303 | 304 | var address1, address2, address3, address4, subnet1, subnet2, subnet3, subnet4 string 305 | if ipt.Proto() == ProtocolIPv6 { 306 | address1 = "2001:db8::1/128" 307 | address2 = "2001:db8::2/128" 308 | address3 = "2001:db8::3/128" 309 | address4 = "2001:db8::4/128" 310 | subnet1 = "2001:db8:a::/48" 311 | subnet2 = "2001:db8:b::/48" 312 | subnet3 = "2001:db8:c::/48" 313 | subnet4 = "2001:db8:d::/48" 314 | } else { 315 | address1 = "203.0.113.1/32" 316 | address2 = "203.0.113.2/32" 317 | address3 = "203.0.113.3/32" 318 | address4 = "203.0.113.4/32" 319 | subnet1 = "192.0.2.0/24" 320 | subnet2 = "198.51.100.0/24" 321 | subnet3 = "198.51.101.0/24" 322 | subnet4 = "198.51.102.0/24" 323 | } 324 | 325 | chain := randChain(t) 326 | 327 | // chain shouldn't exist, this will create new 328 | err := ipt.ClearChain("filter", chain) 329 | if err != nil { 330 | t.Fatalf("ClearChain (of missing) failed: %v", err) 331 | } 332 | 333 | err = ipt.Append("filter", chain, "-s", subnet1, "-d", address1, "-j", "ACCEPT") 334 | if err != nil { 335 | t.Fatalf("Append failed: %v", err) 336 | } 337 | 338 | err = ipt.AppendUnique("filter", chain, "-s", subnet1, "-d", address1, "-j", "ACCEPT") 339 | if err != nil { 340 | t.Fatalf("AppendUnique failed: %v", err) 341 | } 342 | 343 | err = ipt.Append("filter", chain, "-s", subnet2, "-d", address1, "-j", "ACCEPT") 344 | if err != nil { 345 | t.Fatalf("Append failed: %v", err) 346 | } 347 | 348 | err = ipt.Insert("filter", chain, 2, "-s", subnet2, "-d", address2, "-j", "ACCEPT") 349 | if err != nil { 350 | t.Fatalf("Insert failed: %v", err) 351 | } 352 | 353 | err = ipt.InsertUnique("filter", chain, 2, "-s", subnet2, "-d", address2, "-j", "ACCEPT") 354 | if err != nil { 355 | t.Fatalf("Insert failed: %v", err) 356 | } 357 | 358 | err = ipt.Insert("filter", chain, 1, "-s", subnet1, "-d", address2, "-j", "ACCEPT") 359 | if err != nil { 360 | t.Fatalf("Insert failed: %v", err) 361 | } 362 | 363 | err = ipt.Delete("filter", chain, "-s", subnet1, "-d", address2, "-j", "ACCEPT") 364 | if err != nil { 365 | t.Fatalf("Delete failed: %v", err) 366 | } 367 | 368 | err = ipt.Insert("filter", chain, 1, "-s", subnet1, "-d", address2, "-j", "ACCEPT") 369 | if err != nil { 370 | t.Fatalf("Insert failed: %v", err) 371 | } 372 | 373 | err = ipt.Replace("filter", chain, 1, "-s", subnet2, "-d", address2, "-j", "ACCEPT") 374 | if err != nil { 375 | t.Fatalf("Replace failed: %v", err) 376 | } 377 | 378 | err = ipt.Delete("filter", chain, "-s", subnet2, "-d", address2, "-j", "ACCEPT") 379 | if err != nil { 380 | t.Fatalf("Delete failed: %v", err) 381 | } 382 | 383 | err = ipt.Append("filter", chain, "-s", address1, "-d", subnet2, "-j", "ACCEPT") 384 | if err != nil { 385 | t.Fatalf("Append failed: %v", err) 386 | } 387 | 388 | rules, err := ipt.List("filter", chain) 389 | if err != nil { 390 | t.Fatalf("List failed: %v", err) 391 | } 392 | 393 | // Verify DeleteById functionality by adding two new rules and removing second last 394 | ruleCount1 := len(rules) 395 | err = ipt.Append("filter", chain, "-s", address3, "-d", subnet3, "-j", "ACCEPT") 396 | if err != nil { 397 | t.Fatalf("Append failed: %v", err) 398 | } 399 | err = ipt.Append("filter", chain, "-s", address4, "-d", subnet4, "-j", "ACCEPT") 400 | if err != nil { 401 | t.Fatalf("Append failed: %v", err) 402 | } 403 | err = ipt.DeleteById("filter", chain, ruleCount1) 404 | if err != nil { 405 | t.Fatalf("DeleteById failed: %v", err) 406 | } 407 | rules, err = ipt.List("filter", chain) 408 | if err != nil { 409 | t.Fatalf("List failed: %v", err) 410 | } 411 | 412 | expected := []string{ 413 | "-N " + chain, 414 | "-A " + chain + " -s " + subnet1 + " -d " + address1 + " -j ACCEPT", 415 | "-A " + chain + " -s " + subnet2 + " -d " + address2 + " -j ACCEPT", 416 | "-A " + chain + " -s " + subnet2 + " -d " + address1 + " -j ACCEPT", 417 | "-A " + chain + " -s " + address1 + " -d " + subnet2 + " -j ACCEPT", 418 | "-A " + chain + " -s " + address4 + " -d " + subnet4 + " -j ACCEPT", 419 | } 420 | 421 | if !reflect.DeepEqual(rules, expected) { 422 | t.Fatalf("List mismatch: \ngot %#v \nneed %#v", rules, expected) 423 | } 424 | 425 | rules, err = ipt.ListWithCounters("filter", chain) 426 | if err != nil { 427 | t.Fatalf("ListWithCounters failed: %v", err) 428 | } 429 | 430 | makeExpected := func(suffix string) []string { 431 | return []string{ 432 | "-N " + chain, 433 | "-A " + chain + " -s " + subnet1 + " -d " + address1 + " " + suffix, 434 | "-A " + chain + " -s " + subnet2 + " -d " + address2 + " " + suffix, 435 | "-A " + chain + " -s " + subnet2 + " -d " + address1 + " " + suffix, 436 | "-A " + chain + " -s " + address1 + " -d " + subnet2 + " " + suffix, 437 | "-A " + chain + " -s " + address4 + " -d " + subnet4 + " " + suffix, 438 | } 439 | } 440 | // older nf_tables returned the second order 441 | if !reflect.DeepEqual(rules, makeExpected("-c 0 0 -j ACCEPT")) && 442 | !reflect.DeepEqual(rules, makeExpected("-j ACCEPT -c 0 0")) { 443 | t.Fatalf("ListWithCounters mismatch: \ngot %#v \nneed %#v", rules, makeExpected("<-c 0 0 and -j ACCEPT in either order>")) 444 | } 445 | 446 | stats, err := ipt.Stats("filter", chain) 447 | if err != nil { 448 | t.Fatalf("Stats failed: %v", err) 449 | } 450 | 451 | opt := "--" 452 | prot := "0" 453 | if ipt.proto == ProtocolIPv6 && 454 | ipt.v1 == 1 && (ipt.v2 < 8 || (ipt.v2 == 8 && ipt.v3 < 9)) { 455 | // this is fixed in iptables 1.8.9 via iptables/6e41c2d874 456 | opt = " " 457 | // this is fixed in iptables 1.8.9 via iptables/da8ecc62dd 458 | prot = "all" 459 | } 460 | if ipt.proto == ProtocolIPv4 && 461 | ipt.v1 == 1 && (ipt.v2 < 8 || (ipt.v2 == 8 && ipt.v3 < 9)) { 462 | // this is fixed in iptables 1.8.9 via iptables/da8ecc62dd 463 | prot = "all" 464 | } 465 | 466 | expectedStats := [][]string{ 467 | {"0", "0", "ACCEPT", prot, opt, "*", "*", subnet1, address1, ""}, 468 | {"0", "0", "ACCEPT", prot, opt, "*", "*", subnet2, address2, ""}, 469 | {"0", "0", "ACCEPT", prot, opt, "*", "*", subnet2, address1, ""}, 470 | {"0", "0", "ACCEPT", prot, opt, "*", "*", address1, subnet2, ""}, 471 | {"0", "0", "ACCEPT", prot, opt, "*", "*", address4, subnet4, ""}, 472 | } 473 | 474 | if !reflect.DeepEqual(stats, expectedStats) { 475 | t.Fatalf("Stats mismatch: \ngot %#v \nneed %#v", stats, expectedStats) 476 | } 477 | 478 | structStats, err := ipt.StructuredStats("filter", chain) 479 | if err != nil { 480 | t.Fatalf("StructuredStats failed: %v", err) 481 | } 482 | 483 | // It's okay to not check the following errors as they will be evaluated 484 | // in the subsequent usage 485 | _, address1CIDR, _ := net.ParseCIDR(address1) 486 | _, address2CIDR, _ := net.ParseCIDR(address2) 487 | _, address4CIDR, _ := net.ParseCIDR(address4) 488 | _, subnet1CIDR, _ := net.ParseCIDR(subnet1) 489 | _, subnet2CIDR, _ := net.ParseCIDR(subnet2) 490 | _, subnet4CIDR, _ := net.ParseCIDR(subnet4) 491 | 492 | expectedStructStats := []Stat{ 493 | {0, 0, "ACCEPT", prot, opt, "*", "*", subnet1CIDR, address1CIDR, ""}, 494 | {0, 0, "ACCEPT", prot, opt, "*", "*", subnet2CIDR, address2CIDR, ""}, 495 | {0, 0, "ACCEPT", prot, opt, "*", "*", subnet2CIDR, address1CIDR, ""}, 496 | {0, 0, "ACCEPT", prot, opt, "*", "*", address1CIDR, subnet2CIDR, ""}, 497 | {0, 0, "ACCEPT", prot, opt, "*", "*", address4CIDR, subnet4CIDR, ""}, 498 | } 499 | 500 | if !reflect.DeepEqual(structStats, expectedStructStats) { 501 | t.Fatalf("StructuredStats mismatch: \ngot %#v \nneed %#v", 502 | structStats, expectedStructStats) 503 | } 504 | 505 | for i, stat := range expectedStats { 506 | stat, err := ipt.ParseStat(stat) 507 | if err != nil { 508 | t.Fatalf("ParseStat failed: %v", err) 509 | } 510 | if !reflect.DeepEqual(stat, expectedStructStats[i]) { 511 | t.Fatalf("ParseStat mismatch: \ngot %#v \nneed %#v", 512 | stat, expectedStructStats[i]) 513 | } 514 | } 515 | 516 | err = ipt.DeleteIfExists("filter", chain, "-s", address1, "-d", subnet2, "-j", "ACCEPT") 517 | if err != nil { 518 | t.Fatalf("DeleteIfExists failed for existing rule: %v", err) 519 | } 520 | err = ipt.DeleteIfExists("filter", chain, "-s", address1, "-d", subnet2, "-j", "ACCEPT") 521 | if err != nil { 522 | t.Fatalf("DeleteIfExists failed for non-existing rule: %v", err) 523 | } 524 | 525 | // Clear the chain that was created. 526 | err = ipt.ClearChain("filter", chain) 527 | if err != nil { 528 | t.Fatalf("Failed to clear test chain: %v", err) 529 | } 530 | 531 | // Delete the chain that was created 532 | err = ipt.DeleteChain("filter", chain) 533 | if err != nil { 534 | t.Fatalf("Failed to delete test chain: %v", err) 535 | } 536 | } 537 | 538 | // TestError checks that we're OK when iptables fails to execute 539 | func TestError(t *testing.T) { 540 | ipt, err := New() 541 | if err != nil { 542 | t.Fatalf("failed to init: %v", err) 543 | } 544 | 545 | chain := randChain(t) 546 | _, err = ipt.List("filter", chain) 547 | if err == nil { 548 | t.Fatalf("no error with invalid params") 549 | } 550 | switch e := err.(type) { 551 | case *Error: 552 | // OK 553 | default: 554 | t.Fatalf("expected type iptables.Error, got %t", e) 555 | } 556 | 557 | // Now set an invalid binary path 558 | ipt.path = "/does-not-exist" 559 | 560 | _, err = ipt.ListChains("filter") 561 | 562 | if err == nil { 563 | t.Fatalf("no error with invalid ipt binary") 564 | } 565 | 566 | switch e := err.(type) { 567 | case *os.PathError: 568 | // OK 569 | default: 570 | t.Fatalf("expected type os.PathError, got %t", e) 571 | } 572 | } 573 | 574 | func TestIsNotExist(t *testing.T) { 575 | ipt, err := New() 576 | if err != nil { 577 | t.Fatalf("failed to init: %v", err) 578 | } 579 | // Create a chain, add a rule 580 | chainName := randChain(t) 581 | err = ipt.NewChain("filter", chainName) 582 | if err != nil { 583 | t.Fatal(err) 584 | } 585 | defer func() { 586 | if err := ipt.ClearChain("filter", chainName); err != nil { 587 | t.Fatal(err) 588 | } 589 | if err := ipt.DeleteChain("filter", chainName); err != nil { 590 | t.Fatal(err) 591 | } 592 | }() 593 | 594 | err = ipt.Append("filter", chainName, "-p", "tcp", "-j", "DROP") 595 | if err != nil { 596 | t.Fatal(err) 597 | } 598 | 599 | // Delete rule twice 600 | err = ipt.Delete("filter", chainName, "-p", "tcp", "-j", "DROP") 601 | if err != nil { 602 | t.Fatal(err) 603 | } 604 | 605 | err = ipt.Delete("filter", chainName, "-p", "tcp", "-j", "DROP") 606 | if err == nil { 607 | t.Fatal("delete twice got no error...") 608 | } 609 | 610 | e, ok := err.(*Error) 611 | if !ok { 612 | t.Fatalf("Got wrong error type, expected iptables.Error, got %T", err) 613 | } 614 | 615 | if !e.IsNotExist() { 616 | t.Fatal("IsNotExist returned false, expected true") 617 | } 618 | 619 | // Delete chain 620 | err = ipt.DeleteChain("filter", chainName) 621 | if err != nil { 622 | t.Fatal(err) 623 | } 624 | 625 | err = ipt.DeleteChain("filter", chainName) 626 | if err == nil { 627 | t.Fatal("deletechain twice got no error...") 628 | } 629 | 630 | e, ok = err.(*Error) 631 | if !ok { 632 | t.Fatalf("Got wrong error type, expected iptables.Error, got %T", err) 633 | } 634 | 635 | if !e.IsNotExist() { 636 | t.Fatal("IsNotExist returned false, expected true") 637 | } 638 | 639 | // iptables may add more logs to the errors msgs 640 | e.msg = "Another app is currently holding the xtables lock; waiting (1s) for it to exit..." + e.msg 641 | if !e.IsNotExist() { 642 | t.Fatal("IsNotExist returned false, expected true") 643 | } 644 | 645 | } 646 | 647 | func TestIsNotExistForIPv6(t *testing.T) { 648 | ipt, err := NewWithProtocol(ProtocolIPv6) 649 | if err != nil { 650 | t.Fatalf("failed to init: %v", err) 651 | } 652 | // Create a chain, add a rule 653 | chainName := randChain(t) 654 | err = ipt.NewChain("filter", chainName) 655 | if err != nil { 656 | t.Fatal(err) 657 | } 658 | defer func() { 659 | if err := ipt.ClearChain("filter", chainName); err != nil { 660 | t.Fatal(err) 661 | } 662 | if err := ipt.DeleteChain("filter", chainName); err != nil { 663 | t.Fatal(err) 664 | } 665 | }() 666 | 667 | err = ipt.Append("filter", chainName, "-p", "tcp", "-j", "DROP") 668 | if err != nil { 669 | t.Fatal(err) 670 | } 671 | 672 | // Delete rule twice 673 | err = ipt.Delete("filter", chainName, "-p", "tcp", "-j", "DROP") 674 | if err != nil { 675 | t.Fatal(err) 676 | } 677 | 678 | err = ipt.Delete("filter", chainName, "-p", "tcp", "-j", "DROP") 679 | if err == nil { 680 | t.Fatal("delete twice got no error...") 681 | } 682 | 683 | e, ok := err.(*Error) 684 | if !ok { 685 | t.Fatalf("Got wrong error type, expected iptables.Error, got %T", err) 686 | } 687 | 688 | if !e.IsNotExist() { 689 | t.Fatal("IsNotExist returned false, expected true") 690 | } 691 | 692 | // Delete chain 693 | err = ipt.DeleteChain("filter", chainName) 694 | if err != nil { 695 | t.Fatal(err) 696 | } 697 | 698 | err = ipt.DeleteChain("filter", chainName) 699 | if err == nil { 700 | t.Fatal("deletechain twice got no error...") 701 | } 702 | 703 | e, ok = err.(*Error) 704 | if !ok { 705 | t.Fatalf("Got wrong error type, expected iptables.Error, got %T", err) 706 | } 707 | 708 | if !e.IsNotExist() { 709 | t.Fatal("IsNotExist returned false, expected true") 710 | } 711 | 712 | // iptables may add more logs to the errors msgs 713 | e.msg = "Another app is currently holding the xtables lock; waiting (1s) for it to exit..." + e.msg 714 | if !e.IsNotExist() { 715 | t.Fatal("IsNotExist returned false, expected true") 716 | } 717 | } 718 | 719 | func TestFilterRuleOutput(t *testing.T) { 720 | testCases := []struct { 721 | name string 722 | in string 723 | out string 724 | }{ 725 | { 726 | "legacy output", 727 | "-A foo1 -p tcp -m tcp --dport 1337 -j ACCEPT", 728 | "-A foo1 -p tcp -m tcp --dport 1337 -j ACCEPT", 729 | }, 730 | { 731 | "nft output", 732 | "[99:42] -A foo1 -p tcp -m tcp --dport 1337 -j ACCEPT", 733 | "-A foo1 -p tcp -m tcp --dport 1337 -j ACCEPT -c 99 42", 734 | }, 735 | } 736 | 737 | for _, tt := range testCases { 738 | t.Run(tt.name, func(t *testing.T) { 739 | actual := filterRuleOutput(tt.in) 740 | if actual != tt.out { 741 | t.Fatalf("expect %s actual %s", tt.out, actual) 742 | } 743 | }) 744 | } 745 | } 746 | 747 | func TestExtractIptablesVersion(t *testing.T) { 748 | testCases := []struct { 749 | in string 750 | v1, v2, v3 int 751 | mode string 752 | err bool 753 | }{ 754 | { 755 | "iptables v1.8.0 (nf_tables)", 756 | 1, 8, 0, 757 | "nf_tables", 758 | false, 759 | }, 760 | { 761 | "iptables v1.8.0 (legacy)", 762 | 1, 8, 0, 763 | "legacy", 764 | false, 765 | }, 766 | { 767 | "iptables v1.6.2", 768 | 1, 6, 2, 769 | "legacy", 770 | false, 771 | }, 772 | } 773 | 774 | for i, tt := range testCases { 775 | t.Run(fmt.Sprint(i), func(t *testing.T) { 776 | v1, v2, v3, mode, err := extractIptablesVersion(tt.in) 777 | if err == nil && tt.err { 778 | t.Fatal("expected err, got none") 779 | } else if err != nil && !tt.err { 780 | t.Fatalf("unexpected err %s", err) 781 | } 782 | 783 | if v1 != tt.v1 || v2 != tt.v2 || v3 != tt.v3 || mode != tt.mode { 784 | t.Fatalf("expected %d %d %d %s, got %d %d %d %s", 785 | tt.v1, tt.v2, tt.v3, tt.mode, 786 | v1, v2, v3, mode) 787 | } 788 | }) 789 | } 790 | } 791 | 792 | func TestListById(t *testing.T) { 793 | testCases := []struct { 794 | in string 795 | id int 796 | out string 797 | expected bool 798 | }{ 799 | { 800 | "-i lo -p tcp -m tcp --dport 3000 -j DNAT --to-destination 127.0.0.1:3000", 801 | 1, 802 | "-A PREROUTING -i lo -p tcp -m tcp --dport 3000 -j DNAT --to-destination 127.0.0.1:3000", 803 | true, 804 | }, 805 | { 806 | "-i lo -p tcp -m tcp --dport 3000 -j DNAT --to-destination 127.0.0.1:3001", 807 | 2, 808 | "-A PREROUTING -i lo -p tcp -m tcp --dport 3000 -j DNAT --to-destination 127.0.0.1:3001", 809 | true, 810 | }, 811 | { 812 | "-i lo -p tcp -m tcp --dport 3000 -j DNAT --to-destination 127.0.0.1:3002", 813 | 3, 814 | "-A PREROUTING -i lo -p tcp -m tcp --dport 3000 -j DNAT --to-destination 127.0.0.1:3003", 815 | false, 816 | }, 817 | } 818 | 819 | ipt, err := New() 820 | if err != nil { 821 | t.Fatalf("failed to init: %v", err) 822 | } 823 | // ensure to test in a clear environment 824 | err = ipt.ClearChain("nat", "PREROUTING") 825 | if err != nil { 826 | t.Fatal(err) 827 | } 828 | 829 | defer func() { 830 | err = ipt.ClearChain("nat", "PREROUTING") 831 | if err != nil { 832 | t.Fatal(err) 833 | } 834 | }() 835 | 836 | for _, tt := range testCases { 837 | t.Run(fmt.Sprintf("Checking rule with id %d", tt.id), func(t *testing.T) { 838 | err = ipt.Append("nat", "PREROUTING", strings.Split(tt.in, " ")...) 839 | if err != nil { 840 | t.Fatal(err) 841 | } 842 | rule, err := ipt.ListById("nat", "PREROUTING", tt.id) 843 | if err != nil { 844 | t.Fatal(err) 845 | } 846 | fmt.Println(rule) 847 | test_result := false 848 | if rule == tt.out { 849 | test_result = true 850 | } 851 | if test_result != tt.expected { 852 | t.Fatal("Test failed") 853 | } 854 | }) 855 | } 856 | } 857 | -------------------------------------------------------------------------------- /iptables/lock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 CoreOS, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package iptables 16 | 17 | import ( 18 | "os" 19 | "sync" 20 | "syscall" 21 | ) 22 | 23 | const ( 24 | // In earlier versions of iptables, the xtables lock was implemented 25 | // via a Unix socket, but now flock is used via this lockfile: 26 | // http://git.netfilter.org/iptables/commit/?id=aa562a660d1555b13cffbac1e744033e91f82707 27 | // Note the LSB-conforming "/run" directory does not exist on old 28 | // distributions, so assume "/var" is symlinked 29 | xtablesLockFilePath = "/var/run/xtables.lock" 30 | 31 | defaultFilePerm = 0600 32 | ) 33 | 34 | type Unlocker interface { 35 | Unlock() error 36 | } 37 | 38 | type nopUnlocker struct{} 39 | 40 | func (_ nopUnlocker) Unlock() error { return nil } 41 | 42 | type fileLock struct { 43 | // mu is used to protect against concurrent invocations from within this process 44 | mu sync.Mutex 45 | fd int 46 | } 47 | 48 | // tryLock takes an exclusive lock on the xtables lock file without blocking. 49 | // This is best-effort only: if the exclusive lock would block (i.e. because 50 | // another process already holds it), no error is returned. Otherwise, any 51 | // error encountered during the locking operation is returned. 52 | // The returned Unlocker should be used to release the lock when the caller is 53 | // done invoking iptables commands. 54 | func (l *fileLock) tryLock() (Unlocker, error) { 55 | l.mu.Lock() 56 | err := syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB) 57 | switch err { 58 | case syscall.EWOULDBLOCK: 59 | l.mu.Unlock() 60 | return nopUnlocker{}, nil 61 | case nil: 62 | return l, nil 63 | default: 64 | l.mu.Unlock() 65 | return nil, err 66 | } 67 | } 68 | 69 | // Unlock closes the underlying file, which implicitly unlocks it as well. It 70 | // also unlocks the associated mutex. 71 | func (l *fileLock) Unlock() error { 72 | defer l.mu.Unlock() 73 | return syscall.Close(l.fd) 74 | } 75 | 76 | // newXtablesFileLock opens a new lock on the xtables lockfile without 77 | // acquiring the lock 78 | func newXtablesFileLock() (*fileLock, error) { 79 | fd, err := syscall.Open(xtablesLockFilePath, os.O_CREATE, defaultFilePerm) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return &fileLock{fd: fd}, nil 84 | } 85 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Run all go-iptables tests 4 | # ./test 5 | # ./test -v 6 | # 7 | # Run tests for one package 8 | # PKG=./unit ./test 9 | # PKG=ssh ./test 10 | # 11 | set -e 12 | 13 | # Invoke ./cover for HTML output 14 | COVER=${COVER:-"-cover"} 15 | 16 | echo "Checking gofmt..." 17 | fmtRes=$(gofmt -l -s .) 18 | if [ -n "${fmtRes}" ]; then 19 | echo -e "gofmt checking failed:\n${fmtRes}" 20 | exit 255 21 | fi 22 | 23 | echo "Running tests..." 24 | bin=$(mktemp) 25 | 26 | go test -c -o ${bin} ${COVER} ./iptables/... 27 | if [[ -z "$SUDO_PERMITTED" ]]; then 28 | echo "Test aborted for safety reasons. Please set the SUDO_PERMITTED variable." 29 | exit 1 30 | fi 31 | 32 | sudo -E bash -c "${bin} $@ ./iptables/..." 33 | echo "Success" 34 | rm "${bin}" 35 | --------------------------------------------------------------------------------