├── .github
├── dependabot.yml
└── workflows
│ ├── codeql.yml
│ ├── dependency-review.yml
│ ├── lint.yml
│ ├── release.yml
│ ├── scorecards.yml
│ └── tests.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .vscode
└── launch.json
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── apply.go
├── create.go
├── daemon.go
├── daemon_start.go
├── daemon_stop.go
├── delete.go
├── generate.go
├── generate_rules.go
├── generate_systemd.go
├── list.go
├── root.go
└── version.go
├── docs
└── getting-started.md
├── fwdctl-example.png
├── fwdctl.png
├── go.mod
├── go.sum
├── hack
└── fwdctl-seccomp.json
├── install
├── internal
├── constants
│ └── constants.go
├── daemon
│ ├── management.go
│ └── pid_file.go
├── printer
│ ├── json.go
│ ├── printer_interface.go
│ ├── table.go
│ └── yaml.go
├── rules
│ ├── ruleset.go
│ ├── ruleset_test.go
│ └── types.go
└── template
│ ├── rules_template
│ ├── rules.go
│ └── rules.yml.tpl
│ ├── systemd_template
│ ├── fwdctl.service.tpl
│ ├── systemd_service.go
│ └── systemd_service_test.go
│ ├── template.go
│ └── template_test.go
├── main.go
├── main_test.go
├── pkg
└── iptables
│ ├── defaults.go
│ ├── forward.go
│ ├── interface.go
│ ├── interface_test.go
│ ├── rule.go
│ ├── rule_test.go
│ ├── utils.go
│ ├── validation.go
│ └── validation_test.go
├── tests
├── README.md
├── apply.txtar
├── apply_trace.txtar
├── create.txtar
├── create_trace.txtar
├── daemon.txtar
├── daemon_trace.txtar
├── delete.txtar
├── delete_trace.txtar
├── generate.txtar
├── generate_trace.txtar
├── list.txtar
├── list_trace.txtar
├── version.txtar
└── version_trace.txtar
└── utils.go
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: daily
7 |
8 | - package-ecosystem: gomod
9 | directory: /
10 | schedule:
11 | interval: daily
12 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: ["main"]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: ["main"]
20 | schedule:
21 | - cron: "0 0 * * 1"
22 |
23 | permissions:
24 | contents: read
25 |
26 | jobs:
27 | analyze:
28 | name: Analyze
29 | runs-on: ubuntu-latest
30 | permissions:
31 | actions: read
32 | contents: read
33 | security-events: write
34 |
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | language: ["go"]
39 | # CodeQL supports [ $supported-codeql-languages ]
40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
41 |
42 | steps:
43 | - name: Harden Runner
44 | uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1
45 | with:
46 | egress-policy: audit
47 |
48 | - name: Checkout repository
49 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
50 |
51 | # Initializes the CodeQL tools for scanning.
52 | - name: Initialize CodeQL
53 | uses: github/codeql-action/init@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
54 | with:
55 | languages: ${{ matrix.language }}
56 | # If you wish to specify custom queries, you can do so here or in a config file.
57 | # By default, queries listed here will override any specified in a config file.
58 | # Prefix the list here with "+" to use these queries and those in the config file.
59 |
60 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
61 | # If this step fails, then you should remove it and run the build manually (see below)
62 | - name: Autobuild
63 | uses: github/codeql-action/autobuild@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
64 |
65 | # ℹ️ Command-line programs to run using the OS shell.
66 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
67 |
68 | # If the Autobuild fails above, remove it and uncomment the following three lines.
69 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
70 |
71 | # - run: |
72 | # echo "Run, Build Application using script"
73 | # ./location_of_script_within_repo/buildscript.sh
74 |
75 | - name: Perform CodeQL Analysis
76 | uses: github/codeql-action/analyze@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
77 | with:
78 | category: "/language:${{matrix.language}}"
79 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | # Dependency Review Action
2 | #
3 | # This Action will scan dependency manifest files that change as part of a Pull Request,
4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR.
5 | # Once installed, if the workflow run is marked as required,
6 | # PRs introducing known-vulnerable packages will be blocked from merging.
7 | #
8 | # Source repository: https://github.com/actions/dependency-review-action
9 | name: 'Dependency Review'
10 | on: [pull_request]
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | dependency-review:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Harden Runner
20 | uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1
21 | with:
22 | egress-policy: audit
23 |
24 | - name: 'Checkout Repository'
25 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
26 | - name: 'Dependency Review'
27 | uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2.5.1
28 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on: push
3 | permissions:
4 | contents: read
5 | jobs:
6 | golangci:
7 | name: lint
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Harden Runner
11 | uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1
12 | with:
13 | egress-policy: audit
14 |
15 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # ratchet:actions/setup-go@v3
16 | with:
17 | go-version: 1.22
18 |
19 | - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # ratchet:actions/checkout@v3
20 | - name: golangci-lint
21 | uses: golangci/golangci-lint-action@07db5389c99593f11ad7b44463c2d4233066a9b1 # ratchet:golangci/golangci-lint-action@v3
22 | with:
23 | version: v1.60.3
24 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | permissions:
7 | contents: read
8 |
9 | jobs:
10 | goreleaser:
11 | permissions:
12 | actions: write # for anchore/sbom-action to upload workflow artifacts
13 | contents: write # for goreleaser/goreleaser-action to create a GitHub release
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Harden Runner
17 | uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1
18 | with:
19 | egress-policy: audit
20 |
21 | - name: Checkout
22 | uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # ratchet:actions/checkout@v3
23 | with:
24 | fetch-depth: 0
25 | - name: Set up Go
26 | uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # ratchet:actions/setup-go@v3
27 | with:
28 | go-version: '1.22'
29 | check-latest: true
30 | - run: go version
31 | - name: build
32 | run: make build-gh
33 | - uses: anchore/sbom-action@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 # v0.14.3
34 | with:
35 | artifact-name: fwdctl-sbom.spdx.json
36 | - name: Release
37 | uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564
38 | if: startsWith(github.ref, 'refs/tags/')
39 | with:
40 | files: bin/fwdctl
41 |
--------------------------------------------------------------------------------
/.github/workflows/scorecards.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub. They are provided
2 | # by a third-party and are governed by separate terms of service, privacy
3 | # policy, and support documentation.
4 |
5 | name: Scorecard supply-chain security
6 | on:
7 | # For Branch-Protection check. Only the default branch is supported. See
8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
9 | branch_protection_rule:
10 | # To guarantee Maintained check is occasionally updated. See
11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
12 | schedule:
13 | - cron: '20 7 * * 2'
14 | push:
15 | branches: ["main"]
16 |
17 | # Declare default permissions as read only.
18 | permissions: read-all
19 |
20 | jobs:
21 | analysis:
22 | name: Scorecard analysis
23 | runs-on: ubuntu-latest
24 | permissions:
25 | # Needed to upload the results to code-scanning dashboard.
26 | security-events: write
27 | # Needed to publish results and get a badge (see publish_results below).
28 | id-token: write
29 | contents: read
30 | actions: read
31 |
32 | steps:
33 | - name: Harden Runner
34 | uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1
35 | with:
36 | egress-policy: audit
37 |
38 | - name: "Checkout code"
39 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
40 | with:
41 | persist-credentials: false
42 |
43 | - name: "Run analysis"
44 | uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6
45 | with:
46 | results_file: results.sarif
47 | results_format: sarif
48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
49 | # - you want to enable the Branch-Protection check on a *public* repository, or
50 | # - you are installing Scorecards on a *private* repository
51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }}
53 |
54 | # Public repositories:
55 | # - Publish results to OpenSSF REST API for easy access by consumers
56 | # - Allows the repository to include the Scorecard badge.
57 | # - See https://github.com/ossf/scorecard-action#publishing-results.
58 | # For private repositories:
59 | # - `publish_results` will always be set to `false`, regardless
60 | # of the value entered here.
61 | publish_results: true
62 |
63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
64 | # format to the repository Actions tab.
65 | - name: "Upload artifact"
66 | uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
67 | with:
68 | name: SARIF file
69 | path: results.sarif
70 | retention-days: 5
71 |
72 | # Upload the results to GitHub's code scanning dashboard.
73 | - name: "Upload to code-scanning"
74 | uses: github/codeql-action/upload-sarif@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
75 | with:
76 | sarif_file: results.sarif
77 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | unit-test:
7 |
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4
11 |
12 | - name: Build coverage-instrumented binary
13 | run: |
14 | make build-cover && sudo make -B install
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # ratchet:actions/setup-go@v5
18 | with:
19 | go-version: '1.22'
20 |
21 | - name: Run Unit-Test
22 | run: |
23 | mkdir /tmp/unit/
24 | go test \
25 | -v \
26 | -cover ./... \
27 | -skip TestFwdctl \
28 | -args -test.gocoverdir=/tmp/unit/
29 |
30 | - name: Upload cover profiles
31 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # ratchet:actions/upload-artifact@v4
32 | with:
33 | name: unit-test
34 | path: /tmp/unit/
35 |
36 | integration-test:
37 |
38 | runs-on: ubuntu-latest
39 | steps:
40 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4
41 |
42 | - name: Set up Go
43 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # ratchet:actions/setup-go@v5
44 | with:
45 | go-version: '1.22'
46 |
47 | - name: Install iptables
48 | run: |
49 | sudo apt update
50 | sudo apt install -y iptables
51 |
52 | - name: Build coverage-instrumented binary
53 | run: |
54 | make build-cover && sudo make -B install
55 |
56 | - name: Run integration test
57 | run: |
58 | mkdir /tmp/integration
59 | # we have to run integration tests one-by-one
60 | # otherwhise they will run in parallel.
61 | # since fwdctl apply network forwards, these could
62 | # interact with each other and make the test fail.
63 | go test \
64 | -exec sudo \
65 | -cover \
66 | -v ./... \
67 | -run TestFwdctl/apply$ \
68 | -args -test.gocoverdir=/tmp/integration/
69 | go test \
70 | -exec sudo \
71 | -cover \
72 | -v ./... \
73 | -run TestFwdctl/create$ \
74 | -args -test.gocoverdir=/tmp/integration/
75 | go test \
76 | -exec sudo \
77 | -cover \
78 | -v ./... \
79 | -run TestFwdctl/delete$ \
80 | -args -test.gocoverdir=/tmp/integration/
81 | go test \
82 | -exec sudo \
83 | -cover \
84 | -v ./... \
85 | -run TestFwdctl/list$ \
86 | -args -test.gocoverdir=/tmp/integration/
87 | go test \
88 | -exec sudo \
89 | -cover \
90 | -v ./... \
91 | -run TestFwdctl/daemon$ \
92 | -args -test.gocoverdir=/tmp/integration/
93 | go test \
94 | -exec sudo \
95 | -cover \
96 | -v ./... \
97 | -run TestFwdctl/version$ \
98 | -args -test.gocoverdir=/tmp/integration/
99 |
100 | - name: Upload cover profiles
101 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # ratchet:actions/upload-artifact@v4
102 | with:
103 | name: integration-test
104 | path: /tmp/integration/
105 |
106 | code-coverage:
107 |
108 | runs-on: ubuntu-latest
109 | needs: [unit-test,integration-test]
110 | steps:
111 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4
112 |
113 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # ratchet:actions/download-artifact@v4
114 | with:
115 | name: unit-test
116 | path: /tmp/unit-test
117 |
118 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # ratchet:actions/download-artifact@v4
119 | with:
120 | name: integration-test
121 | path: /tmp/integration-test
122 |
123 | - name: list files
124 | run: |
125 | ls -lah /tmp/unit-test
126 | ls -lah /tmp/integration-test
127 |
128 | - name: Set up Go
129 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # ratchet:actions/setup-go@v5
130 | with:
131 | go-version: '1.22'
132 |
133 | - name: Calculate total coverage
134 | run: |
135 | go tool \
136 | covdata \
137 | textfmt \
138 | -i=/tmp/unit-test,/tmp/integration-test \
139 | -o code-coverage
140 | go tool \
141 | cover \
142 | -func code-coverage
143 |
144 | - name: Update coverage report
145 | uses: ncruces/go-coverage-report@494b2847891f4dd3b10f6704ca533367dbb7493d # ratchet:ncruces/go-coverage-report@v0
146 | with:
147 | report: true
148 | chart: true
149 | amend: true
150 | coverage-file: ./code-coverage
151 |
152 | trace-unit-test:
153 |
154 | runs-on: ubuntu-latest
155 | needs: [unit-test]
156 | steps:
157 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
158 |
159 | - name: Set up Go
160 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # ratchet:actions/setup-go@v5
161 | with:
162 | go-version: '1.22'
163 |
164 | - name: Build coverage-instrumented binary
165 | run: |
166 | make build-cover && sudo make -B install
167 |
168 | - name: Install harpoon
169 | run: |
170 | curl -s https://raw.githubusercontent.com/alegrey91/harpoon/main/install | sudo bash -s -- --install-version v0.9.3
171 |
172 | - name: Analyze binaries
173 | run: |
174 | sudo harpoon analyze \
175 | --exclude .git/ \
176 | --save
177 |
178 | - name: Trace system calls
179 | run: |
180 | sudo harpoon hunt \
181 | --file harpoon-report.yml \
182 | --directory unit-test-syscalls \
183 | --save
184 |
185 | - name: Upload metadata from unit-tests
186 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # ratchet:actions/upload-artifact@v4
187 | with:
188 | name: unit-test-syscalls
189 | path: unit-test-syscalls
190 |
191 | trace-integration-test:
192 |
193 | runs-on: ubuntu-latest
194 | needs: [integration-test]
195 | steps:
196 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
197 |
198 | - name: Set up Go
199 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # ratchet:actions/setup-go@v5
200 | with:
201 | go-version: '1.22'
202 |
203 | - name: Install iptables
204 | run: |
205 | sudo apt update
206 | sudo apt install -y iptables
207 |
208 | - name: Build coverage-instrumented binary
209 | run: |
210 | make build && sudo make -B install
211 |
212 | - name: Install harpoon
213 | run: |
214 | curl -s https://raw.githubusercontent.com/alegrey91/harpoon/main/install | sudo bash -s -- --install-version v0.9.3
215 |
216 | - name: Run integration test
217 | run: |
218 | mkdir -p integration-test-syscalls
219 | go test \
220 | -exec sudo \
221 | -v ./... \
222 | -run TestFwdctl/apply_trace
223 | go test \
224 | -exec sudo \
225 | -v ./... \
226 | -run TestFwdctl/create_trace
227 | go test \
228 | -exec sudo \
229 | -v ./... \
230 | -run TestFwdctl/delete_trace
231 | go test \
232 | -exec sudo \
233 | -v ./... \
234 | -run TestFwdctl/generate_trace
235 | go test \
236 | -exec sudo \
237 | -v ./... \
238 | -run TestFwdctl/list_trace
239 | go test \
240 | -exec sudo \
241 | -v ./... \
242 | -run TestFwdctl/version_trace
243 | shell: bash
244 |
245 | - name: Upload cover profiles
246 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # ratchet:actions/upload-artifact@v4
247 | with:
248 | name: integration-test-syscalls
249 | path: integration-test-syscalls
250 |
251 | build-seccomp-profile:
252 |
253 | runs-on: ubuntu-latest
254 | needs: [trace-unit-test, trace-integration-test]
255 | steps:
256 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4
257 |
258 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # ratchet:actions/download-artifact@v4
259 | with:
260 | name: unit-test-syscalls
261 | path: unit-test-syscalls
262 |
263 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # ratchet:actions/download-artifact@v4
264 | with:
265 | name: integration-test-syscalls
266 | path: ./integration-test-syscalls
267 |
268 | - name: list files
269 | run: |
270 | ls -lah ./unit-test-syscalls
271 | ls -lah ./integration-test-syscalls
272 |
273 | - name: Set up Go
274 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # ratchet:actions/setup-go@v5
275 | with:
276 | go-version: '1.22'
277 |
278 | - name: Install harpoon
279 | run: |
280 | curl -s https://raw.githubusercontent.com/alegrey91/harpoon/main/install | sudo bash -s -- --install-version v0.9.3
281 |
282 | - name: Create unique directory
283 | run: |
284 | mkdir -p harpoon
285 | mv unit-test-syscalls/* harpoon/
286 | mv integration-test-syscalls/* harpoon/
287 |
288 | - name: Build Seccomp Profile
289 | run: |
290 | sudo harpoon build \
291 | --add-syscall-sets=dynamic,docker \
292 | --directory harpoon/ \
293 | --save \
294 | --name fwdctl-seccomp.json
295 |
296 | - name: Upload seccomp profile
297 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # ratchet:actions/upload-artifact@v4
298 | with:
299 | name: fwdctl-seccomp.json
300 | path: fwdctl-seccomp.json
301 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | dist/
3 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/gitleaks/gitleaks
3 | rev: v8.16.3
4 | hooks:
5 | - id: gitleaks
6 | - repo: https://github.com/golangci/golangci-lint
7 | rev: v1.52.2
8 | hooks:
9 | - id: golangci-lint
10 | - repo: https://github.com/jumanjihouse/pre-commit-hooks
11 | rev: 3.0.0
12 | hooks:
13 | - id: shellcheck
14 | - repo: https://github.com/pre-commit/pre-commit-hooks
15 | rev: v4.4.0
16 | hooks:
17 | - id: end-of-file-fixer
18 | - id: trailing-whitespace
19 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Exec with argument (apply)",
9 | "type": "go",
10 | "request": "launch",
11 | "mode": "exec",
12 | "asRoot": true,
13 | "console": "integratedTerminal",
14 | "args": [
15 | "apply"
16 | ],
17 | "program": "${workspaceFolder}/bin/fwdctl",
18 | "showLog": true,
19 | }
20 |
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BINARY_NAME=fwdctl
2 | BINARY_DIR=./bin
3 |
4 | build: create-bin-dir
5 | go mod download
6 | go build \
7 | -v \
8 | -o ${BINARY_DIR}/${BINARY_NAME} \
9 | .
10 |
11 | build-cover: create-bin-dir
12 | go mod download
13 | go build \
14 | -v \
15 | -cover \
16 | -o ${BINARY_DIR}/${BINARY_NAME} \
17 | .
18 |
19 | build-gh: create-bin-dir
20 | ifndef GITHUB_REF_NAME
21 | $(error GITHUB_REF_NAME is undefined)
22 | endif
23 | go mod download
24 | go build \
25 | -v \
26 | -ldflags='-s -w -X "github.com/alegrey91/fwdctl/internal/constants.Version=${GITHUB_REF_NAME}"' \
27 | -o ${BINARY_DIR}/${BINARY_NAME} \
28 | .
29 |
30 |
31 | install:
32 | cp ${BINARY_DIR}/${BINARY_NAME} /usr/local/bin/
33 |
34 | create-bin-dir:
35 | mkdir -p ${BINARY_DIR}
36 |
37 | clean:
38 | rm -rf ${BINARY_DIR}
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fwdctl
2 |
3 |
4 |
5 |
6 |
7 |
8 | [](https://pkg.go.dev/github.com/alegrey91/fwdctl)
9 | [](https://goreportcard.com/report/github.com/alegrey91/fwdctl)
10 | [](https://raw.githack.com/wiki/alegrey91/fwdctl/coverage.html)
11 | 
12 | [](https://github.com/avelino/awesome-go/)
13 |
14 | **fwdctl** is a simple and intuitive CLI to manage forwards in your **Linux** server.
15 |
16 | ## How it works
17 |
18 | It essentially provides commands to manage forwards, using **iptables** under the hood.
19 |
20 | Let's do an example:
21 |
22 | Suppose you have an **hypervisor** server that hosts some virtual machines inside itself. If you need to expose an internal service, managed by one of these VMs, you can use **fwdctl** from the hypervisor to add the forward to expose this service.
23 |
24 | 
25 |
26 | To do so, you have to type this easy command:
27 |
28 | ``` shell
29 | sudo fwdctl create --destination-port 3000 --source-address 192.168.199.105 --source-port 80
30 | ```
31 |
32 | That's it.
33 |
34 | Full **documentation** [here](docs/getting-started.md).
35 |
36 | ## Installation
37 |
38 | #### Linux x86_64
39 |
40 | ```shell
41 | curl -s https://raw.githubusercontent.com/alegrey91/fwdctl/main/install | sudo sh
42 | ```
43 |
44 | ## Seccomp (experimental)
45 |
46 | I've recently added a new functionality to trace the system calls used by `fwdctl` during the test pipeline.
47 |
48 | This is done by using another project of mine: [`harpoon`](https://github.com/alegrey91/harpoon).
49 |
50 | Thanks to this, at the end of the pipeline, we have a **seccomp** profile as artifact. You can use this to run `fwdctl` in a more secure way.
51 |
52 | Find the **seccomp** profile here: [`fwdctl-seccomp.json`](hack/fwdctl-seccomp.json).
53 |
--------------------------------------------------------------------------------
/cmd/apply.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 | "os"
21 |
22 | "github.com/spf13/cobra"
23 | "golang.org/x/sync/errgroup"
24 |
25 | c "github.com/alegrey91/fwdctl/internal/constants"
26 | "github.com/alegrey91/fwdctl/internal/rules"
27 | iptables "github.com/alegrey91/fwdctl/pkg/iptables"
28 | )
29 |
30 | // applyCmd represents the apply command
31 | var applyCmd = &cobra.Command{
32 | Use: "apply",
33 | Short: "Apply rules from file",
34 | Long: `Apply rules described in a configuration file`,
35 | Example: c.ProgramName + " apply --file rule.yml",
36 | RunE: func(cmd *cobra.Command, args []string) error {
37 | rulesContent, err := os.Open(c.RulesFile)
38 | if err != nil {
39 | return fmt.Errorf("opening file: %v", err)
40 | }
41 | ruleSet, err := rules.NewRuleSetFromFile(rulesContent)
42 | if err != nil {
43 | return fmt.Errorf("unable to open rules file: %v", err)
44 | }
45 | ipt, err := iptables.NewIPTablesInstance()
46 | if err != nil {
47 | return fmt.Errorf("unable to get iptables instance: %v", err)
48 | }
49 |
50 | g := new(errgroup.Group)
51 | rulesFileIsValid := true
52 |
53 | g.SetLimit(10)
54 | for _, rule := range ruleSet.Rules {
55 | r := &rule
56 | g.Go(func() error {
57 | err := ipt.ValidateForward(r)
58 | if err != nil {
59 | rulesFileIsValid = false
60 | }
61 | return err
62 | })
63 | }
64 |
65 | if err := g.Wait(); err != nil {
66 | return fmt.Errorf("validating rule: %v", err)
67 | }
68 |
69 | if rulesFileIsValid {
70 | for ruleId, rule := range ruleSet.Rules {
71 | if err := ipt.CreateForward(&rule); err != nil {
72 | return fmt.Errorf("applying rule (%s): %v", ruleId, err)
73 | }
74 | }
75 | }
76 | return nil
77 | },
78 | }
79 |
80 | func init() {
81 | rootCmd.AddCommand(applyCmd)
82 |
83 | applyCmd.Flags().StringVarP(&c.RulesFile, "file", "f", "rules.yml", "rules file")
84 | }
85 |
--------------------------------------------------------------------------------
/cmd/create.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 |
21 | c "github.com/alegrey91/fwdctl/internal/constants"
22 | iptables "github.com/alegrey91/fwdctl/pkg/iptables"
23 | "github.com/spf13/cobra"
24 | )
25 |
26 | var (
27 | iface string
28 | proto string
29 | dport int
30 | saddr string
31 | sport int
32 | )
33 |
34 | // createCmd represents the create command
35 | var createCmd = &cobra.Command{
36 | Use: "create",
37 | Aliases: []string{"add"},
38 | SuggestFor: []string{},
39 | Short: "Create forward using IPTables util",
40 | Long: `Create forward rule using IPTables util under the hood.
41 | This is really useful in case you need to forward
42 | the traffic from an internal virtual machine inside
43 | your hypervisor, to external.
44 |
45 | +----------------------------+
46 | | +-----------+ |
47 | | | | |
48 | | +-----+:80 VM | |
49 | | | | | |
50 | =:3000<--+ +-----------+ |
51 | | Hypervisor |
52 | +----------------------------+
53 | `,
54 | Example: c.ProgramName + " create -d 3000 -s 192.168.199.105 -p 80",
55 | RunE: func(cmd *cobra.Command, args []string) error {
56 | ipt, err := iptables.NewIPTablesInstance()
57 | if err != nil {
58 | return fmt.Errorf("unable to get iptables instance: %v", err)
59 | }
60 | rule := iptables.NewRule(iface, proto, dport, saddr, sport)
61 | if err := ipt.CreateForward(rule); err != nil{
62 | return fmt.Errorf("creating new rule: %v", err)
63 | }
64 | return nil
65 | },
66 | }
67 |
68 | func init() {
69 | rootCmd.AddCommand(createCmd)
70 |
71 | createCmd.Flags().StringVarP(&iface, "interface", "i", "lo", "interface name")
72 | createCmd.Flags().StringVarP(&proto, "proto", "P", "tcp", "protocol")
73 |
74 | createCmd.Flags().IntVarP(&dport, "destination-port", "d", 0, "destination port")
75 | _ = createCmd.MarkFlagRequired("destination-port")
76 |
77 | createCmd.Flags().StringVarP(&saddr, "source-address", "s", "", "source address")
78 | _ = createCmd.MarkFlagRequired("source-address")
79 |
80 | createCmd.Flags().IntVarP(&sport, "source-port", "p", 0, "source port")
81 | _ = createCmd.MarkFlagRequired("source-port")
82 | }
83 |
--------------------------------------------------------------------------------
/cmd/daemon.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2023 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "github.com/spf13/cobra"
20 | )
21 |
22 | // daemonCmd represents the daemon command
23 | var daemonCmd = &cobra.Command{
24 | Use: "daemon",
25 | Short: "Run fwdctl as deamon",
26 | Run: func(cmd *cobra.Command, args []string) {
27 | _ = cmd.Help()
28 | },
29 | }
30 |
31 | func init() {
32 | rootCmd.AddCommand(daemonCmd)
33 | }
34 |
--------------------------------------------------------------------------------
/cmd/daemon_start.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 | "os"
21 |
22 | c "github.com/alegrey91/fwdctl/internal/constants"
23 | "github.com/alegrey91/fwdctl/internal/daemon"
24 | iptables "github.com/alegrey91/fwdctl/pkg/iptables"
25 | "github.com/spf13/cobra"
26 | )
27 |
28 | // daemonStartCmd represents the daemon command
29 | var daemonStartCmd = &cobra.Command{
30 | Use: "start",
31 | Short: "Start fwdctl daemon",
32 | Long: ``,
33 | RunE: func(cmd *cobra.Command, args []string) error{
34 | ipt, err := iptables.NewIPTablesInstance()
35 | if err != nil {
36 | return fmt.Errorf("unable to get iptables instance: %v", err)
37 | }
38 | rulesFile, err := cmd.Flags().GetString("file")
39 | if err != nil {
40 | return fmt.Errorf("unable to read from flag: %v", err)
41 | }
42 | if res := daemon.Start(ipt, rulesFile); res != 0 {
43 | os.Exit(1)
44 | }
45 | return nil
46 | },
47 | }
48 |
49 | func init() {
50 | daemonCmd.AddCommand(daemonStartCmd)
51 | daemonStartCmd.Flags().StringVarP(&c.RulesFile, "file", "f", "rules.yml", "rules file")
52 | }
53 |
--------------------------------------------------------------------------------
/cmd/daemon_stop.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 |
21 | "github.com/alegrey91/fwdctl/internal/daemon"
22 | "github.com/spf13/cobra"
23 | )
24 |
25 | // daemonStopCmd represents the daemon command
26 | var daemonStopCmd = &cobra.Command{
27 | Use: "stop",
28 | Short: "Stop fwdctl daemon",
29 | Long: ``,
30 | Run: func(cmd *cobra.Command, args []string) {
31 | fmt.Println("stopping daemon")
32 | daemon.Stop()
33 | },
34 | }
35 |
36 | func init() {
37 | daemonCmd.AddCommand(daemonStopCmd)
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/cmd/delete.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 | "os"
21 |
22 | //"os"
23 |
24 | c "github.com/alegrey91/fwdctl/internal/constants"
25 | "github.com/alegrey91/fwdctl/internal/rules"
26 | iptables "github.com/alegrey91/fwdctl/pkg/iptables"
27 | "github.com/spf13/cobra"
28 | )
29 |
30 | var (
31 | ruleId int
32 | file string
33 | )
34 |
35 | // deleteCmd represents the delete command
36 | var deleteCmd = &cobra.Command{
37 | Use: "delete",
38 | Aliases: []string{"rm"},
39 | Short: "Delete forward",
40 | Long: `Delete forward by passing a rule file or rule id.
41 | `,
42 | Example: c.ProgramName + " delete -n 2",
43 | RunE: func(cmd *cobra.Command, args []string) error {
44 | ipt, err := iptables.NewIPTablesInstance()
45 | if err != nil {
46 | return fmt.Errorf("unable to get iptables instance: %v", err)
47 | }
48 | // Delete rule number
49 | if cmd.Flags().Lookup("id").Changed {
50 | if err := ipt.DeleteForwardById(ruleId); err != nil {
51 | return fmt.Errorf("delete forward by ID: %v", err)
52 | }
53 | return nil
54 | }
55 |
56 | // Loop over file content and delete rule one-by-one.
57 | if cmd.Flags().Lookup("file").Changed {
58 | if err := deleteFromFile(ipt, file); err != nil {
59 | return fmt.Errorf("delete from file: %v", err)
60 | }
61 | return nil
62 | }
63 |
64 | if cmd.Flags().Lookup("all").Changed {
65 | if err := ipt.DeleteAllForwards();err != nil {
66 | return fmt.Errorf("delete all forwards: %v", err)
67 | }
68 | return nil
69 | }
70 |
71 | if err = deleteFromFile(ipt, file);err != nil {
72 | return fmt.Errorf("delete from file: %v", err)
73 | }
74 | return nil
75 | },
76 | }
77 |
78 | func init() {
79 | rootCmd.AddCommand(deleteCmd)
80 |
81 | deleteCmd.Flags().IntVarP(&ruleId, "id", "n", 0, "delete rules through ID")
82 | deleteCmd.Flags().StringVarP(&file, "file", "f", "rules.yml", "delete rules through file")
83 | deleteCmd.Flags().BoolP("all", "a", false, "delete all rules")
84 | deleteCmd.MarkFlagsMutuallyExclusive("id", "file", "all")
85 | }
86 |
87 | func deleteFromFile(ipt *iptables.IPTablesInstance, file string) error {
88 | rulesContent, err := os.Open(file)
89 | if err != nil {
90 | return fmt.Errorf("error opening file: %v", err)
91 | }
92 | rulesFile, err := rules.NewRuleSetFromFile(rulesContent)
93 | if err != nil {
94 | return fmt.Errorf("error instantiating ruleset from file: %v", err)
95 | }
96 | for _, rule := range rulesFile.Rules {
97 | err := ipt.DeleteForwardByRule(&rule)
98 | if err != nil {
99 | return fmt.Errorf("error deleting rule [%s %s %d %s %d]: %v", rule.Iface, rule.Proto, rule.Dport, rule.Saddr, rule.Sport, err)
100 | }
101 | }
102 | return nil
103 | }
104 |
--------------------------------------------------------------------------------
/cmd/generate.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 |
21 | "github.com/spf13/cobra"
22 | )
23 |
24 | var outputFile string
25 |
26 | // generateCmd represents the generate command
27 | var generateCmd = &cobra.Command{
28 | Use: "generate",
29 | Aliases: []string{"gen"},
30 | Short: "Generates templated files",
31 | Long: `Generates templated file for fwdtcl
32 | `,
33 | RunE: func(cmd *cobra.Command, args []string) error {
34 | fmt.Println(cmd.Help())
35 | return nil
36 | },
37 | }
38 |
39 | func init() {
40 | rootCmd.AddCommand(generateCmd)
41 | }
42 |
--------------------------------------------------------------------------------
/cmd/generate_rules.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 |
21 | "github.com/spf13/cobra"
22 |
23 | "github.com/alegrey91/fwdctl/internal/template"
24 | rt "github.com/alegrey91/fwdctl/internal/template/rules_template"
25 | )
26 |
27 | // generateRulesCmd represents the generateRules command
28 | var generateRulesCmd = &cobra.Command{
29 | Use: "rules",
30 | Short: "Generates empty rules file",
31 | Long: `Generates empty rules file
32 | `,
33 | RunE: func(cmd *cobra.Command, args []string) error {
34 | rules := rt.NewRules()
35 | if err := template.GenerateTemplate(rules, outputFile); err != nil {
36 | return fmt.Errorf("generating template: %w", err)
37 | }
38 | return nil
39 | },
40 | }
41 |
42 | func init() {
43 | generateCmd.AddCommand(generateRulesCmd)
44 |
45 | generateRulesCmd.PersistentFlags().StringVarP(&outputFile, "output-path", "O", "", "output path")
46 | _ = generateRulesCmd.MarkPersistentFlagRequired("output-path")
47 | }
48 |
--------------------------------------------------------------------------------
/cmd/generate_systemd.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 |
21 | c "github.com/alegrey91/fwdctl/internal/constants"
22 | "github.com/alegrey91/fwdctl/internal/template"
23 | st "github.com/alegrey91/fwdctl/internal/template/systemd_template"
24 | "github.com/spf13/cobra"
25 | )
26 |
27 | var installationPath string
28 | var serviceType string
29 |
30 | // generateSystemdCmd represents the generateSystemd command
31 | var generateSystemdCmd = &cobra.Command{
32 | Use: "systemd",
33 | Short: "Generates systemd service file",
34 | Long: `Generates systemd service file to run fwdctl at boot
35 | `,
36 | RunE: func(cmd *cobra.Command, args []string) error {
37 | systemd, err := st.NewSystemdService(serviceType, installationPath, c.RulesFile)
38 | if err != nil {
39 | return fmt.Errorf("cannot create systemd service: %v", err)
40 | }
41 |
42 | if err = template.GenerateTemplate(systemd, outputFile); err != nil {
43 | return fmt.Errorf("generating templated file: %v", err)
44 | }
45 | return nil
46 | },
47 | }
48 |
49 | func init() {
50 | generateCmd.AddCommand(generateSystemdCmd)
51 |
52 | generateSystemdCmd.Flags().StringVarP(&installationPath, "installation-path", "p", "/usr/local/bin", "fwdctl installation path")
53 | generateSystemdCmd.Flags().StringVarP(&c.RulesFile, "file", "f", "rules.yml", "rules file path")
54 | generateSystemdCmd.Flags().StringVarP(&serviceType, "type", "t", "oneshot", "systemd service type [oneshot, fork]")
55 |
56 | generateSystemdCmd.PersistentFlags().StringVarP(&outputFile, "output-path", "O", "", "output path")
57 | _ = generateSystemdCmd.MarkPersistentFlagRequired("output-path")
58 | }
59 |
--------------------------------------------------------------------------------
/cmd/list.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 |
21 | c "github.com/alegrey91/fwdctl/internal/constants"
22 | "github.com/alegrey91/fwdctl/internal/printer"
23 | iptables "github.com/alegrey91/fwdctl/pkg/iptables"
24 | "github.com/spf13/cobra"
25 | )
26 |
27 | var (
28 | format string
29 | )
30 |
31 | // listCmd represents the list command
32 | var listCmd = &cobra.Command{
33 | Use: "list",
34 | Aliases: []string{"ls"},
35 | Short: "List forwards",
36 | Long: `List forwards made with iptables`,
37 | Example: c.ProgramName + "list -o table",
38 | RunE: func(cmd *cobra.Command, args []string) error{
39 | ipt, err := iptables.NewIPTablesInstance()
40 | if err != nil {
41 | return fmt.Errorf("getting iptables instance: %v", err)
42 | }
43 | ruleList, err := ipt.ListForward(format)
44 | if err != nil {
45 | return fmt.Errorf("listing rules: %v", err)
46 | }
47 |
48 | p := printer.NewPrinter(format)
49 | if err = p.PrintResult(ruleList);err != nil {
50 | return fmt.Errorf("printing result: %v", err)
51 | }
52 | return nil
53 | },
54 | }
55 |
56 | func init() {
57 | rootCmd.AddCommand(listCmd)
58 |
59 | listCmd.Flags().StringVarP(&format, "output", "o", "table", "output format [table]")
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 | "os"
21 |
22 | "github.com/spf13/cobra"
23 | )
24 |
25 | // rootCmd represents the base command when called without any subcommands
26 | var rootCmd = &cobra.Command{
27 | Use: "fwdctl",
28 | Short: "fwdctl is a simple and intuitive CLI to manage IPTables forwards",
29 | Long: ``,
30 | // Uncomment the following line if your bare application
31 | // has an action associated with it:
32 | // Run: func(cmd *cobra.Command, args []string) { },
33 | }
34 |
35 | // Execute adds all child commands to the root command and sets flags appropriately.
36 | // This is called by main.main(). It only needs to happen once to the rootCmd.
37 | func Execute() {
38 | if err := rootCmd.Execute(); err != nil {
39 | fmt.Println(err)
40 | os.Exit(1)
41 | }
42 | }
43 |
44 | func init() {
45 | cobra.OnInitialize(initConfig)
46 | }
47 |
48 | // initConfig reads in config file and ENV variables if set.
49 | func initConfig() {
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "fmt"
20 |
21 | c "github.com/alegrey91/fwdctl/internal/constants"
22 | "github.com/spf13/cobra"
23 | )
24 |
25 | // versionCmd represents the version command
26 | var versionCmd = &cobra.Command{
27 | Use: "version",
28 | Short: "A brief description of your command",
29 | Long: `A longer description that spans multiple lines and likely contains examples
30 | and usage of using your command. For example:
31 |
32 | Cobra is a CLI library for Go that empowers applications.
33 | This application is a tool to generate the needed files
34 | to quickly create a Cobra application.`,
35 | Run: func(cmd *cobra.Command, args []string) {
36 | fmt.Printf("%s\n", c.Version)
37 | },
38 | }
39 |
40 | func init() {
41 | rootCmd.AddCommand(versionCmd)
42 | }
43 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | **fwdctl** has many commands to manage your forwards. Let's dive into them!
4 |
5 | ### Apply
6 |
7 | The `apply` command is used to apply rules from a rules file. Here's a rules file example.
8 |
9 | ```shell
10 | cat << EOF > rules.yml
11 | rules:
12 | - dport: 2022
13 | saddr: 192.168.122.43
14 | sport: 22
15 | iface: eth1
16 | proto: tcp
17 | - dport: 3022
18 | saddr: 192.168.122.44
19 | sport: 22
20 | iface: eth1
21 | proto: tcp
22 | EOF
23 | ```
24 |
25 | Once created, you can easily run:
26 |
27 | ```shell
28 | sudo fwdctl apply --file rules.yml
29 | ```
30 |
31 | and apply all the rules listed in the file.
32 |
33 | ### Create
34 |
35 | The `create` command is used to create single rules manually. Let's see an example.
36 |
37 | 
38 |
39 | To implement the rule for this scenario, just type the following command:
40 |
41 | ```shell
42 | sudo fwdctl create --destination-port 3000 --source-address 192.168.199.105 --source-port 80
43 | ```
44 |
45 | ### Daemon
46 |
47 | The `daemon` command is used to run `fwdctl` as service.
48 |
49 | When modifications are applied to the rules file, `fwdctl` is triggered and start to apply changes that have been requested.
50 |
51 | To start the daemon, run the following command:
52 |
53 | ```shell
54 | sudo fwdctl daemon start -f rules.yaml
55 | ```
56 |
57 | When you want to stop the daemon execution, then type:
58 |
59 | ```shell
60 | sudo fwdctl daemon stop
61 | ```
62 |
63 | This will remove all the forwards that have been applied during its execution.
64 |
65 | Here's a demo on how to use this command:
66 |
67 | [](https://asciinema.org/a/553296)
68 |
69 | ### Delete
70 |
71 | The `delete` command is used to delete rules using their ID or a file where the rules are listed.
72 |
73 | To delete a specific rule (identified with a number), type the following command:
74 |
75 | ```shell
76 | sudo fwdctl delete --id 4
77 | ```
78 |
79 | This will remove the rule n. **4**, listed from **iptables**.
80 |
81 | To delete a set of rules listed in a *rule file*, type the following command:
82 |
83 | ```shell
84 | sudo fwdctl delete --file rules.yaml
85 | ```
86 |
87 | This will remove the listed rules within the file.
88 |
89 | ### List
90 |
91 | The `list` command is used to list the applied rules.
92 |
93 | To list the rules, type the following command:
94 |
95 | ```shell
96 | sudo fwdctl list
97 | ```
98 |
99 | The output will look like this:
100 |
101 | ```shell
102 | +--------+-----------+----------+---------------+----------------+---------------+
103 | | NUMBER | INTERFACE | PROTOCOL | EXTERNAL PORT | INTERNAL IP | INTERNAL PORT |
104 | +--------+-----------+----------+---------------+----------------+---------------+
105 | | 1 | lo | tcp | 2022 | 192.168.122.43 | 22 |
106 | | 2 | lo | tcp | 3022 | 192.168.122.44 | 443 |
107 | +--------+-----------+----------+---------------+----------------+---------------+
108 | ```
109 |
110 | If you want to use a different view of applied rules, you can choose between different format:
111 |
112 | ```shell
113 | sudo fwdctl list --output json
114 | ```
115 |
116 | ### Generate
117 |
118 | The `generate` command is used to generate the following files:
119 |
120 | * **systemd** service for `fwdctl`
121 | * rules empty file
122 |
123 | To generate a systemd service, type the following command:
124 |
125 | ```shell
126 | fwdctl generate systemd -o fwdctl.service
127 | ```
128 |
129 | To generate a `fwdctl` rules file, instead, type the following command:
130 |
131 | ```shell
132 | fwdctl generate rules -o rules.yml
133 | ```
134 |
135 | ### Version
136 |
137 | The `version` command is used show the version of the program.
138 |
--------------------------------------------------------------------------------
/fwdctl-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alegrey91/fwdctl/b69fc55d544dcc052280994704240b7e00574c8e/fwdctl-example.png
--------------------------------------------------------------------------------
/fwdctl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alegrey91/fwdctl/b69fc55d544dcc052280994704240b7e00574c8e/fwdctl.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/alegrey91/fwdctl
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/coreos/go-iptables v0.6.1-0.20220901214115-d2b8608923d1
7 | github.com/fsnotify/fsnotify v1.7.0
8 | github.com/spf13/cobra v1.5.0
9 | golang.org/x/sync v0.8.0
10 | gopkg.in/yaml.v3 v3.0.1
11 | )
12 |
13 | require (
14 | github.com/hashicorp/hcl v1.0.0 // indirect
15 | github.com/magiconair/properties v1.8.7 // indirect
16 | github.com/mattn/go-runewidth v0.0.14 // indirect
17 | github.com/mitchellh/mapstructure v1.5.0 // indirect
18 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
19 | github.com/rivo/uniseg v0.4.4 // indirect
20 | github.com/sagikazarmark/locafero v0.4.0 // indirect
21 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect
22 | github.com/sourcegraph/conc v0.3.0 // indirect
23 | github.com/spf13/afero v1.11.0 // indirect
24 | github.com/spf13/cast v1.6.0 // indirect
25 | github.com/subosito/gotenv v1.6.0 // indirect
26 | go.uber.org/atomic v1.9.0 // indirect
27 | go.uber.org/multierr v1.9.0 // indirect
28 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
29 | golang.org/x/sys v0.18.0 // indirect
30 | golang.org/x/text v0.14.0 // indirect
31 | golang.org/x/tools v0.18.0 // indirect
32 | gopkg.in/ini.v1 v1.67.0 // indirect
33 | )
34 |
35 | require (
36 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
37 | github.com/olekukonko/tablewriter v0.0.5
38 | github.com/rogpeppe/go-internal v1.12.0
39 | github.com/spf13/pflag v1.0.5 // indirect
40 | github.com/spf13/viper v1.19.0
41 | gopkg.in/yaml.v2 v2.4.0
42 | )
43 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/coreos/go-iptables v0.6.1-0.20220901214115-d2b8608923d1 h1:zSiUKnogKeEwIIeUQP/WPH7m0BJ/IvW0VyL4muaauUY=
2 | github.com/coreos/go-iptables v0.6.1-0.20220901214115-d2b8608923d1/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
3 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
9 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
10 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
11 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
12 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
13 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
14 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
15 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
16 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
17 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
18 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
19 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
22 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
23 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
24 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
25 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
26 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
27 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
28 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
29 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
30 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
31 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
32 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
34 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
35 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
36 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
37 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
38 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
39 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
40 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
41 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
42 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
43 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
44 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
45 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
46 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
47 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
48 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
49 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
50 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
51 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
52 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
53 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
54 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
55 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
56 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
57 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
59 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
60 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
61 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
62 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
63 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
64 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
65 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
66 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
67 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
68 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
69 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
70 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
71 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
72 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
73 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
74 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
75 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
76 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
77 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
78 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
79 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
80 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
81 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
82 | golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
83 | golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
85 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
86 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
87 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
88 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
89 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
90 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
91 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
92 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
93 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
94 |
--------------------------------------------------------------------------------
/hack/fwdctl-seccomp.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultAction": "SCMP_ACT_ERRNO",
3 | "architectures": [
4 | "SCMP_ARCH_X86_64",
5 | "SCMP_ARCH_X86",
6 | "SCMP_ARCH_X32"
7 | ],
8 | "syscalls": [
9 | {
10 | "names": [
11 | "access",
12 | "arch_prctl",
13 | "bind",
14 | "brk",
15 | "capget",
16 | "capset",
17 | "chdir",
18 | "clone",
19 | "clone3",
20 | "close",
21 | "dup3",
22 | "epoll_create1",
23 | "epoll_ctl",
24 | "epoll_pwait",
25 | "eventfd",
26 | "eventfd2",
27 | "execve",
28 | "exit_group",
29 | "faccessat2",
30 | "fchown",
31 | "fcntl",
32 | "fstat",
33 | "fstatfs",
34 | "futex",
35 | "getcwd",
36 | "getdents64",
37 | "geteuid",
38 | "getpgrp",
39 | "getpid",
40 | "getppid",
41 | "getrandom",
42 | "getrlimit",
43 | "getsockname",
44 | "gettid",
45 | "ioctl",
46 | "lseek",
47 | "lstat",
48 | "madvise",
49 | "mmap",
50 | "mprotect",
51 | "munmap",
52 | "nanosleep",
53 | "newfstatat",
54 | "openat",
55 | "pipe2",
56 | "prctl",
57 | "pread64",
58 | "prlimit64",
59 | "read",
60 | "recvfrom",
61 | "rseq",
62 | "rt_sigaction",
63 | "rt_sigprocmask",
64 | "rt_sigreturn",
65 | "sched_getaffinity",
66 | "sendto",
67 | "set_robust_list",
68 | "set_tid_address",
69 | "setgid",
70 | "setgroups",
71 | "setrlimit",
72 | "setsid",
73 | "setuid",
74 | "sigaltstack",
75 | "socket",
76 | "stat",
77 | "tgkill",
78 | "wait4",
79 | "waitid",
80 | "write"
81 | ],
82 | "action": "SCMP_ACT_ALLOW"
83 | }
84 | ]
85 | }
86 |
--------------------------------------------------------------------------------
/install:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | BINARY_NAME="fwdctl"
6 | KERNEL_NAME=$(uname -s)
7 | MACHINE_HW_NAME=$(uname -m)
8 | BINARY_RELEASE_NAME="${BINARY_NAME}"
9 | CHECKSUMS="checksums.txt"
10 |
11 | cd /tmp
12 |
13 | # download binary + checksum file
14 | curl -s https://api.github.com/repos/alegrey91/fwdctl/releases/latest | \
15 | grep "browser_download_url" | \
16 | cut -d : -f 2,3 | \
17 | tr -d \" | \
18 | wget -q -i -
19 |
20 | # check if binary has been downloaded, otherwise exit
21 | if [ ! -f "$BINARY_RELEASE_NAME" ]; then
22 | echo "$BINARY_RELEASE_NAME doesn't exists."
23 | exit 1
24 | fi
25 | printf "[download succeded]\n"
26 |
27 | # install binary
28 | chmod +x "$BINARY_RELEASE_NAME"
29 | mv "$BINARY_RELEASE_NAME" "/usr/local/bin/$BINARY_NAME"
30 | printf "[installation succeded]\n"
31 |
32 | # clean up
33 | rm -rf "$BINARY_NAME"* "$CHECKSUMS"*
34 | printf "[clean up succeded]\n"
35 |
36 |
--------------------------------------------------------------------------------
/internal/constants/constants.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | var (
4 | Version = "dev"
5 | ProgramName = "fwdctl"
6 | RulesFile string
7 | )
8 |
--------------------------------------------------------------------------------
/internal/daemon/management.go:
--------------------------------------------------------------------------------
1 | package daemon
2 |
3 | import (
4 | "log"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 |
9 | "github.com/alegrey91/fwdctl/internal/rules"
10 | "github.com/alegrey91/fwdctl/pkg/iptables"
11 | "github.com/fsnotify/fsnotify"
12 | "github.com/spf13/viper"
13 | )
14 |
15 | var (
16 | infoLogger *log.Logger
17 | errorLogger *log.Logger
18 | )
19 |
20 | func init() {
21 | // Initialize different loggers
22 | infoLogger = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
23 | errorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
24 | }
25 |
26 | // Print daemon banner
27 | func banner() string {
28 | return `
29 | ┌─┐┬ ┬┌┬┐┌─┐┌┬┐┬
30 | ├┤ │││ │││ │ │
31 | └ └┴┘─┴┘└─┘ ┴ ┴─┘
32 | ┌┬┐┌─┐┌─┐┌┬┐┌─┐┌┐┌
33 | ││├─┤├┤ ││││ ││││
34 | ─┴┘┴ ┴└─┘┴ ┴└─┘┘└┘`
35 | }
36 |
37 | // Start run fwdctl in daemon mode
38 | // The flow has the following steps:
39 | // - Open the rules file
40 | // - Create all the defined forwards
41 | // - Start listening on rules file changes
42 | // - When file changes:
43 | // - Calculate the Diff between old and new ruleset
44 | // - Delete unwanted forwards
45 | // - Create wanted forawrds
46 | //
47 | // - Listen for SIGTERM signals to gracefuly shutdown
48 | // - When SIGTERM signal occurs:
49 | // - Delete all the applied forwards
50 | // - Shutdown the daemon
51 | func Start(ipt *iptables.IPTablesInstance, rulesFile string) int {
52 | infoLogger.Println(banner())
53 |
54 | err := createPidFile()
55 | if err != nil {
56 | errorLogger.Println(err)
57 | return 1
58 | }
59 | defer func() {
60 | err = removePidFile()
61 | }()
62 | infoLogger.Println("PID file created")
63 |
64 | // preparing rule set from rules file
65 | rulesContent, err := os.Open(rulesFile)
66 | if err != nil {
67 | errorLogger.Printf("error opening file: %v", err)
68 | return 1
69 | }
70 | ruleSet, err := rules.NewRuleSetFromFile(rulesContent)
71 | if err != nil {
72 | errorLogger.Println(err)
73 | return 1
74 | }
75 | // apply all the rules present in rulesFile
76 | for ruleId, rule := range ruleSet.Rules {
77 | err = ipt.CreateForward(&rule)
78 | if err != nil {
79 | infoLogger.Printf("rule %s - %v\n", ruleId, err)
80 | }
81 | }
82 | infoLogger.Println("rules from file have been applied")
83 |
84 | // preparing viper module to manage rules file
85 | v := viper.New()
86 | v.SetConfigFile(rulesFile)
87 | v.OnConfigChange(func(e fsnotify.Event) {
88 | infoLogger.Println("configuration has changed")
89 | rulesContent, err := os.Open(rulesFile)
90 | if err != nil {
91 | errorLogger.Printf("error opening file: %v", err)
92 | return
93 | }
94 | newRuleSet, err := rules.NewRuleSetFromFile(rulesContent)
95 | if err != nil {
96 | errorLogger.Println(err)
97 | return
98 | }
99 | rsd := rules.Diff(ruleSet, newRuleSet)
100 | // delete all the rules to be removed
101 | for _, rule := range rsd.ToRemove {
102 | err = ipt.DeleteForwardByRule(rule)
103 | if err != nil {
104 | errorLogger.Println(err)
105 | }
106 | }
107 | // create all the rules to be added
108 | for _, rule := range rsd.ToAdd {
109 | err = ipt.CreateForward(rule)
110 | if err != nil {
111 | errorLogger.Println(err)
112 | }
113 | }
114 | // set the new rule set as the current one
115 | ruleSet = newRuleSet
116 | })
117 | v.WatchConfig()
118 |
119 | sigChnl := make(chan os.Signal, 1)
120 | signal.Notify(sigChnl, syscall.SIGTERM)
121 | exitcChnl := make(chan bool, 1)
122 |
123 | go func() {
124 | for {
125 | select {
126 | case <-sigChnl:
127 | // flush rules before exit
128 | err := ipt.DeleteAll()
129 | if err != nil {
130 | errorLogger.Println(err)
131 | }
132 | infoLogger.Println("daemon stopped")
133 | exitcChnl <- true
134 | default:
135 | continue
136 | }
137 | }
138 | }()
139 | <-exitcChnl
140 | return 0
141 | }
142 |
143 | // Stop send a SIGTERM signal to the daemon process
144 | func Stop() {
145 | infoLogger.Println("stopping daemon")
146 | pid, err := readPidFile()
147 | if err != nil {
148 | errorLogger.Println(err)
149 | }
150 | err = syscall.Kill(pid, syscall.SIGTERM)
151 | if err != nil {
152 | errorLogger.Println(err)
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/internal/daemon/pid_file.go:
--------------------------------------------------------------------------------
1 | package daemon
2 |
3 | import (
4 | "os"
5 | "strconv"
6 | )
7 |
8 | var (
9 | pidFilePath = "/tmp/fwdctl.pid"
10 | )
11 |
12 | // Create PID file
13 | func createPidFile() error {
14 | pid := []byte(strconv.Itoa(os.Getpid()))
15 | err := os.WriteFile(pidFilePath, pid, 0644)
16 | if err != nil {
17 | return err
18 | }
19 | return nil
20 | }
21 |
22 | // Retrieve PID by reading file content
23 | func readPidFile() (int, error) {
24 | pidB, err := os.ReadFile(pidFilePath)
25 | if err != nil {
26 | return 0, err
27 | }
28 | pid, err := strconv.Atoi(string(pidB))
29 | if err != nil {
30 | return 0, err
31 | }
32 | return pid, nil
33 | }
34 |
35 | // Remove PID file
36 | func removePidFile() error {
37 | err := os.Remove(pidFilePath)
38 | if err != nil {
39 | return err
40 | }
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/internal/printer/json.go:
--------------------------------------------------------------------------------
1 | package printer
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/alegrey91/fwdctl/internal/rules"
8 | "github.com/alegrey91/fwdctl/pkg/iptables"
9 | )
10 |
11 | type Json struct {
12 | }
13 |
14 | func NewJson() *Json {
15 | return &Json{}
16 | }
17 |
18 | func (j *Json) PrintResult(ruleList map[int]string) error {
19 | rules := rules.NewRuleSet()
20 | for _, rule := range ruleList {
21 | jsonRule, err := iptables.ExtractRuleInfo(rule)
22 | if err != nil {
23 | continue
24 | }
25 |
26 | rules.Add(*jsonRule)
27 | }
28 | val, err := json.MarshalIndent(rules.Rules, "", " ")
29 | if err != nil {
30 | return err
31 | }
32 |
33 | fmt.Println(string(val))
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/internal/printer/printer_interface.go:
--------------------------------------------------------------------------------
1 | package printer
2 |
3 | type Printer interface {
4 | PrintResult(ruleList map[int]string) error
5 | }
6 |
7 | func NewPrinter(printFormat string) Printer {
8 | switch printFormat {
9 | case "table":
10 | return NewTable()
11 | case "json":
12 | return NewJson()
13 | case "yaml":
14 | return NewYaml()
15 | default:
16 | return NewTable()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/internal/printer/table.go:
--------------------------------------------------------------------------------
1 | package printer
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/alegrey91/fwdctl/pkg/iptables"
8 | "github.com/olekukonko/tablewriter"
9 | )
10 |
11 | type Table struct {
12 | }
13 |
14 | func NewTable() *Table {
15 | return &Table{}
16 | }
17 |
18 | func (t *Table) PrintResult(ruleList map[int]string) error {
19 | table := tablewriter.NewWriter(os.Stdout)
20 | table.SetHeader([]string{"number", "interface", "protocol", "external port", "internal ip", "internal port"})
21 | for ruleId, rule := range ruleList {
22 | tabRule, err := iptables.ExtractRuleInfo(rule)
23 | if err != nil {
24 | continue
25 | }
26 | tabRow := []string{
27 | fmt.Sprintf("%d", ruleId),
28 | tabRule.Iface,
29 | tabRule.Proto,
30 | fmt.Sprintf("%d", tabRule.Dport),
31 | tabRule.Saddr,
32 | fmt.Sprintf("%d", tabRule.Sport),
33 | }
34 | table.Append(tabRow)
35 | }
36 | table.Render()
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/internal/printer/yaml.go:
--------------------------------------------------------------------------------
1 | package printer
2 |
3 | import (
4 | "fmt"
5 |
6 | yaml "gopkg.in/yaml.v3"
7 |
8 | "github.com/alegrey91/fwdctl/internal/rules"
9 | "github.com/alegrey91/fwdctl/pkg/iptables"
10 | )
11 |
12 | type Yaml struct {
13 | }
14 |
15 | func NewYaml() *Yaml {
16 | return &Yaml{}
17 | }
18 |
19 | func (y *Yaml) PrintResult(ruleList map[int]string) error {
20 | rules := rules.NewRuleSet()
21 | for _, rule := range ruleList {
22 | jsonRule, err := iptables.ExtractRuleInfo(rule)
23 | if err != nil {
24 | continue
25 | }
26 |
27 | rules.Add(*jsonRule)
28 | }
29 | val, err := yaml.Marshal(rules.Rules)
30 | if err != nil {
31 | return err
32 | }
33 |
34 | fmt.Println(string(val))
35 | return nil
36 | }
37 |
--------------------------------------------------------------------------------
/internal/rules/ruleset.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "fmt"
7 | "io"
8 |
9 | "github.com/alegrey91/fwdctl/pkg/iptables"
10 | "gopkg.in/yaml.v2"
11 | )
12 |
13 | func NewRuleSet() *RuleSet {
14 | return &RuleSet{
15 | Rules: make(map[string]iptables.Rule),
16 | }
17 | }
18 |
19 | // NewRuleSet return the struct that contains informations about rules
20 | func NewRuleSetFromFile(file io.Reader) (*RuleSet, error) {
21 | // Read rules from file
22 | rulesFile, err := io.ReadAll(file)
23 | if err != nil {
24 | return nil, fmt.Errorf("error reading file: %v", err)
25 | }
26 |
27 | // Retrieve rules to fill RuleSet
28 | rules := supportRuleSet{}
29 | err = yaml.Unmarshal(rulesFile, &rules)
30 | if err != nil {
31 | return nil, fmt.Errorf("error unmarshaling content: %v", err)
32 | }
33 |
34 | // Fill RuleSet with rules taken from file
35 | rs := NewRuleSet()
36 | for _, rule := range rules.Rules {
37 | ruleHash := hash(rule)
38 | rs.Rules[ruleHash] = rule
39 | }
40 | return rs, nil
41 | }
42 |
43 | func hash(rule iptables.Rule) string {
44 | md5.New()
45 | ruleString := fmt.Sprintf("%s%s%d%s%d",
46 | rule.Iface,
47 | rule.Proto,
48 | rule.Dport,
49 | rule.Saddr,
50 | rule.Sport,
51 | )
52 | hash := md5.Sum([]byte(ruleString))
53 | return hex.EncodeToString(hash[:])
54 | }
55 |
56 | func (rs *RuleSet) GetHash(rule iptables.Rule) string {
57 | return hash(rule)
58 | }
59 |
60 | func (rs *RuleSet) Add(rule iptables.Rule) {
61 | ruleHash := hash(rule)
62 | rs.Rules[ruleHash] = rule
63 | }
64 |
65 | func (rs *RuleSet) Remove(ruleHash string) {
66 | delete(rs.Rules, ruleHash)
67 | }
68 |
69 | type RuleSetDiff struct {
70 | ToRemove []*iptables.Rule
71 | ToAdd []*iptables.Rule
72 | }
73 |
74 | // Diff method returns a *RuleSetDiff struct.
75 | // It contains a list of Rule(s) to be added / remove
76 | // in order to achieve the new RuleSet state.
77 | func Diff(oldRS, newRS *RuleSet) *RuleSetDiff {
78 | ruleSetDiff := &RuleSetDiff{}
79 | // loop over old rules set, to find rules to be removed
80 | for hash := range oldRS.Rules {
81 | // if key in oldRules is not present in rs,
82 | // then the old rule must be removed
83 | if _, ok := newRS.Rules[hash]; !ok {
84 | rule := oldRS.Rules[hash]
85 | ruleSetDiff.ToRemove = append(ruleSetDiff.ToRemove, &rule)
86 | }
87 | }
88 | // loop over new rules set, to find rules to be added
89 | for hash := range newRS.Rules {
90 | // if key in rs in not present in oldRs,
91 | // then the new rule must be added
92 | if _, ok := oldRS.Rules[hash]; !ok {
93 | rule := newRS.Rules[hash]
94 | ruleSetDiff.ToAdd = append(ruleSetDiff.ToAdd, &rule)
95 | }
96 | }
97 | return ruleSetDiff
98 | }
99 |
--------------------------------------------------------------------------------
/internal/rules/ruleset_test.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/alegrey91/fwdctl/pkg/iptables"
9 | )
10 |
11 | func TestDiff(t *testing.T) {
12 | type args struct {
13 | newRS string
14 | oldRS string
15 | }
16 | tests := []struct {
17 | name string
18 | args args
19 | want *RuleSetDiff
20 | }{
21 | // TODO: Add test cases.
22 | {
23 | name: "should_return_empty_RuleSetDiff",
24 | args: args{
25 | newRS: "",
26 | oldRS: "",
27 | },
28 | want: &RuleSetDiff{},
29 | },
30 | {
31 | name: "should_return_two_Rules_to_be_added",
32 | args: args{
33 | newRS: `
34 | rules:
35 | - dport: 3000
36 | saddr: 127.0.0.1
37 | sport: 80
38 | iface: lo
39 | proto: tcp
40 | - dport: 2000
41 | saddr: 127.0.0.1
42 | sport: 22
43 | iface: lo
44 | proto: tcp
45 | `,
46 | oldRS: "",
47 | },
48 | want: &RuleSetDiff{
49 | ToAdd: []*iptables.Rule{
50 | {
51 | Iface: "lo",
52 | Proto: "tcp",
53 | Dport: 3000,
54 | Saddr: "127.0.0.1",
55 | Sport: 80,
56 | },
57 | {
58 | Iface: "lo",
59 | Proto: "tcp",
60 | Dport: 2000,
61 | Saddr: "127.0.0.1",
62 | Sport: 22,
63 | },
64 | },
65 | },
66 | },
67 | {
68 | name: "should_return_two_Rules_to_be_removed",
69 | args: args{
70 | newRS: "",
71 | oldRS: `
72 | rules:
73 | - dport: 3000
74 | saddr: 127.0.0.1
75 | sport: 80
76 | iface: lo
77 | proto: tcp
78 | - dport: 2000
79 | saddr: 127.0.0.1
80 | sport: 22
81 | iface: lo
82 | proto: tcp
83 | `,
84 | },
85 | want: &RuleSetDiff{
86 | ToRemove: []*iptables.Rule{
87 | {
88 | Iface: "lo",
89 | Proto: "tcp",
90 | Dport: 3000,
91 | Saddr: "127.0.0.1",
92 | Sport: 80,
93 | },
94 | {
95 | Iface: "lo",
96 | Proto: "tcp",
97 | Dport: 2000,
98 | Saddr: "127.0.0.1",
99 | Sport: 22,
100 | },
101 | },
102 | },
103 | },
104 | }
105 | for _, tt := range tests {
106 | t.Run(tt.name, func(t *testing.T) {
107 | newRuleSet, err := NewRuleSetFromFile(strings.NewReader(tt.args.newRS))
108 | if err != nil {
109 | t.Error(err)
110 | }
111 | oldRuleSet, err := NewRuleSetFromFile(strings.NewReader(tt.args.oldRS))
112 | if err != nil {
113 | t.Error(err)
114 | }
115 | if got := Diff(oldRuleSet, newRuleSet); !reflect.DeepEqual(got, tt.want) {
116 | t.Errorf("Diff() = %v, want %v", got, tt.want)
117 | }
118 | })
119 | }
120 | }
121 |
122 | func TestNewRuleSetFromFile(t *testing.T) {
123 | type args struct {
124 | file string
125 | }
126 | tests := []struct {
127 | name string
128 | args args
129 | want *RuleSet
130 | wantErr bool
131 | }{
132 | // TODO: Add test cases.
133 | {
134 | name: "should_return_error",
135 | args: args{
136 | file: `
137 | {
138 | "rules": {
139 | "dport": 3000,
140 | "saddr": "127.0.0.1",
141 | "sport": 80,
142 | "iface": "lo",
143 | "proto": "tcp",
144 | },
145 | }
146 | `,
147 | },
148 | want: nil,
149 | wantErr: true,
150 | },
151 | {
152 | name: "should_return_valid_resultset",
153 | args: args{
154 | file: `
155 | rules:
156 | - dport: 3000
157 | saddr: 127.0.0.1
158 | sport: 80
159 | iface: lo
160 | proto: tcp
161 | `,
162 | },
163 | want: &RuleSet{
164 | Rules: map[string]iptables.Rule{
165 | "0be1c5f4141015ca6a8e873344da06e6": {
166 | Iface: "lo",
167 | Proto: "tcp",
168 | Dport: 3000,
169 | Saddr: "127.0.0.1",
170 | Sport: 80,
171 | },
172 | },
173 | },
174 | wantErr: false,
175 | },
176 | }
177 | for _, tt := range tests {
178 | t.Run(tt.name, func(t *testing.T) {
179 | got, err := NewRuleSetFromFile(strings.NewReader(tt.args.file))
180 | if (err != nil) != tt.wantErr {
181 | t.Errorf("NewRuleSetFromFile() error = %v, wantErr %v", err, tt.wantErr)
182 | return
183 | }
184 | if !reflect.DeepEqual(got, tt.want) {
185 | t.Errorf("NewRuleSetFromFile() = %v, want %v", got, tt.want)
186 | }
187 | })
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/internal/rules/types.go:
--------------------------------------------------------------------------------
1 | package rules
2 |
3 | import (
4 | "github.com/alegrey91/fwdctl/pkg/iptables"
5 | )
6 |
7 | // RulesFile keep track of listed rules
8 | // the result looks like so:
9 | // rules:
10 | // - iface: eth0
11 | // proto: tcp
12 | // dport: 3000
13 | // saddr: 192.168.122.43
14 | // sport: 22
15 | // - iface: eth0
16 | // ...
17 | type RuleSet struct {
18 | //Rules []iptables.Rule `yaml:"rules"`
19 | Rules map[string]iptables.Rule `yaml:"rules"`
20 | }
21 |
22 | // Struct to support creation of RuleSet
23 | type supportRuleSet struct {
24 | Rules []iptables.Rule `yaml:"rules"`
25 | }
26 |
--------------------------------------------------------------------------------
/internal/template/rules_template/rules.go:
--------------------------------------------------------------------------------
1 | package rules_template
2 |
3 | import (
4 | _ "embed"
5 | )
6 |
7 | //go:embed rules.yml.tpl
8 | var rulesTemplate string
9 | var rulesTemplateName = "rules"
10 | var rulesFileName = "rules.yml"
11 |
12 | type Rule struct {
13 | }
14 |
15 | func NewRules() *Rule {
16 | return &Rule{}
17 | }
18 |
19 | func (r *Rule) GetTemplateStruct() interface{} {
20 | return r
21 | }
22 |
23 | func (r *Rule) GetFileContent() string {
24 | return rulesTemplate
25 | }
26 |
27 | func (r *Rule) GetTemplateName() string {
28 | return rulesTemplateName
29 | }
30 |
31 | func (r *Rule) GetFileName() string {
32 | return rulesFileName
33 | }
34 |
--------------------------------------------------------------------------------
/internal/template/rules_template/rules.yml.tpl:
--------------------------------------------------------------------------------
1 | rules:
2 | - dport:
3 | saddr:
4 | sport:
5 | iface:
6 | proto:
7 |
--------------------------------------------------------------------------------
/internal/template/systemd_template/fwdctl.service.tpl:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=fwdctl systemd service
3 | After=network.target
4 |
5 | [Service]
6 | Type={{.ServiceType}}
7 | {{if eq .ServiceType "oneshot"}}ExecStart={{.InstallationPath}}/fwdctl apply --file={{.RulesFile}}{{else if eq .ServiceType "fork"}}ExecStart={{.InstallationPath}}/fwdctl daemon start --file={{.RulesFile}}
8 | ExecStop={{.InstallationPath}}/fwdctl daemon stop
9 | Restart=always
10 | RestartSec=5s{{end}}
11 | StandardOutput=journal
12 |
13 | [Install]
14 | WantedBy=multi-user.target
15 |
--------------------------------------------------------------------------------
/internal/template/systemd_template/systemd_service.go:
--------------------------------------------------------------------------------
1 | package systemd_template
2 |
3 | import (
4 | _ "embed"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "path/filepath"
9 | )
10 |
11 | //go:embed fwdctl.service.tpl
12 | var systemdTemplate string
13 | var systemdTemplateName = "systemd"
14 | var systemdFileName = "fwdctl.service"
15 | var allowedServiceTypes = [2]string{"oneshot", "fork"}
16 |
17 | type SystemdService struct {
18 | ServiceType string
19 | InstallationPath string
20 | RulesFile string
21 | }
22 |
23 | func serviceTypeAllowed(st string) bool {
24 | for _, ast := range allowedServiceTypes {
25 | if ast == st {
26 | return true
27 | }
28 | }
29 | return false
30 | }
31 |
32 | func NewSystemdService(serviceType, installationPath, rulesFile string) (*SystemdService, error) {
33 | // checks for systemd service type
34 | if !serviceTypeAllowed(serviceType) {
35 | return nil, fmt.Errorf("service type is not allowed: %s", serviceType)
36 | }
37 | // checks for installation path
38 | if !filepath.IsAbs(installationPath) {
39 | return nil, fmt.Errorf("installation path is not absolute: %s", installationPath)
40 | }
41 | if _, err := os.Stat(installationPath); errors.Is(err, os.ErrNotExist) {
42 | return nil, fmt.Errorf("installation path does not exist: %s", installationPath)
43 | }
44 | // checks for rules file
45 | if !filepath.IsAbs(rulesFile) {
46 | return nil, fmt.Errorf("rules file path is not absolute: %s", rulesFile)
47 | }
48 | if _, err := os.Stat(rulesFile); errors.Is(err, os.ErrNotExist) {
49 | return nil, fmt.Errorf("rules file path does not exist: %s", rulesFile)
50 | }
51 |
52 | return &SystemdService{
53 | ServiceType: serviceType,
54 | InstallationPath: installationPath,
55 | RulesFile: rulesFile,
56 | }, nil
57 | }
58 |
59 | func (s *SystemdService) GetTemplateStruct() interface{} {
60 | return s
61 | }
62 |
63 | func (s *SystemdService) GetFileContent() string {
64 | return systemdTemplate
65 | }
66 |
67 | func (s *SystemdService) GetTemplateName() string {
68 | return systemdTemplateName
69 | }
70 |
71 | func (s *SystemdService) GetFileName() string {
72 | return systemdFileName
73 | }
74 |
--------------------------------------------------------------------------------
/internal/template/systemd_template/systemd_service_test.go:
--------------------------------------------------------------------------------
1 | package systemd_template
2 |
3 | import (
4 | _ "embed"
5 | "os"
6 | "reflect"
7 | "testing"
8 | )
9 |
10 | func Test_serviceTypeAllowed(t *testing.T) {
11 | type args struct {
12 | st string
13 | }
14 | tests := []struct {
15 | name string
16 | args args
17 | want bool
18 | }{
19 | // TODO: Add test cases.
20 | {
21 | name: "service_type_not_allowed",
22 | args: args{
23 | st: "none",
24 | },
25 | want: false,
26 | },
27 | {
28 | name: "service_type_allowed",
29 | args: args{
30 | st: "fork",
31 | },
32 | want: true,
33 | },
34 | }
35 | for _, tt := range tests {
36 | t.Run(tt.name, func(t *testing.T) {
37 | if got := serviceTypeAllowed(tt.args.st); got != tt.want {
38 | t.Errorf("serviceTypeAllowed() = %v, want %v", got, tt.want)
39 | }
40 | })
41 | }
42 | }
43 |
44 | func TestNewSystemdService(t *testing.T) {
45 | type args struct {
46 | serviceType string
47 | installationPath string
48 | rulesFile string
49 | }
50 | tests := []struct {
51 | name string
52 | args args
53 | want *SystemdService
54 | wantErr bool
55 | }{
56 | // TODO: Add test cases.
57 | {
58 | name: "should_return_valid_struct",
59 | args: args{
60 | serviceType: "fork",
61 | installationPath: "/opt/",
62 | rulesFile: "/tmp/rules.yml",
63 | },
64 | want: &SystemdService{
65 | ServiceType: "fork",
66 | InstallationPath: "/opt/",
67 | RulesFile: "/tmp/rules.yml",
68 | },
69 | wantErr: false,
70 | },
71 | {
72 | name: "should_return_nil_due_to_wrong_service_type",
73 | args: args{
74 | serviceType: "aaa",
75 | installationPath: "/opt/",
76 | rulesFile: "/home/user/rules.yml",
77 | },
78 | want: nil,
79 | wantErr: true,
80 | },
81 | {
82 | name: "should_return_nil_due_to_wrong_installation_path",
83 | args: args{
84 | serviceType: "oneshot",
85 | installationPath: "../../tmp/",
86 | rulesFile: "/home/user/rules.yml",
87 | },
88 | want: nil,
89 | wantErr: true,
90 | },
91 | {
92 | name: "should_return_nil_due_to_wrong_installation_path_2",
93 | args: args{
94 | serviceType: "oneshot",
95 | installationPath: "/this/path/doesnt/exist",
96 | rulesFile: "/home/user/rules.yml",
97 | },
98 | want: nil,
99 | wantErr: true,
100 | },
101 | {
102 | name: "should_return_nil_due_to_wrong_rules_file",
103 | args: args{
104 | serviceType: "oneshot",
105 | installationPath: "/opt/",
106 | rulesFile: "rules.yml",
107 | },
108 | want: nil,
109 | wantErr: true,
110 | },
111 | {
112 | name: "should_return_nil_due_to_wrong_rules_file_2",
113 | args: args{
114 | serviceType: "oneshot",
115 | installationPath: "/opt/",
116 | rulesFile: "/this/path/doesnt/exist/rules.yml",
117 | },
118 | want: nil,
119 | wantErr: true,
120 | },
121 | }
122 | fileName := "/tmp/rules.yml"
123 | _, err := os.Stat(fileName)
124 | if os.IsNotExist(err) {
125 | file, err := os.Create(fileName)
126 | if err != nil {
127 | t.Fatal(err)
128 | }
129 | defer file.Close()
130 | }
131 | for _, tt := range tests {
132 | t.Run(tt.name, func(t *testing.T) {
133 | got, err := NewSystemdService(tt.args.serviceType, tt.args.installationPath, tt.args.rulesFile)
134 | if (err != nil) != tt.wantErr {
135 | t.Errorf("NewSystemdService() error = %v, wantErr %v", err, tt.wantErr)
136 | return
137 | }
138 | if !reflect.DeepEqual(got, tt.want) {
139 | t.Errorf("NewSystemdService() = %v, want %v", got, tt.want)
140 | }
141 | })
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/internal/template/template.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "text/template"
9 | )
10 |
11 | type Generator interface {
12 | GetTemplateStruct() interface{}
13 | GetFileContent() string
14 | GetTemplateName() string
15 | GetFileName() string
16 | }
17 |
18 | func GenerateTemplate(g Generator, outputPath string) error {
19 | tpl, err := template.New(g.GetTemplateName()).Parse(g.GetFileContent())
20 | if err != nil {
21 | return fmt.Errorf("error getting template instance: %v", err)
22 | }
23 |
24 | if !filepath.IsAbs(outputPath) {
25 | return fmt.Errorf("output path is not absolute: %s", outputPath)
26 | }
27 | // if last char of outputPath is "/" we want to remove,
28 | // so the final output will be cleaned.
29 | // this way: /root/template.file instead of /root//template.file
30 | if outputPath != "/" && outputPath[len(outputPath)-1:] == "/" {
31 | outputPath = strings.TrimSuffix(outputPath, "/")
32 | }
33 |
34 | outFile, err := os.Create(filepath.Join(outputPath, g.GetFileName()))
35 | if err != nil {
36 | return fmt.Errorf("error creating output file: %v", err)
37 | }
38 |
39 | err = tpl.Execute(outFile, g.GetTemplateStruct())
40 | if err != nil {
41 | return fmt.Errorf("error writing content into file: %v", err)
42 | }
43 | return nil
44 | }
45 |
--------------------------------------------------------------------------------
/internal/template/template_test.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import "testing"
4 |
5 | type Test struct {
6 | }
7 |
8 | func NewTest() *Test {
9 | return &Test{}
10 | }
11 |
12 | func (t *Test) GetTemplateStruct() interface{} {
13 | return t
14 | }
15 |
16 | func (t *Test) GetFileContent() string {
17 | return "this_is_the_content"
18 | }
19 |
20 | func (t *Test) GetTemplateName() string {
21 | return "this_is_the_name"
22 | }
23 |
24 | func (t *Test) GetFileName() string {
25 | return "this_is_the_file_name"
26 | }
27 |
28 | func TestGenerateTemplate(t *testing.T) {
29 | type args struct {
30 | g Generator
31 | outputPath string
32 | }
33 | tests := []struct {
34 | name string
35 | args args
36 | wantErr bool
37 | }{
38 | // TODO: Add test cases.
39 | {
40 | name: "this_should_pass",
41 | args: args{
42 | g: NewTest(),
43 | outputPath: "/tmp/",
44 | },
45 | wantErr: false,
46 | },
47 | {
48 | name: "this_should_fail_due_to_wrong_output_path",
49 | args: args{
50 | g: NewTest(),
51 | outputPath: "tmp/",
52 | },
53 | wantErr: true,
54 | },
55 | }
56 | for _, tt := range tests {
57 | t.Run(tt.name, func(t *testing.T) {
58 | if err := GenerateTemplate(tt.args.g, tt.args.outputPath); (err != nil) != tt.wantErr {
59 | t.Errorf("GenerateTemplate() error = %v, wantErr %v", err, tt.wantErr)
60 | }
61 | })
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 Alessio Greggi
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package main
17 |
18 | import "github.com/alegrey91/fwdctl/cmd"
19 |
20 | func main() {
21 | cmd.Execute()
22 | }
23 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/rogpeppe/go-internal/testscript"
7 | )
8 |
9 | func TestFwdctl(t *testing.T) {
10 | testscript.Run(t, testscript.Params{
11 | TestWork: true,
12 | Dir: "tests",
13 | Cmds: customCommands(),
14 | RequireExplicitExec: true,
15 | Setup: func(env *testscript.Env) error {
16 | env.Setenv("GOCOVERDIR", "/tmp/integration")
17 | return nil
18 | },
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/iptables/defaults.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | var (
4 | FwdTable = "nat"
5 | FwdChain = "PREROUTING"
6 | FwdTarget = "DNAT"
7 | )
8 |
--------------------------------------------------------------------------------
/pkg/iptables/forward.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/coreos/go-iptables/iptables"
9 | )
10 |
11 | var (
12 | label string = "fwdctl"
13 | )
14 |
15 | type IPTablesInstance struct {
16 | *iptables.IPTables
17 | }
18 |
19 | func NewIPTablesInstance() (*IPTablesInstance, error) {
20 | ipt := IPTablesInstance{}
21 | iptables, err := getIPTablesInstance()
22 | if err != nil {
23 | return nil, fmt.Errorf("failed: %v", err)
24 | }
25 | ipt.IPTables = iptables
26 | return &ipt, nil
27 | }
28 |
29 | func (ipt *IPTablesInstance) ValidateForward(rule *Rule) error {
30 | return validate(rule.Iface, rule.Proto, rule.Dport, rule.Saddr, rule.Sport)
31 | }
32 |
33 | func (ipt *IPTablesInstance) CreateForward(rule *Rule) error {
34 | // example rule:
35 | // iptables -t nat -A PREROUTING -i eth0 -p tcp -m tcp --dport 3000 -j DNAT --to-destination 192.168.199.105:80
36 |
37 | // check if input interface exists on the system
38 | ifaceExits, err := interfaceExists(rule.Iface)
39 | if err != nil {
40 | return fmt.Errorf("error reading interfaces: %v", err)
41 | }
42 | if !ifaceExits {
43 | return fmt.Errorf("interface %s does not exists", rule.Iface)
44 | }
45 |
46 | // check if provided rule already exists
47 | ruleExists, err := ipt.Exists(FwdTable, FwdChain, rule.String()...)
48 | if err != nil {
49 | return fmt.Errorf("%v", err)
50 | }
51 | if ruleExists {
52 | return fmt.Errorf("rule already exists")
53 | }
54 |
55 | // apply provided rule
56 | err = ipt.AppendUnique(FwdTable, FwdChain, rule.String()...)
57 | if err != nil {
58 | return fmt.Errorf("rule failed: %v", err)
59 | }
60 | return nil
61 | }
62 |
63 | func (ipt *IPTablesInstance) ListForward(outputFormat string) (map[int]string, error) {
64 | ruleList, err := ipt.List(FwdTable, FwdChain)
65 | if err != nil {
66 | return nil, fmt.Errorf("failed listing rules: %v", err)
67 | }
68 |
69 | // check listed rules are tagged with custom tag
70 | fwdRules := make(map[int]string)
71 | for ruleId, rule := range ruleList {
72 | if strings.Contains(rule, label) {
73 | fwdRules[ruleId] = rule
74 | }
75 | }
76 |
77 | return fwdRules, nil
78 | }
79 |
80 | func (ipt *IPTablesInstance) DeleteForwardById(ruleId int) error {
81 | // delete rule
82 | err := ipt.Delete(FwdTable, FwdChain, strconv.Itoa(ruleId))
83 | if err != nil {
84 | return fmt.Errorf("failed deleting rule n. %d\nerr: %v", ruleId, err)
85 | }
86 | return nil
87 | }
88 |
89 | func (ipt *IPTablesInstance) DeleteForwardByRule(rule *Rule) error {
90 | // TODO: create function to return []string with packed rule, passing iface, proto, etc as arguments.
91 | err := ipt.Delete(FwdTable, FwdChain, rule.String()...)
92 | if err != nil {
93 | return fmt.Errorf("failed deleting rule: '%s'\n err: %v", rule.String(), err)
94 | }
95 | return nil
96 | }
97 |
98 | func (ipt *IPTablesInstance) DeleteAllForwards() error {
99 | ruleList, err := ipt.List(FwdTable, FwdChain)
100 | if err != nil {
101 | return fmt.Errorf("failed listing rules: %v", err)
102 | }
103 |
104 | // check listed rules are tagged with custom tag
105 | fwdRules := make(map[int]string)
106 | for ruleId, rule := range ruleList {
107 | if strings.Contains(rule, label) {
108 | fwdRules[ruleId] = rule
109 | }
110 | }
111 |
112 | for _, rule := range fwdRules {
113 | r, err := ExtractRuleInfo(rule)
114 | if err != nil {
115 | return fmt.Errorf("error extracting rule info: %v", err)
116 | }
117 |
118 | err = ipt.Delete(FwdTable, FwdChain, r.String()...)
119 | if err != nil {
120 | return fmt.Errorf("error deleting rule: %v", err)
121 | }
122 | }
123 | return nil
124 | }
125 |
--------------------------------------------------------------------------------
/pkg/iptables/interface.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import (
4 | "net"
5 | )
6 |
7 | func interfaceExists(iface string) (bool, error) {
8 | ifi, err := net.InterfaceByName(iface)
9 | if err != nil {
10 | return false, err
11 | }
12 |
13 | if ifi != nil {
14 | return true, nil
15 | }
16 | return false, nil
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/iptables/interface_test.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import "testing"
4 |
5 | func Test_interfaceExists(t *testing.T) {
6 | type args struct {
7 | iface string
8 | }
9 | tests := []struct {
10 | name string
11 | args args
12 | want bool
13 | wantErr bool
14 | }{
15 | // TODO: Add test cases.
16 | {
17 | name: "should_exist",
18 | args: args{
19 | iface: "lo",
20 | },
21 | want: true,
22 | wantErr: false,
23 | },
24 | {
25 | name: "should_not_exist",
26 | args: args{
27 | iface: "xyz0",
28 | },
29 | want: false,
30 | wantErr: true,
31 | },
32 | {
33 | name: "should_exist",
34 | args: args{
35 | iface: "lo",
36 | },
37 | want: true,
38 | wantErr: false,
39 | },
40 | }
41 | for _, tt := range tests {
42 | t.Run(tt.name, func(t *testing.T) {
43 | got, err := interfaceExists(tt.args.iface)
44 | if (err != nil) != tt.wantErr {
45 | t.Errorf("interfaceExists() error = %v, wantErr %v", err, tt.wantErr)
46 | return
47 | }
48 | if got != tt.want {
49 | t.Errorf("interfaceExists() = %v, want %v", got, tt.want)
50 | }
51 | })
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/iptables/rule.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | type Rule struct {
10 | Iface string `json:"iface" yaml:"iface" default:"lo"`
11 | Proto string `json:"proto" yaml:"proto" default:"tcp"`
12 | Dport int `json:"dport" yaml:"dport"`
13 | Saddr string `json:"saddr" yaml:"saddr"`
14 | Sport int `json:"sport" yaml:"sport"`
15 | Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
16 | }
17 |
18 | func NewRule(iface string, proto string, dport int, saddr string, sport int) *Rule {
19 | return &Rule{
20 | Iface: iface,
21 | Proto: proto,
22 | Dport: dport,
23 | Saddr: saddr,
24 | Sport: sport,
25 | Comment: label,
26 | }
27 | }
28 |
29 | // ExtractRuleInfo extract forward information from rule
30 | // if it matches the requirements.
31 | // Returns the Rule struct and error
32 | func ExtractRuleInfo(rawRule string) (*Rule, error) {
33 | // extract rules info:
34 | // -t nat -A PREROUTING -i eth0 -p tcp -m tcp --dport 3000 -j DNAT --to-destination 192.168.199.105:80
35 | // result:
36 | // Rule{Iface: eth0, Proto: tcp, Dport: 3000, Saddr: 192.168.199.105, Sport: 80}
37 | ruleSplit := strings.Split(rawRule, " ")
38 | rule := &Rule{}
39 | for id, arg := range ruleSplit {
40 | switch arg {
41 | case "-i":
42 | rule.Iface = ruleSplit[id+1]
43 | case "-p":
44 | rule.Proto = ruleSplit[id+1]
45 | case "--dport":
46 | dport, err := strconv.Atoi(ruleSplit[id+1])
47 | if err != nil {
48 | return nil, fmt.Errorf("error converting string '%s' to int: %v", ruleSplit[id+1], err)
49 | }
50 | rule.Dport = dport
51 | case "--to-destination":
52 | rule.Saddr = strings.Split(ruleSplit[id+1], ":")[0]
53 | sport, err := strconv.Atoi(strings.Split(ruleSplit[id+1], ":")[1])
54 | if err != nil {
55 | return nil, fmt.Errorf("error converting string '%s' to int: %v", ruleSplit[id+1], err)
56 | }
57 | rule.Sport = sport
58 | }
59 | }
60 | if rule.Iface == "" {
61 | return nil, fmt.Errorf("missing iface value")
62 | }
63 | if rule.Proto == "" {
64 | return nil, fmt.Errorf("missing proto value")
65 | }
66 | if rule.Dport == 0 {
67 | return nil, fmt.Errorf("missing dport value")
68 | }
69 | if rule.Saddr == "" {
70 | return nil, fmt.Errorf("missing saddr value")
71 | }
72 | if rule.Sport == 0 {
73 | return nil, fmt.Errorf("missing sport value")
74 | }
75 |
76 | return rule, nil
77 | }
78 |
79 | // String returns a list of string that compose the iptables rule.
80 | // Eg: -t nat -A PREROUTING -i eth0 -p tcp -m tcp --dport 3000 -m comment --comment fwdctl -j DNAT --to-destination 192.168.199.105:80
81 | func (rule *Rule) String() []string {
82 | return []string{
83 | "-i", rule.Iface,
84 | "-p", rule.Proto,
85 | "-m", rule.Proto,
86 | "--dport", fmt.Sprintf("%d", rule.Dport),
87 | "-m", "comment", "--comment", label,
88 | "-j", FwdTarget,
89 | "--to-destination", rule.Saddr + ":" + fmt.Sprintf("%d", rule.Sport),
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/pkg/iptables/rule_test.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestExtractRuleInfo(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | rule string
12 | want *Rule
13 | wantErr bool
14 | }{
15 | // TODO: Add test cases.
16 | {
17 | name: "example_1",
18 | rule: "-A PREROUTING -i lo -p tcp -m tcp --dport 3001 -m comment --comment fwdctl -j DNAT --to-destination 127.0.0.1:80",
19 | want: &Rule{
20 | Iface: "lo",
21 | Proto: "tcp",
22 | Dport: 3001,
23 | Saddr: "127.0.0.1",
24 | Sport: 80,
25 | },
26 | },
27 | {
28 | name: "example_2",
29 | rule: "-A PREROUTING -i eth0 -p tcp -m tcp --dport 3001 -m comment --comment fwdctl -j DNAT --to-destination 127.0.0.1:80",
30 | want: &Rule{
31 | Iface: "eth0",
32 | Proto: "tcp",
33 | Dport: 3001,
34 | Saddr: "127.0.0.1",
35 | Sport: 80,
36 | },
37 | },
38 | {
39 | name: "example_3",
40 | rule: "-A PREROUTING -i eth0 -p tcp -m tcp --dport 3001 -m comment --comment fwdctl -j DNAT",
41 | want: nil,
42 | wantErr: true,
43 | },
44 | }
45 | for _, tt := range tests {
46 | t.Run(tt.name, func(t *testing.T) {
47 | got, err := ExtractRuleInfo(tt.rule)
48 | if (err != nil) != tt.wantErr {
49 | t.Errorf("ExtractRuleInfo() error = %v", err)
50 | return
51 | }
52 | if !reflect.DeepEqual(got, tt.want) {
53 | t.Errorf("ExtractRuleInfo() = %v, want %v", got, tt.want)
54 | }
55 | })
56 | }
57 | }
58 |
59 | func TestRule_String(t *testing.T) {
60 | type fields struct {
61 | Iface string
62 | Proto string
63 | Dport int
64 | Saddr string
65 | Sport int
66 | Comment string
67 | }
68 | tests := []struct {
69 | name string
70 | fields fields
71 | want []string
72 | }{
73 | // TODO: Add test cases.
74 | {
75 | name: "should_return_consistent_value",
76 | fields: fields{
77 | Iface: "eth0",
78 | Proto: "tcp",
79 | Dport: 8080,
80 | Saddr: "127.0.0.1",
81 | Sport: 80,
82 | },
83 | want: []string{"-i", "eth0", "-p", "tcp", "-m", "tcp", "--dport", "8080", "-m", "comment", "--comment", "fwdctl", "-j", "DNAT", "--to-destination", "127.0.0.1:80"},
84 | },
85 | {
86 | name: "should_return_consistent_value",
87 | fields: fields{
88 | Iface: "lo",
89 | Proto: "tcp",
90 | Dport: 8080,
91 | Saddr: "127.0.0.1",
92 | Sport: 90,
93 | },
94 | want: []string{"-i", "lo", "-p", "tcp", "-m", "tcp", "--dport", "8080", "-m", "comment", "--comment", "fwdctl", "-j", "DNAT", "--to-destination", "127.0.0.1:90"},
95 | },
96 | }
97 | for _, tt := range tests {
98 | t.Run(tt.name, func(t *testing.T) {
99 | rule := &Rule{
100 | Iface: tt.fields.Iface,
101 | Proto: tt.fields.Proto,
102 | Dport: tt.fields.Dport,
103 | Saddr: tt.fields.Saddr,
104 | Sport: tt.fields.Sport,
105 | Comment: tt.fields.Comment,
106 | }
107 | if got := rule.String(); !reflect.DeepEqual(got, tt.want) {
108 | t.Errorf("Rule.String() = %v, want %v", got, tt.want)
109 | }
110 | })
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/pkg/iptables/utils.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/coreos/go-iptables/iptables"
7 | )
8 |
9 | var once sync.Once
10 |
11 | type single *iptables.IPTables
12 |
13 | var singleInstance single
14 |
15 | // getIPTablesInstance create a singletone instance for iptables.New()
16 | func getIPTablesInstance() (*iptables.IPTables, error) {
17 | var err error
18 | if singleInstance == nil {
19 | once.Do(func() {
20 | singleInstance, err = iptables.New()
21 | })
22 | }
23 |
24 | return singleInstance, err
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/iptables/validation.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | )
7 |
8 | func validateIface(iface string) error {
9 | if iface == "" {
10 | return fmt.Errorf("name is empty")
11 | }
12 | ifaces, err := net.Interfaces()
13 | if err != nil {
14 | return fmt.Errorf("error: %v", err)
15 | }
16 | found := false
17 | for _, i := range ifaces {
18 | if i.Name == iface {
19 | found = true
20 | }
21 | }
22 | if !found {
23 | return fmt.Errorf("not found")
24 | }
25 | return nil
26 | }
27 |
28 | func validateProto(proto string) error {
29 | if proto == "" {
30 | return fmt.Errorf("protocol name is empty")
31 | }
32 | if (proto != "tcp") && (proto != "udp") && (proto != "icmp") {
33 | return fmt.Errorf("protocol name not allowed")
34 | }
35 | return nil
36 | }
37 |
38 | func validatePort(port int) error {
39 | if port < 1 || port > 65535 {
40 | return fmt.Errorf("port number not allowed")
41 | }
42 | return nil
43 | }
44 |
45 | func validateAddress(address string) error {
46 | // not a valid check for now.
47 | if address == "" {
48 | return fmt.Errorf("address is empty")
49 | }
50 | return nil
51 | }
52 |
53 | // validate returns both bool and error.
54 | // The boolean return true in case the rule passes all checks.
55 | // In case it does not, then the error will describe the problem.
56 | func validate(iface string, proto string, dport int, saddr string, sport int) error {
57 | err := validateIface(iface)
58 | if err != nil {
59 | return fmt.Errorf("interface: '%s' %v", iface, err)
60 | }
61 |
62 | err = validateProto(proto)
63 | if err != nil {
64 | return fmt.Errorf("protocol: '%s' %v", proto, err)
65 | }
66 |
67 | err = validatePort(dport)
68 | if err != nil {
69 | return fmt.Errorf("destination port: '%d' %v", dport, err)
70 | }
71 |
72 | err = validateAddress(saddr)
73 | if err != nil {
74 | return fmt.Errorf("source address: '%s' %v", saddr, err)
75 | }
76 |
77 | err = validatePort(sport)
78 | if err != nil {
79 | return fmt.Errorf("source port: '%d' %v", sport, err)
80 | }
81 | return nil
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/iptables/validation_test.go:
--------------------------------------------------------------------------------
1 | package iptables
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | )
7 |
8 | func Test_validateForward(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | iface string
12 | proto string
13 | dport int
14 | saddr string
15 | sport int
16 | expectedError error
17 | wantErr bool
18 | }{
19 | {
20 | "should_not_fail",
21 | "lo",
22 | "tcp",
23 | 9090,
24 | "127.0.0.1",
25 | 80,
26 | nil,
27 | false,
28 | },
29 | {
30 | "should_fail_protocol",
31 | "lo",
32 | "tcps",
33 | 9090,
34 | "127.0.0.1",
35 | 80,
36 | errors.New("protocol: 'tcps' protocol name not allowed"),
37 | true,
38 | },
39 | {
40 | "should_fail_destination_port",
41 | "lo",
42 | "tcp",
43 | 10202020,
44 | "127.0.0.1",
45 | 80,
46 | errors.New("destination port: '10202020' port number not allowed"),
47 | true,
48 | },
49 | {
50 | "shoudl_fail_source_port",
51 | "lo",
52 | "tcp",
53 | 9090,
54 | "127.0.0.1",
55 | 800000000,
56 | errors.New("source port: '800000000' port number not allowed"),
57 | true,
58 | },
59 | {
60 | "shoudl_fail_destination_port",
61 | "lo",
62 | "tcp",
63 | 0,
64 | "127.0.0.1",
65 | 8000,
66 | errors.New("source port: '0' port number not allowed"),
67 | true,
68 | },
69 | }
70 | for _, tt := range tests {
71 | t.Run(tt.name, func(t *testing.T) {
72 | if err := validate(tt.iface, tt.proto, tt.dport, tt.saddr, tt.sport); (err != nil) != tt.wantErr {
73 | t.Errorf("validateForward() error = %v, wantErr %v", err, tt.wantErr)
74 | }
75 | })
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # Integration Tests
2 |
3 | This directory is dedicated to host integration tests written with [`testscript`](https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript).
4 |
5 | ## Converting from test to trace file
6 |
7 | As you can see, for each command we have a couple of files (eg. `apply.txtar`, `apply_trace.txtar`).
8 |
9 | The `.txtar` file is used for testing purposes and is run at the beginning of the pipeline to ensure the binary is behave like we expect.
10 |
11 | The `_trace.txtar`, on the other side, is used for tracing purposes. This means that we re-run the same commands of the previous file, tracing them with [`harpoon`](https://github.com/alegrey91/harpoon) under the hood.
12 |
13 | The file content are quite similar, there are just few differences to follow:
14 |
15 | * Each `exec` of the command under test (eg. `fwdctl apply`) have to be replaced with `exec_cmd`.
16 |
17 | `exec_cmd` is a custom testscript function to trace the command using `harpoon`.
18 |
19 | * If the `exec` of the command under test had a negation (`!`), this should not be added in the command used with `exec_cmd`.
20 |
21 | This because in this case we don't care about the result of `harpoon` that will execute the real command.
22 |
23 | Here's an example:
24 |
25 | ```txt
26 | # command.txtar
27 |
28 | # normal execution of command
29 | exec command list -a
30 | # -x flag doesn't exists, so this should handle the error
31 | ! exec command list -x
32 | ```
33 |
34 | Should be converted into this:
35 |
36 | ```txt
37 | # command_trace.txtar
38 |
39 | exec_cmd command list -a
40 | exec_cmd command list -x
41 | ```
--------------------------------------------------------------------------------
/tests/apply.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'apply' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec fwdctl apply --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | # test primarly subcommand 'apply'
13 | exec fwdctl apply --file rules-2.yaml
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 | fwd_exists lo tcp 3001 127.0.0.1 80
16 | fwd_exists lo udp 3002 127.0.0.1 80
17 |
18 | # clean up first test
19 | exec fwdctl delete -n 1
20 | exec fwdctl delete -n 1
21 | exec fwdctl delete -n 1
22 | exec fwdctl delete -n 1
23 |
24 | exec fwdctl apply
25 |
26 | # clean up second test
27 | exec fwdctl delete -n 1
28 | exec fwdctl delete -n 1
29 | exec fwdctl delete -n 1
30 |
31 | # removing the default rules.yml file
32 | # it should fail
33 | exec rm rules.yml
34 | ! exec fwdctl apply
35 |
36 | # should not apply
37 | # (wrong protocol)
38 | ! exec fwdctl apply --file rules-wrong.yaml
39 | ! fwd_exists lo tcp 3000 127.0.0.1 80
40 | ! fwd_exists lo tcp 3001 127.0.0.1 80
41 | ! fwd_exists lo tcp 3002 127.0.0.1 80
42 |
43 | # should not apply
44 | # (empty interface)
45 | ! exec fwdctl apply --file rules-wrong2.yaml
46 | ! fwd_exists lo tcp 3000 127.0.0.1 80
47 | ! fwd_exists lo tcp 3001 127.0.0.1 80
48 | ! fwd_exists lo tcp 3002 127.0.0.1 80
49 |
50 | # should not apply
51 | # (interface doesn't exist)
52 | ! exec fwdctl apply --file rules-wrong3.yaml
53 | ! fwd_exists lo tcp 3000 127.0.0.1 80
54 | ! fwd_exists lo tcp 3001 127.0.0.1 80
55 | ! fwd_exists lo tcp 3002 127.0.0.1 80
56 |
57 | # should not apply
58 | # (empty destination port)
59 | ! exec fwdctl apply --file rules-wrong4.yaml
60 | ! fwd_exists lo tcp 3000 127.0.0.1 80
61 | ! fwd_exists lo tcp 3001 127.0.0.1 80
62 | ! fwd_exists lo tcp 3002 127.0.0.1 80
63 |
64 | # should not apply
65 | # (empty source address)
66 | ! exec fwdctl apply --file rules-wrong5.yaml
67 | ! fwd_exists lo tcp 3000 127.0.0.1 80
68 | ! fwd_exists lo tcp 3001 127.0.0.1 80
69 | ! fwd_exists lo tcp 3002 127.0.0.1 80
70 |
71 | # should not apply
72 | # (empty source port)
73 | ! exec fwdctl apply --file rules-wrong6.yaml
74 | ! fwd_exists lo tcp 3000 127.0.0.1 80
75 | ! fwd_exists lo tcp 3001 127.0.0.1 80
76 | ! fwd_exists lo tcp 3002 127.0.0.1 80
77 |
78 | -- rules.yml --
79 | rules:
80 | - dport: 3000
81 | saddr: 127.0.0.1
82 | sport: 80
83 | iface: lo
84 | proto: tcp
85 | - dport: 3001
86 | saddr: 127.0.0.1
87 | sport: 80
88 | iface: lo
89 | proto: tcp
90 | - dport: 3002
91 | saddr: 127.0.0.1
92 | sport: 80
93 | iface: lo
94 | proto: udp
95 |
96 | -- rules-2.yaml --
97 | rules:
98 | - dport: 3000
99 | saddr: 127.0.0.1
100 | sport: 80
101 | iface: lo
102 | proto: tcp
103 | - dport: 3001
104 | saddr: 127.0.0.1
105 | sport: 80
106 | iface: lo
107 | proto: tcp
108 | - dport: 3002
109 | saddr: 127.0.0.1
110 | sport: 80
111 | iface: lo
112 | proto: udp
113 |
114 | -- rules-wrong.yaml --
115 | rules:
116 | - dport: 3000
117 | saddr: 127.0.0.1
118 | sport: 80
119 | iface: lo
120 | proto: tcp
121 | - dport: 3001
122 | saddr: 127.0.0.1
123 | sport: 80
124 | iface: lo
125 | proto: tcp
126 | - dport: 3002
127 | saddr: 127.0.0.1
128 | sport: 80
129 | iface: lo
130 | proto: xxx
131 |
132 | -- rules-wrong2.yaml --
133 | rules:
134 | - dport: 3000
135 | saddr: 127.0.0.1
136 | sport: 80
137 | iface: lo
138 | proto: tcp
139 | - dport: 3002
140 | saddr: 127.0.0.1
141 | sport: 80
142 | iface:
143 | proto: tcp
144 | - dport: 3001
145 | saddr: 127.0.0.1
146 | sport: 80
147 | iface: lo
148 | proto: tcp
149 |
150 | -- rules-wrong3.yaml --
151 | rules:
152 | - dport: 3000
153 | saddr: 127.0.0.1
154 | sport: 80
155 | iface: lo
156 | proto: tcp
157 | - dport: 3002
158 | saddr: 127.0.0.1
159 | sport: 80
160 | iface: aaa
161 | proto: tcp
162 | - dport: 3001
163 | saddr: 127.0.0.1
164 | sport: 80
165 | iface: lo
166 | proto: tcp
167 |
168 | -- rules-wrong4.yaml --
169 | rules:
170 | - dport: 3000
171 | saddr: 127.0.0.1
172 | sport: 80
173 | iface: lo
174 | proto: tcp
175 | - dport:
176 | saddr: 127.0.0.1
177 | sport: 80
178 | iface: lo
179 | proto: tcp
180 | - dport: 3001
181 | saddr: 127.0.0.1
182 | sport: 80
183 | iface: lo
184 | proto: tcp
185 |
186 | -- rules-wrong5.yaml --
187 | rules:
188 | - dport: 3000
189 | saddr: 127.0.0.1
190 | sport: 80
191 | iface: lo
192 | proto: tcp
193 | - dport: 3001
194 | saddr:
195 | sport: 80
196 | iface: lo
197 | proto: tcp
198 | - dport: 3001
199 | saddr: 127.0.0.1
200 | sport: 80
201 | iface: lo
202 | proto: tcp
203 |
204 | -- rules-wrong6.yaml --
205 | rules:
206 | - dport: 3000
207 | saddr: 127.0.0.1
208 | sport: 80
209 | iface: lo
210 | proto: tcp
211 | - dport: 3001
212 | saddr: 127.0.0.1
213 | sport:
214 | iface: lo
215 | proto: tcp
216 | - dport: 3001
217 | saddr: 127.0.0.1
218 | sport: 80
219 | iface: lo
220 | proto: tcp
221 |
--------------------------------------------------------------------------------
/tests/apply_trace.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'apply' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec_cmd fwdctl apply --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | # test primarly subcommand 'apply'
13 | exec_cmd fwdctl apply --file rules-2.yaml
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 | fwd_exists lo tcp 3001 127.0.0.1 80
16 | fwd_exists lo udp 3002 127.0.0.1 80
17 |
18 | # clean up first test
19 | exec fwdctl delete -n 1
20 | exec fwdctl delete -n 1
21 | exec fwdctl delete -n 1
22 | exec fwdctl delete -n 1
23 |
24 | exec_cmd fwdctl apply
25 |
26 | # clean up second test
27 | exec fwdctl delete -n 1
28 | exec fwdctl delete -n 1
29 | exec fwdctl delete -n 1
30 |
31 | # removing the default rules.yml file
32 | # it should fail
33 | exec rm rules.yml
34 | exec_cmd fwdctl apply
35 |
36 | # should not apply
37 | # (wrong protocol)
38 | exec_cmd fwdctl apply --file rules-wrong.yaml
39 | ! fwd_exists lo tcp 3000 127.0.0.1 80
40 | ! fwd_exists lo tcp 3001 127.0.0.1 80
41 | ! fwd_exists lo tcp 3002 127.0.0.1 80
42 |
43 | # should not apply
44 | # (empty interface)
45 | exec_cmd fwdctl apply --file rules-wrong2.yaml
46 | ! fwd_exists lo tcp 3000 127.0.0.1 80
47 | ! fwd_exists lo tcp 3001 127.0.0.1 80
48 | ! fwd_exists lo tcp 3002 127.0.0.1 80
49 |
50 | # should not apply
51 | # (interface doesn't exist)
52 | exec_cmd fwdctl apply --file rules-wrong3.yaml
53 | ! fwd_exists lo tcp 3000 127.0.0.1 80
54 | ! fwd_exists lo tcp 3001 127.0.0.1 80
55 | ! fwd_exists lo tcp 3002 127.0.0.1 80
56 |
57 | # should not apply
58 | # (empty destination port)
59 | exec_cmd fwdctl apply --file rules-wrong4.yaml
60 | ! fwd_exists lo tcp 3000 127.0.0.1 80
61 | ! fwd_exists lo tcp 3001 127.0.0.1 80
62 | ! fwd_exists lo tcp 3002 127.0.0.1 80
63 |
64 | # should not apply
65 | # (empty source address)
66 | exec_cmd fwdctl apply --file rules-wrong5.yaml
67 | ! fwd_exists lo tcp 3000 127.0.0.1 80
68 | ! fwd_exists lo tcp 3001 127.0.0.1 80
69 | ! fwd_exists lo tcp 3002 127.0.0.1 80
70 |
71 | # should not apply
72 | # (empty source port)
73 | exec_cmd fwdctl apply --file rules-wrong6.yaml
74 | ! fwd_exists lo tcp 3000 127.0.0.1 80
75 | ! fwd_exists lo tcp 3001 127.0.0.1 80
76 | ! fwd_exists lo tcp 3002 127.0.0.1 80
77 |
78 | -- rules.yml --
79 | rules:
80 | - dport: 3000
81 | saddr: 127.0.0.1
82 | sport: 80
83 | iface: lo
84 | proto: tcp
85 | - dport: 3001
86 | saddr: 127.0.0.1
87 | sport: 80
88 | iface: lo
89 | proto: tcp
90 | - dport: 3002
91 | saddr: 127.0.0.1
92 | sport: 80
93 | iface: lo
94 | proto: udp
95 |
96 | -- rules-2.yaml --
97 | rules:
98 | - dport: 3000
99 | saddr: 127.0.0.1
100 | sport: 80
101 | iface: lo
102 | proto: tcp
103 | - dport: 3001
104 | saddr: 127.0.0.1
105 | sport: 80
106 | iface: lo
107 | proto: tcp
108 | - dport: 3002
109 | saddr: 127.0.0.1
110 | sport: 80
111 | iface: lo
112 | proto: udp
113 |
114 | -- rules-wrong.yaml --
115 | rules:
116 | - dport: 3000
117 | saddr: 127.0.0.1
118 | sport: 80
119 | iface: lo
120 | proto: tcp
121 | - dport: 3001
122 | saddr: 127.0.0.1
123 | sport: 80
124 | iface: lo
125 | proto: tcp
126 | - dport: 3002
127 | saddr: 127.0.0.1
128 | sport: 80
129 | iface: lo
130 | proto: xxx
131 |
132 | -- rules-wrong2.yaml --
133 | rules:
134 | - dport: 3000
135 | saddr: 127.0.0.1
136 | sport: 80
137 | iface: lo
138 | proto: tcp
139 | - dport: 3002
140 | saddr: 127.0.0.1
141 | sport: 80
142 | iface:
143 | proto: tcp
144 | - dport: 3001
145 | saddr: 127.0.0.1
146 | sport: 80
147 | iface: lo
148 | proto: tcp
149 |
150 | -- rules-wrong3.yaml --
151 | rules:
152 | - dport: 3000
153 | saddr: 127.0.0.1
154 | sport: 80
155 | iface: lo
156 | proto: tcp
157 | - dport: 3002
158 | saddr: 127.0.0.1
159 | sport: 80
160 | iface: aaa
161 | proto: tcp
162 | - dport: 3001
163 | saddr: 127.0.0.1
164 | sport: 80
165 | iface: lo
166 | proto: tcp
167 |
168 | -- rules-wrong4.yaml --
169 | rules:
170 | - dport: 3000
171 | saddr: 127.0.0.1
172 | sport: 80
173 | iface: lo
174 | proto: tcp
175 | - dport:
176 | saddr: 127.0.0.1
177 | sport: 80
178 | iface: lo
179 | proto: tcp
180 | - dport: 3001
181 | saddr: 127.0.0.1
182 | sport: 80
183 | iface: lo
184 | proto: tcp
185 |
186 | -- rules-wrong5.yaml --
187 | rules:
188 | - dport: 3000
189 | saddr: 127.0.0.1
190 | sport: 80
191 | iface: lo
192 | proto: tcp
193 | - dport: 3001
194 | saddr:
195 | sport: 80
196 | iface: lo
197 | proto: tcp
198 | - dport: 3001
199 | saddr: 127.0.0.1
200 | sport: 80
201 | iface: lo
202 | proto: tcp
203 |
204 | -- rules-wrong6.yaml --
205 | rules:
206 | - dport: 3000
207 | saddr: 127.0.0.1
208 | sport: 80
209 | iface: lo
210 | proto: tcp
211 | - dport: 3001
212 | saddr: 127.0.0.1
213 | sport:
214 | iface: lo
215 | proto: tcp
216 | - dport: 3001
217 | saddr: 127.0.0.1
218 | sport: 80
219 | iface: lo
220 | proto: tcp
221 |
--------------------------------------------------------------------------------
/tests/create.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'create' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec fwdctl create --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | # test primarly subcommand 'create'
13 | exec fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 |
16 | # test alternative name 'add'
17 | exec fwdctl add -d 3001 -s 127.0.0.1 -p 80 -i lo
18 | fwd_exists lo tcp 3001 127.0.0.1 80
19 |
20 | # test creation of udp rule
21 | exec fwdctl create -d 3002 -s 127.0.0.1 -p 80 -i lo -P udp
22 | fwd_exists lo udp 3002 127.0.0.1 80
23 |
24 | exec fwdctl list -o json
25 | cmp stdout fwdctl_list.json
26 |
27 | # clean up environment
28 | exec fwdctl delete -n 1
29 | exec fwdctl delete -n 1
30 | exec fwdctl delete -n 1
31 |
32 | # create rule without specifying interface
33 | exec fwdctl create -d 3003 -s 127.0.0.1 -p 80
34 | fwd_exists lo tcp 3003 127.0.0.1 80
35 | exec fwdctl delete -n 1
36 |
37 | # should not create rules
38 | ! exec fwdctl create -d 3003 -s 127.0.0.1
39 | ! exec fwdctl create -d 3003 -p 80
40 | ! exec fwdctl create -s 127.0.0.1 -p 80
41 | ! exec fwdctl create -i lo -P tcp
42 |
43 | -- fwdctl_list.json --
44 | {
45 | "02277b77be4aec43a6e91433e2fc1fb0": {
46 | "iface": "lo",
47 | "proto": "udp",
48 | "dport": 3002,
49 | "saddr": "127.0.0.1",
50 | "sport": 80
51 | },
52 | "0be1c5f4141015ca6a8e873344da06e6": {
53 | "iface": "lo",
54 | "proto": "tcp",
55 | "dport": 3000,
56 | "saddr": "127.0.0.1",
57 | "sport": 80
58 | },
59 | "73d008929b591e12220cef0bb9a2710e": {
60 | "iface": "lo",
61 | "proto": "tcp",
62 | "dport": 3001,
63 | "saddr": "127.0.0.1",
64 | "sport": 80
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/create_trace.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'create' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec_cmd fwdctl create --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | # test primarly subcommand 'create'
13 | exec_cmd fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 |
16 | # test alternative name 'add'
17 | exec fwdctl add -d 3001 -s 127.0.0.1 -p 80 -i lo
18 | fwd_exists lo tcp 3001 127.0.0.1 80
19 |
20 | # test creation of udp rule
21 | exec_cmd fwdctl create -d 3002 -s 127.0.0.1 -p 80 -i lo -P udp
22 | fwd_exists lo udp 3002 127.0.0.1 80
23 |
24 | exec fwdctl list -o json
25 | cmp stdout fwdctl_list.json
26 |
27 | # clean up environment
28 | exec fwdctl delete -n 1
29 | exec fwdctl delete -n 1
30 | exec fwdctl delete -n 1
31 |
32 | # create rule without specifying interface
33 | exec_cmd fwdctl create -d 3003 -s 127.0.0.1 -p 80
34 | fwd_exists lo tcp 3003 127.0.0.1 80
35 | exec fwdctl delete -n 1
36 |
37 | # should not create rules
38 | exec_cmd fwdctl create -d 3003 -s 127.0.0.1
39 | exec_cmd fwdctl create -d 3003 -p 80
40 | exec_cmd fwdctl create -s 127.0.0.1 -p 80
41 | exec_cmd fwdctl create -i lo -P tcp
42 |
43 | -- fwdctl_list.json --
44 | {
45 | "02277b77be4aec43a6e91433e2fc1fb0": {
46 | "iface": "lo",
47 | "proto": "udp",
48 | "dport": 3002,
49 | "saddr": "127.0.0.1",
50 | "sport": 80
51 | },
52 | "0be1c5f4141015ca6a8e873344da06e6": {
53 | "iface": "lo",
54 | "proto": "tcp",
55 | "dport": 3000,
56 | "saddr": "127.0.0.1",
57 | "sport": 80
58 | },
59 | "73d008929b591e12220cef0bb9a2710e": {
60 | "iface": "lo",
61 | "proto": "tcp",
62 | "dport": 3001,
63 | "saddr": "127.0.0.1",
64 | "sport": 80
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/daemon.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'daemon ' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec fwdctl daemon
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | exec fwdctl daemon --help
13 | stdout 'Usage:'
14 |
15 | exec fwdctl daemon start --help
16 | stdout 'Usage:'
17 |
18 | exec fwdctl daemon stop --help
19 | stdout 'Usage:'
20 |
21 | ! exec fwdctl daemon start --file file_doesnt_exists.yml
22 |
23 | # should apply the rules from default file
24 | ! exec fwdctl daemon start &
25 | exec sleep 5
26 | fwd_exists lo tcp 3000 127.0.0.1 80
27 | fwd_exists lo tcp 3001 127.0.0.1 80
28 | fwd_exists lo tcp 3002 127.0.0.1 80
29 |
30 | # should change one rule out of three
31 | exec cp rules-2.yml rules.yml
32 | exec sleep 5
33 | fwd_exists lo tcp 3000 127.0.0.1 80
34 | fwd_exists lo tcp 3001 127.0.0.1 80
35 | fwd_exists lo udp 3002 127.0.0.1 81
36 |
37 | # should leave just one single rule
38 | exec cp rules-3.yml rules.yml
39 | exec sleep 5
40 | fwd_exists lo udp 1111 127.0.0.1 22
41 |
42 | # should remove all the rules since the rules file is empty
43 | exec cp rules-4.yml rules.yml
44 | exec sleep 5
45 | exec fwdctl list -o json
46 | cmp stdout empty-output.txt
47 |
48 | # should not apply rules if file is not valid
49 | exec cp rules-5.yml rules.yml
50 | exec sleep 5
51 | exec fwdctl list -o json
52 | exec fwdctl daemon stop
53 |
54 | -- rules.yml --
55 | rules:
56 | - dport: 3000
57 | saddr: 127.0.0.1
58 | sport: 80
59 | iface: lo
60 | proto: tcp
61 | - dport: 3001
62 | saddr: 127.0.0.1
63 | sport: 80
64 | iface: lo
65 | proto: tcp
66 | - dport: 3002
67 | saddr: 127.0.0.1
68 | sport: 80
69 | iface: lo
70 | proto: tcp
71 |
72 | -- rules-2.yml --
73 | rules:
74 | - dport: 3000
75 | saddr: 127.0.0.1
76 | sport: 80
77 | iface: lo
78 | proto: tcp
79 | - dport: 3001
80 | saddr: 127.0.0.1
81 | sport: 80
82 | iface: lo
83 | proto: tcp
84 | - dport: 3002
85 | saddr: 127.0.0.1
86 | sport: 81
87 | iface: lo
88 | proto: udp
89 |
90 | -- rules-3.yml --
91 | rules:
92 | - dport: 1111
93 | saddr: 127.0.0.1
94 | sport: 22
95 | iface: lo
96 | proto: udp
97 |
98 | -- rules-4.yml --
99 | rules:
100 |
101 | -- rules-5.yml --
102 | rules:
103 | - dport:
104 | saddr: 127.0.0.1
105 | sport: 80
106 | iface: lo
107 | proto: tcp
108 | - dport: 3001
109 | saddr: 127.0.0.1
110 | sport: 80
111 | iface: lo
112 | proto: tcp
113 |
114 | -- empty-output.txt --
115 | {}
116 |
--------------------------------------------------------------------------------
/tests/daemon_trace.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'daemon' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec_cmd fwdctl daemon
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec_cmd fwdctl delete --all
11 |
12 | exec_cmd fwdctl daemon --help
13 | stdout 'Usage:'
14 |
15 | exec_cmd fwdctl daemon start --help
16 | stdout 'Usage:'
17 |
18 | exec_cmd fwdctl daemon stop --help
19 | stdout 'Usage:'
20 |
21 | exec_cmd fwdctl daemon start --file file_doesnt_exists.yml
22 |
23 | # should apply the rules from default file
24 | exec_cmd fwdctl daemon start &
25 | exec sleep 15
26 | fwd_exists lo tcp 3000 127.0.0.1 80
27 | fwd_exists lo tcp 3001 127.0.0.1 80
28 | fwd_exists lo tcp 3002 127.0.0.1 80
29 |
30 | # should change one rule out of three
31 | exec cp rules-2.yml rules.yml
32 | exec sleep 5
33 | fwd_exists lo tcp 3000 127.0.0.1 80
34 | fwd_exists lo tcp 3001 127.0.0.1 80
35 | fwd_exists lo udp 3002 127.0.0.1 81
36 |
37 | # should leave just one single rule
38 | exec cp rules-3.yml rules.yml
39 | exec sleep 5
40 | fwd_exists lo udp 1111 127.0.0.1 22
41 |
42 | # should remove all the rules since the rules file is empty
43 | exec cp rules-4.yml rules.yml
44 | exec sleep 5
45 | exec fwdctl list -o json
46 | cmp stdout empty-output.txt
47 |
48 | # should not apply rules if file is not valid
49 | exec cp rules-5.yml rules.yml
50 | exec sleep 5
51 | exec fwdctl list -o json
52 | exec fwdctl daemon stop
53 |
54 | -- rules.yml --
55 | rules:
56 | - dport: 3000
57 | saddr: 127.0.0.1
58 | sport: 80
59 | iface: lo
60 | proto: tcp
61 | - dport: 3001
62 | saddr: 127.0.0.1
63 | sport: 80
64 | iface: lo
65 | proto: tcp
66 | - dport: 3002
67 | saddr: 127.0.0.1
68 | sport: 80
69 | iface: lo
70 | proto: tcp
71 |
72 | -- rules-2.yml --
73 | rules:
74 | - dport: 3000
75 | saddr: 127.0.0.1
76 | sport: 80
77 | iface: lo
78 | proto: tcp
79 | - dport: 3001
80 | saddr: 127.0.0.1
81 | sport: 80
82 | iface: lo
83 | proto: tcp
84 | - dport: 3002
85 | saddr: 127.0.0.1
86 | sport: 81
87 | iface: lo
88 | proto: udp
89 |
90 | -- rules-3.yml --
91 | rules:
92 | - dport: 1111
93 | saddr: 127.0.0.1
94 | sport: 22
95 | iface: lo
96 | proto: udp
97 |
98 | -- rules-4.yml --
99 | rules:
100 |
101 | -- rules-5.yml --
102 | rules:
103 | - dport:
104 | saddr: 127.0.0.1
105 | sport: 80
106 | iface: lo
107 | proto: tcp
108 | - dport: 3001
109 | saddr: 127.0.0.1
110 | sport: 80
111 | iface: lo
112 | proto: tcp
113 |
114 | -- empty-output.txt --
115 | {}
116 |
--------------------------------------------------------------------------------
/tests/delete.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'delete' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec fwdctl delete --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | # delete e simple rule by passing id
13 | exec fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 | exec fwdctl delete -n 1
16 |
17 | # delete list of rules by using a rules.yml file
18 | exec fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
19 | fwd_exists lo tcp 3000 127.0.0.1 80
20 | exec fwdctl create -d 3001 -s 127.0.0.1 -p 80 -i lo
21 | fwd_exists lo tcp 3001 127.0.0.1 80
22 | exec fwdctl create -d 3002 -s 127.0.0.1 -p 80 -i lo
23 | fwd_exists lo tcp 3002 127.0.0.1 80
24 | exec fwdctl delete --file rules.yml
25 |
26 | # delete multiple ids
27 | exec fwdctl apply -f rules.yml
28 | fwd_exists lo tcp 3000 127.0.0.1 80
29 | fwd_exists lo tcp 3001 127.0.0.1 80
30 | fwd_exists lo tcp 3002 127.0.0.1 80
31 | exec fwdctl delete -n 3
32 | exec fwdctl delete -n 2
33 | exec fwdctl delete -n 1
34 |
35 | # create forwards and then delete them with '--all#
36 | exec fwdctl apply -f rules.yml
37 | fwd_exists lo tcp 3000 127.0.0.1 80
38 | fwd_exists lo tcp 3001 127.0.0.1 80
39 | fwd_exists lo tcp 3002 127.0.0.1 80
40 | exec fwdctl delete --all
41 | ! fwd_exists lo tcp 3000 127.0.0.1 80
42 | ! fwd_exists lo tcp 3001 127.0.0.1 80
43 | ! fwd_exists lo tcp 3002 127.0.0.1 80
44 |
45 | # unable to use all the flags at the same time
46 | ! exec fwdctl delete -n 1 -a -f rules.yml
47 |
48 | exec fwdctl apply
49 | fwd_exists lo tcp 3000 127.0.0.1 80
50 | fwd_exists lo tcp 3001 127.0.0.1 80
51 | fwd_exists lo tcp 3002 127.0.0.1 80
52 | exec fwdctl delete
53 | ! fwd_exists lo tcp 3000 127.0.0.1 80
54 | ! fwd_exists lo tcp 3001 127.0.0.1 80
55 | ! fwd_exists lo tcp 3002 127.0.0.1 80
56 |
57 | ! exec fwdctl delete -f file_doesnt_exists.yml
58 |
59 | -- rules.yml --
60 | rules:
61 | - dport: 3000
62 | saddr: 127.0.0.1
63 | sport: 80
64 | iface: lo
65 | proto: tcp
66 | - dport: 3001
67 | saddr: 127.0.0.1
68 | sport: 80
69 | iface: lo
70 | proto: tcp
71 | - dport: 3002
72 | saddr: 127.0.0.1
73 | sport: 80
74 | iface: lo
75 | proto: tcp
76 |
--------------------------------------------------------------------------------
/tests/delete_trace.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'delete' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec_cmd fwdctl delete --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec_cmd fwdctl delete --all
11 |
12 | # delete e simple rule by passing id
13 | exec fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 | exec_cmd fwdctl delete -n 1
16 |
17 | # delete list of rules by using a rules.yml file
18 | exec fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
19 | fwd_exists lo tcp 3000 127.0.0.1 80
20 | exec fwdctl create -d 3001 -s 127.0.0.1 -p 80 -i lo
21 | fwd_exists lo tcp 3001 127.0.0.1 80
22 | exec fwdctl create -d 3002 -s 127.0.0.1 -p 80 -i lo
23 | fwd_exists lo tcp 3002 127.0.0.1 80
24 | exec_cmd fwdctl delete --file rules.yml
25 |
26 | # delete multiple ids
27 | exec fwdctl apply -f rules.yml
28 | fwd_exists lo tcp 3000 127.0.0.1 80
29 | fwd_exists lo tcp 3001 127.0.0.1 80
30 | fwd_exists lo tcp 3002 127.0.0.1 80
31 | exec_cmd fwdctl delete -n 3
32 | exec_cmd fwdctl delete -n 2
33 | exec_cmd fwdctl delete -n 1
34 |
35 | # create forwards and then delete them with '--all#
36 | exec fwdctl apply -f rules.yml
37 | fwd_exists lo tcp 3000 127.0.0.1 80
38 | fwd_exists lo tcp 3001 127.0.0.1 80
39 | fwd_exists lo tcp 3002 127.0.0.1 80
40 | exec_cmd fwdctl delete --all
41 | ! fwd_exists lo tcp 3000 127.0.0.1 80
42 | ! fwd_exists lo tcp 3001 127.0.0.1 80
43 | ! fwd_exists lo tcp 3002 127.0.0.1 80
44 |
45 | # unable to use all the flags at the same time
46 | exec_cmd fwdctl delete -n 1 -a -f rules.yml
47 |
48 | exec fwdctl apply
49 | fwd_exists lo tcp 3000 127.0.0.1 80
50 | fwd_exists lo tcp 3001 127.0.0.1 80
51 | fwd_exists lo tcp 3002 127.0.0.1 80
52 | exec_cmd fwdctl delete
53 | ! fwd_exists lo tcp 3000 127.0.0.1 80
54 | ! fwd_exists lo tcp 3001 127.0.0.1 80
55 | ! fwd_exists lo tcp 3002 127.0.0.1 80
56 |
57 | exec_cmd fwdctl delete -f file_doesnt_exists.yml
58 |
59 | -- rules.yml --
60 | rules:
61 | - dport: 3000
62 | saddr: 127.0.0.1
63 | sport: 80
64 | iface: lo
65 | proto: tcp
66 | - dport: 3001
67 | saddr: 127.0.0.1
68 | sport: 80
69 | iface: lo
70 | proto: tcp
71 | - dport: 3002
72 | saddr: 127.0.0.1
73 | sport: 80
74 | iface: lo
75 | proto: tcp
76 |
--------------------------------------------------------------------------------
/tests/generate.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'generate' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec fwdctl generate
7 | stdout 'Usage:'
8 |
9 | exec fwdctl generate --help
10 | stdout 'Usage:'
11 |
12 | # test 'generate rules' command
13 | exec fwdctl generate rules --help
14 | stdout 'Usage:'
15 |
16 | ! exec fwdctl generate rules -O rules.yaml
17 | stdout 'output path is not absolute:'
18 |
19 | ! exec fwdctl generate rules -O /tmp/rules.yaml
20 |
21 | exec fwdctl generate rules -O /tmp/
22 | exists /tmp/rules.yml
23 | cmp /tmp/rules.yml rules.yml
24 |
25 | # test 'generate systemd' command
26 | exec fwdctl generate systemd --help
27 | stdout 'Usage:'
28 |
29 | ! exec fwdctl generate systemd --file /tmp/rules.yml
30 | ! exec fwdctl generate systemd --installation-path /usr/local/bin
31 | ! exec fwdctl generate systemd --type "fork"
32 | ! exec fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin --type fork
33 |
34 | # test with options '--type oneshot' (default)
35 | exec fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin -O /tmp/
36 | cmp /tmp/fwdctl.service fwdctl-oneshot.service
37 |
38 | # test with options '--type fork'
39 | exec fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin --type fork -O /tmp/
40 | cmp /tmp/fwdctl.service fwdctl-fork.service
41 |
42 | # test with options '--type null' (does not exist)
43 | ! exec fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin --type null -O /tmp/
44 |
45 | -- rules.yml --
46 | rules:
47 | - dport:
48 | saddr:
49 | sport:
50 | iface:
51 | proto:
52 | -- fwdctl-oneshot.service --
53 | [Unit]
54 | Description=fwdctl systemd service
55 | After=network.target
56 |
57 | [Service]
58 | Type=oneshot
59 | ExecStart=/usr/local/bin/fwdctl apply --file=/tmp/rules.yml
60 | StandardOutput=journal
61 |
62 | [Install]
63 | WantedBy=multi-user.target
64 | -- fwdctl-fork.service --
65 | [Unit]
66 | Description=fwdctl systemd service
67 | After=network.target
68 |
69 | [Service]
70 | Type=fork
71 | ExecStart=/usr/local/bin/fwdctl daemon start --file=/tmp/rules.yml
72 | ExecStop=/usr/local/bin/fwdctl daemon stop
73 | Restart=always
74 | RestartSec=5s
75 | StandardOutput=journal
76 |
77 | [Install]
78 | WantedBy=multi-user.target
79 |
--------------------------------------------------------------------------------
/tests/generate_trace.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'generate' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec_cmd fwdctl generate
7 | stdout 'Usage:'
8 |
9 | exec_cmd fwdctl generate --help
10 | stdout 'Usage:'
11 |
12 | # test 'generate rules' command
13 | exec_cmd fwdctl generate rules --help
14 | stdout 'Usage:'
15 |
16 | exec_cmd fwdctl generate rules -O rules.yaml
17 | stdout 'output path is not absolute:'
18 |
19 | exec_cmd fwdctl generate rules -O /tmp/rules.yaml
20 |
21 | exec_cmd fwdctl generate rules -O /tmp/
22 | exists /tmp/rules.yml
23 | cmp /tmp/rules.yml rules.yml
24 |
25 | # test 'generate systemd' command
26 | exec_cmd fwdctl generate systemd --help
27 | stdout 'Usage:'
28 |
29 | exec_cmd fwdctl generate systemd --file /tmp/rules.yml
30 | exec_cmd fwdctl generate systemd --installation-path /usr/local/bin
31 | exec_cmd fwdctl generate systemd --type "fork"
32 | exec_cmd fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin --type fork
33 |
34 | # test with options '--type oneshot' (default)
35 | exec_cmd fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin -O /tmp/
36 | cmp /tmp/fwdctl.service fwdctl-oneshot.service
37 |
38 | # test with options '--type fork'
39 | exec_cmd fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin --type fork -O /tmp/
40 | cmp /tmp/fwdctl.service fwdctl-fork.service
41 |
42 | # test with options '--type null' (does not exist)
43 | exec_cmd fwdctl generate systemd --file /tmp/rules.yml --installation-path /usr/local/bin --type null -O /tmp/
44 |
45 | -- rules.yml --
46 | rules:
47 | - dport:
48 | saddr:
49 | sport:
50 | iface:
51 | proto:
52 | -- fwdctl-oneshot.service --
53 | [Unit]
54 | Description=fwdctl systemd service
55 | After=network.target
56 |
57 | [Service]
58 | Type=oneshot
59 | ExecStart=/usr/local/bin/fwdctl apply --file=/tmp/rules.yml
60 | StandardOutput=journal
61 |
62 | [Install]
63 | WantedBy=multi-user.target
64 | -- fwdctl-fork.service --
65 | [Unit]
66 | Description=fwdctl systemd service
67 | After=network.target
68 |
69 | [Service]
70 | Type=fork
71 | ExecStart=/usr/local/bin/fwdctl daemon start --file=/tmp/rules.yml
72 | ExecStop=/usr/local/bin/fwdctl daemon stop
73 | Restart=always
74 | RestartSec=5s
75 | StandardOutput=journal
76 |
77 | [Install]
78 | WantedBy=multi-user.target
79 |
--------------------------------------------------------------------------------
/tests/list.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'create' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec fwdctl list --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | # test primarly subcommand 'create'
13 | exec fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 |
16 | # compare table view
17 | exec fwdctl list
18 | cmp stdout list-table.txt
19 |
20 | # compare json view
21 | exec fwdctl list -o json
22 | cmp stdout list-json.txt
23 |
24 | # compare yaml view
25 | exec fwdctl list -o yaml
26 | cmp stdout list-yaml.txt
27 |
28 | # check alias works
29 | exec fwdctl ls
30 |
31 | -- list-table.txt --
32 | +--------+-----------+----------+---------------+-------------+---------------+
33 | | NUMBER | INTERFACE | PROTOCOL | EXTERNAL PORT | INTERNAL IP | INTERNAL PORT |
34 | +--------+-----------+----------+---------------+-------------+---------------+
35 | | 1 | lo | tcp | 3000 | 127.0.0.1 | 80 |
36 | +--------+-----------+----------+---------------+-------------+---------------+
37 | -- list-json.txt --
38 | {
39 | "0be1c5f4141015ca6a8e873344da06e6": {
40 | "iface": "lo",
41 | "proto": "tcp",
42 | "dport": 3000,
43 | "saddr": "127.0.0.1",
44 | "sport": 80
45 | }
46 | }
47 | -- list-yaml.txt --
48 | 0be1c5f4141015ca6a8e873344da06e6:
49 | iface: lo
50 | proto: tcp
51 | dport: 3000
52 | saddr: 127.0.0.1
53 | sport: 80
54 |
55 |
--------------------------------------------------------------------------------
/tests/list_trace.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'create' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec_cmd fwdctl list --help
7 | stdout 'Usage:'
8 |
9 | # remove all previously applied forwards
10 | exec fwdctl delete --all
11 |
12 | # test primarly subcommand 'create'
13 | exec fwdctl create -d 3000 -s 127.0.0.1 -p 80 -i lo
14 | fwd_exists lo tcp 3000 127.0.0.1 80
15 |
16 | # compare table view
17 | exec_cmd fwdctl list
18 | cmp stdout list-table.txt
19 |
20 | # compare json view
21 | exec_cmd fwdctl list -o json
22 | cmp stdout list-json.txt
23 |
24 | # compare yaml view
25 | exec_cmd fwdctl list -o yaml
26 | cmp stdout list-yaml.txt
27 |
28 | # check alias works
29 | exec_cmd fwdctl ls
30 |
31 | -- list-table.txt --
32 | stdout: +--------+-----------+----------+---------------+-------------+---------------+
33 | stdout: | NUMBER | INTERFACE | PROTOCOL | EXTERNAL PORT | INTERNAL IP | INTERNAL PORT |
34 | stdout: +--------+-----------+----------+---------------+-------------+---------------+
35 | stdout: | 1 | lo | tcp | 3000 | 127.0.0.1 | 80 |
36 | stdout: +--------+-----------+----------+---------------+-------------+---------------+
37 | -- list-json.txt --
38 | stdout: {
39 | stdout: "0be1c5f4141015ca6a8e873344da06e6": {
40 | stdout: "iface": "lo",
41 | stdout: "proto": "tcp",
42 | stdout: "dport": 3000,
43 | stdout: "saddr": "127.0.0.1",
44 | stdout: "sport": 80
45 | stdout: }
46 | stdout: }
47 | -- list-yaml.txt --
48 | stdout: 0be1c5f4141015ca6a8e873344da06e6:
49 | stdout: iface: lo
50 | stdout: proto: tcp
51 | stdout: dport: 3000
52 | stdout: saddr: 127.0.0.1
53 | stdout: sport: 80
54 | stdout:
55 |
--------------------------------------------------------------------------------
/tests/version.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'apply' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec fwdctl version --help
7 | stdout 'Usage:'
8 |
9 | exec fwdctl version
10 | stdout 'dev'
11 |
12 | # use an invalid argument
13 | ! exec fwdctl version -x
14 | stdout 'unknown shorthand flag:'
--------------------------------------------------------------------------------
/tests/version_trace.txtar:
--------------------------------------------------------------------------------
1 | # this testscript test the 'apply' command
2 |
3 | # if go is not installed, then skip
4 | [!exec:go] skip
5 |
6 | exec_cmd fwdctl version --help
7 | stdout 'Usage:'
8 |
9 | exec_cmd fwdctl version
10 | stdout 'dev'
11 |
12 | # use an invalid argument
13 | exec_cmd fwdctl version -x
14 | stdout 'unknown shorthand flag:'
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/base64"
6 | "fmt"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "regexp"
11 | "strings"
12 |
13 | "github.com/alegrey91/fwdctl/pkg/iptables"
14 | goiptables "github.com/coreos/go-iptables/iptables"
15 | "github.com/rogpeppe/go-internal/testscript"
16 | )
17 |
18 | func fwdExists(ts *testscript.TestScript, neg bool, args []string) {
19 | if len(args) < 5 {
20 | ts.Fatalf("syntax: fwd_exists iface proto dest_port src_addr src_port")
21 | }
22 |
23 | ipt, err := goiptables.New()
24 | if err != nil {
25 | ts.Fatalf("error creating iptables instance: %q", err)
26 | }
27 |
28 | ruleSpec := []string{
29 | "-i", args[0], // interface
30 | "-p", args[1], // protocol
31 | "-m", args[1], // protocol
32 | "--dport", args[2], // destination-port
33 | "-m", "comment", "--comment", "fwdctl",
34 | "-j", iptables.FwdTarget, // target (DNAT)
35 | "--to-destination", args[3] + ":" + args[4], // source-address / source-port
36 | }
37 |
38 | exists, err := ipt.Exists(iptables.FwdTable, iptables.FwdChain, ruleSpec...)
39 | if err != nil {
40 | ts.Fatalf("error checking rule: %v", err)
41 | }
42 | if neg && !exists {
43 | ts.Logf("forward doesn't exist")
44 | return
45 | }
46 | if !exists {
47 | ts.Fatalf("forward doesn't exist")
48 | }
49 | }
50 |
51 | //nolint:all
52 | func execCmd(ts *testscript.TestScript, neg bool, args []string) {
53 | var backgroundSpecifier = regexp.MustCompile(`^&([a-zA-Z_0-9]+&)?$`)
54 | uuid := getRandomString()
55 | workDir, err := os.Getwd()
56 | if err != nil {
57 | ts.Fatalf("unable to find work dir: %v", err)
58 | }
59 | customCommand := []string{
60 | "/usr/sbin/harpoon",
61 | "capture",
62 | "-f",
63 | "main.main",
64 | "--save",
65 | "--directory",
66 | fmt.Sprintf("%s/integration-test-syscalls", workDir),
67 | "--include-cmd-stdout",
68 | "--include-cmd-stderr",
69 | "--name",
70 | fmt.Sprintf("main_main_%s", uuid),
71 | "--",
72 | }
73 |
74 | // find binary path for primary command
75 | cmdPath, err := exec.LookPath(args[0])
76 | if err != nil {
77 | ts.Fatalf("unable to find binary path for %s: %v", args[0], err)
78 | }
79 | args[0] = cmdPath
80 | customCommand = append(customCommand, args...)
81 |
82 | ts.Logf("executing tracing command: %s", strings.Join(customCommand, " "))
83 | // check if command has '&' as last char to be ran in background
84 | if len(args) > 0 && backgroundSpecifier.MatchString(args[len(args)-1]) {
85 | _, err = execBackground(ts, customCommand[0], customCommand[1:len(args)-1]...)
86 | } else {
87 | err = ts.Exec(customCommand[0], customCommand[1:]...)
88 | }
89 | if err != nil {
90 | ts.Logf("[%v]\n", err)
91 | if !neg {
92 | ts.Fatalf("unexpected go command failure")
93 | }
94 | } else {
95 | if neg {
96 | ts.Fatalf("unexpected go command success")
97 | }
98 | }
99 | }
100 |
101 | func execBackground(ts *testscript.TestScript, command string, args ...string) (*exec.Cmd, error) {
102 | cmd := exec.Command(command, args...)
103 | path := ts.MkAbs(".")
104 | dir, _ := filepath.Split(path)
105 |
106 | var stdoutBuf, stderrBuf strings.Builder
107 | cmd.Dir = dir
108 | cmd.Env = append(cmd.Env, "PWD="+dir)
109 | cmd.Stdout = &stdoutBuf
110 | cmd.Stderr = &stderrBuf
111 | return cmd, cmd.Start()
112 | }
113 |
114 | //nolint:all
115 | func getRandomString() string {
116 | b := make([]byte, 4) // 4 bytes will give us 6 base64 characters
117 | _, err := rand.Read(b)
118 | if err != nil {
119 | return ""
120 | }
121 | randomString := base64.URLEncoding.EncodeToString(b)[:6]
122 | return randomString
123 | }
124 |
125 | func customCommands() map[string]func(ts *testscript.TestScript, neg bool, args []string) {
126 | return map[string]func(ts *testscript.TestScript, neg bool, args []string){
127 |
128 | // fwd_exists check that the given forward exists
129 | // invoke as "fwd_exists iface proto dest_port src_addr src_port"
130 | "fwd_exists": fwdExists,
131 | "exec_cmd": execCmd,
132 | }
133 | }
134 |
--------------------------------------------------------------------------------