main bcb81cd563e8 cached
96 files
208.7 KB
66.3k tokens
245 symbols
1 requests
Download .txt
Showing preview only (231K chars total). Download the full file or copy to clipboard to get everything.
Repository: crowdsecurity/cs-firewall-bouncer
Branch: main
Commit: bcb81cd563e8
Files: 96
Total size: 208.7 KB

Directory structure:
gitextract_hkmsgkt_/

├── .envrc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   ├── config.yml
│   │   └── feature_request.yaml
│   ├── dependabot.yml
│   ├── governance.yml
│   ├── release-drafter.yml
│   ├── release.py
│   └── workflows/
│       ├── build-binary-package.yml
│       ├── governance-bot.yaml
│       ├── lint.yml
│       ├── release-drafter.yml
│       ├── tests.yml
│       └── tests_deb.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── Makefile
├── README.md
├── cmd/
│   └── root.go
├── config/
│   ├── crowdsec-firewall-bouncer.service
│   └── crowdsec-firewall-bouncer.yaml
├── debian/
│   ├── changelog
│   ├── compat
│   ├── control
│   ├── crowdsec-firewall-bouncer-iptables.postinst
│   ├── crowdsec-firewall-bouncer-iptables.postrm
│   ├── crowdsec-firewall-bouncer-iptables.preinst
│   ├── crowdsec-firewall-bouncer-iptables.prerm
│   ├── crowdsec-firewall-bouncer-nftables.postinst
│   ├── crowdsec-firewall-bouncer-nftables.postrm
│   ├── crowdsec-firewall-bouncer-nftables.preinst
│   ├── crowdsec-firewall-bouncer-nftables.prerm
│   └── rules
├── flake.nix
├── go.mod
├── go.sum
├── main.go
├── pkg/
│   ├── backend/
│   │   └── backend.go
│   ├── cfg/
│   │   ├── config.go
│   │   └── logging.go
│   ├── dryrun/
│   │   └── dryrun.go
│   ├── ipsetcmd/
│   │   └── ipset.go
│   ├── iptables/
│   │   ├── iptables.go
│   │   ├── iptables_context.go
│   │   ├── iptables_stub.go
│   │   └── metrics.go
│   ├── metrics/
│   │   └── metrics.go
│   ├── nftables/
│   │   ├── metrics.go
│   │   ├── nftables.go
│   │   ├── nftables_context.go
│   │   └── nftables_stub.go
│   ├── pf/
│   │   ├── metrics.go
│   │   ├── metrics_test.go
│   │   ├── pf.go
│   │   └── pf_context.go
│   └── types/
│       └── types.go
├── rpm/
│   ├── SOURCES/
│   │   └── 80-crowdsec-firewall-bouncer.preset
│   └── SPECS/
│       └── crowdsec-firewall-bouncer.spec
├── scripts/
│   ├── _bouncer.sh
│   ├── install.sh
│   ├── uninstall.sh
│   └── upgrade.sh
└── test/
    ├── .python-version
    ├── README.md
    ├── default.env
    ├── pyproject.toml
    ├── pytest.ini
    └── tests/
        ├── __init__.py
        ├── backends/
        │   ├── __init__.py
        │   ├── iptables/
        │   │   ├── __init__.py
        │   │   ├── crowdsec-firewall-bouncer-logging.yaml
        │   │   ├── crowdsec-firewall-bouncer.yaml
        │   │   └── test_iptables.py
        │   ├── mock_lapi.py
        │   ├── nftables/
        │   │   ├── __init__.py
        │   │   ├── crowdsec-firewall-bouncer.yaml
        │   │   └── test_nftables.py
        │   └── utils.py
        ├── bouncer/
        │   ├── __init__.py
        │   ├── test_firewall_bouncer.py
        │   ├── test_iptables_deny_action.py
        │   ├── test_tls.py
        │   └── test_yaml_local.py
        ├── conftest.py
        ├── install/
        │   ├── __init__.py
        │   ├── no_crowdsec/
        │   │   ├── __init__.py
        │   │   ├── test_no_crowdsec_deb.py
        │   │   └── test_no_crowdsec_scripts.py
        │   └── with_crowdsec/
        │       ├── __init__.py
        │       ├── test_crowdsec_deb.py
        │       └── test_crowdsec_scripts.py
        └── pkg/
            ├── __init__.py
            ├── test_build_deb.py
            ├── test_build_rpm.py
            └── test_scripts_nonroot.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .envrc
================================================
use flake


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Bug report
description: Report a bug encountered while operating crowdsec
labels: kind/bug
body:
  - type: textarea
    id: problem
    attributes:
      label: What happened?
      description: |
        Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner.
        If this matter is security related, please disclose it privately to security@crowdsec.net
    validations:
      required: true

  - type: textarea
    id: expected
    attributes:
      label: What did you expect to happen?
    validations:
      required: true

  - type: textarea
    id: repro
    attributes:
      label: How can we reproduce it (as minimally and precisely as possible)?
    validations:
      required: true

  - type: textarea
    id: additional
    attributes:
      label: Anything else we need to know?

  - type: textarea
    id: Version
    attributes:
      label: version
      value: |
        remediation component version:

        <details>

        ```console
        $ crowdsec-firewall-bouncer --version
        # paste output here
        ```

        </details>
    validations:
      required: true
  - type: textarea
    id: CS-Version
    attributes:
      label: crowdsec version
      value: |
        crowdsec version:

        <details>
        
        ```console
        $ crowdsec --version
        # paste output here
        ```

        </details>
    validations:
      required: true

  - type: textarea
    id: osVersion
    attributes:
      label: OS version
      value: |
        <details>

        ```console
        # On Linux:
        $ cat /etc/os-release
        # paste output here
        $ uname -a
        # paste output here

        # On Windows:
        C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
        # paste output here
        ```

        </details>

================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
contact_links:
  - name: Support Request
    url: https://discourse.crowdsec.net
    about: Support request or question relating to Crowdsec

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
name: Feature request
description: Suggest an improvement or a new feature
body:
  - type: textarea
    id: feature
    attributes:
      label: What would you like to be added?
      description: |
        Significant feature requests are unlikely to make progress as issues. Please consider engaging on discord (discord.gg/crowdsec) and forums (https://discourse.crowdsec.net), instead.
      value: |
        For feature request please pick a kind label by removing `<!-- -->` that wrap the example lines below


        <!-- /kind feature -->
        <!-- Completely new feature not currently available  -->

        <!-- /kind enhancement -->
        <!-- Feature is available but this extends or adds extra functionality -->
    validations:
      required: true

  - type: textarea
    id: rationale
    attributes:
      label: Why is this needed?
    validations:
      required: true


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"
    labels:
      - "kind/dependencies"
      - "github-actions"
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "daily"
    labels:
      - "kind/dependencies"
      - "go"
  - package-ecosystem: "uv"
    directory: "test"
    schedule:
      interval: "daily"
    labels:
      - "kind/dependencies"
      - "python"


================================================
FILE: .github/governance.yml
================================================
version: v1

issue:
  captures:
    - regex: 'version: v(.+)-'
      github_release: true
      ignore_case: true
      label: 'version/$CAPTURED'

  labels:
    - prefix: triage
      list: ['accepted']
      multiple: false
      author_association:
        collaborator: true
        member: true
        owner: true
      needs:
        comment: |
          @$AUTHOR: Thanks for opening an issue, it is currently awaiting triage.

          In the meantime, you can:

          1. Check [Documentation](https://docs.crowdsec.net/docs/next/bouncers/firewall) to see if your issue can be self resolved.
          2. You can also join our [Discord](https://discord.gg/crowdsec)

    - prefix: kind
      list: ['feature', 'bug', 'packaging', 'enhancement']
      multiple: false
      author_association:
        author: true
        collaborator: true
        member: true
        owner: true
      needs:
        comment: |
          @$AUTHOR: There are no 'kind' label on this issue. You need a 'kind' label to start the triage process.
          * `/kind feature`
          * `/kind enhancement`
          * `/kind bug`
          * `/kind packaging`


================================================
FILE: .github/release-drafter.yml
================================================
template: |
  ## What’s Changed

  $CHANGES


================================================
FILE: .github/release.py
================================================
#!/usr/bin/env python3

import argparse
import json
import os
import shutil
import subprocess
import sys


def _goos():
    yield 'linux'
    yield 'freebsd'


def _goarch(goos):
    yield '386'
    yield 'amd64'
    yield 'arm'
    yield 'arm64'
    if goos == 'linux':
        yield 'ppc64le'
        yield 's390x'
    yield 'riscv64'


def _goarm(goarch):
    if goarch != 'arm':
        yield ''
        return
    yield '6'
    yield '7'


def _build_tarball(os):
    if os == 'linux':
        yield True
    else:
        yield False


def filename_for_entry(prog_name, entry):
    arch = entry['goarch']
    if entry['goarch'] == 'arm':
        arch += 'v' + entry['goarm']
    ret = f'{prog_name}-{entry["goos"]}-{arch}'
    if entry['build_tarball']:
        ret += '.tgz'
    return ret


def matrix(prog_name):
    for goos in _goos():
        for goarch in _goarch(goos):
            if (goos, goarch) == ('freebsd', 'riscv64'):
                # platform not supported
                # gopsutil/v4@v4.25.8/cpu/cpu_freebsd.go:85:13: undefined: cpuTimes
                continue
            for goarm in _goarm(goarch):
                for build_tarball in _build_tarball(goos):
                    yield {
                        'goos': goos,
                        'goarch': goarch,
                        'goarm': goarm,
                        'build_tarball': build_tarball,
                    }


def print_matrix(prog_name):
    j = {'include': list(matrix(prog_name))}

    if os.isatty(sys.stdout.fileno()):
        print(json.dumps(j, indent=2))
    else:
        print(json.dumps(j))


default_tarball = {
    'goos': 'linux',
    'goarch': 'amd64',
    'goarm': '',
    'build_tarball': True,
}

default_binary = {
    'goos': 'linux',
    'goarch': 'amd64',
    'goarm': '',
    'build_tarball': False,
}


def run_build(prog_name):
    # call the makefile for each matrix entry

    default_tarball_filename = None
    default_binary_filename = None

    for entry in matrix(prog_name):
        env = {'GOOS': entry['goos'], 'GOARCH': entry['goarch']}

        if entry['goarm']:
            env['GOARM'] = entry['goarm']

        if entry['build_tarball']:
            target = 'tarball'
        else:
            target = 'binary'

        print(f"Running make {target} for {env}")

        subprocess.run(['make', target], env=os.environ | env, check=True)

        want_filename = filename_for_entry(prog_name, entry)

        if entry['build_tarball']:
            os.rename(f'{prog_name}.tgz', want_filename)
        else:
            os.rename(f'{prog_name}', want_filename)

        # if this is the default tarball or binary, save the filename
        # we'll use it later to publish a "default" package

        if entry == default_tarball:
            default_tarball_filename = want_filename

        if entry == default_binary:
            default_binary_filename = want_filename

        # Remove the directory to reuse it
        subprocess.run(['make', 'clean-release-dir'], env=os.environ | env, check=True)

    # publish the default tarball and binary
    if default_tarball_filename:
        shutil.copy(default_tarball_filename, f'{prog_name}.tgz')

    if default_binary_filename:
        shutil.copy(default_binary_filename, f'{prog_name}')


def main():
    parser = argparse.ArgumentParser(
        description='Build release binaries and tarballs for all supported platforms')
    parser.add_argument('action', help='Action to perform (ex. run-build, print-matrix)')
    parser.add_argument('prog_name', help='Name of the program (ex. crowdsec-firewall-bouncer)')

    args = parser.parse_args()

    if args.action == 'print-matrix':
        print_matrix(args.prog_name)

    if args.action == 'run-build':
        run_build(args.prog_name)


if __name__ == '__main__':
    main()


================================================
FILE: .github/workflows/build-binary-package.yml
================================================
name: build-binary-package

on:
  release:
    types:
    - prereleased

permissions:
  # Use write for: hub release edit
  contents: write

env:
  PROGRAM_NAME: crowdsec-firewall-bouncer

jobs:
  build:
    name: Build and upload all platforms
    runs-on: ubuntu-latest

    steps:

    - name: Check out repository
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      with:
        fetch-depth: 0

    - name: Set up Go
      uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
      with:
        go-version-file: go.mod

    - name: Build all platforms
      run: |
        # build platform-all first so the .xz vendor file is not removed
        make platform-all vendor

    - name: Upload to release
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        tag_name="${GITHUB_REF##*/}"
        # this will upload the $PROGRAM_NAME-vendor.tar.xz file as well
        gh release upload "$tag_name" $PROGRAM_NAME* vendor.tgz


================================================
FILE: .github/workflows/governance-bot.yaml
================================================
# .github/workflow/governance.yml

on:
  pull_request_target:
    types: [ synchronize, opened, labeled, unlabeled ]
  issues:
    types: [ opened, labeled, unlabeled ]
  issue_comment:
    types: [ created ]

# You can use permissions to modify the default permissions granted to the GITHUB_TOKEN, 
# adding or removing access as required, so that you only allow the minimum required access. 
permissions:
  contents: read
  issues: write
  pull-requests: write
  statuses: write
  checks: write

jobs:
  governance:
    name: Governance
    runs-on: ubuntu-latest
    steps:
      # Semantic versioning, lock to different version: v2, v2.0 or a commit hash.
      - uses: BirthdayResearch/oss-governance-bot@3abd2d1fd2376ba9990fbc795e7a4c54254e9c61 # v4.0.0
        with:
          # You can use a PAT to post a comment/label/status so that it shows up as a user instead of github-actions
          github-token: ${{secrets.GITHUB_TOKEN}} # optional, default to '${{ github.token }}'
          config-path: .github/governance.yml # optional, default to '.github/governance.yml'


================================================
FILE: .github/workflows/lint.yml
================================================
name: Static Analysis

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]


jobs:
  build:
    name: "golangci-lint + codeql"
    runs-on: ubuntu-latest

    permissions:
      security-events: write

    steps:
      - name: Check out code into the Go module directory
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Set up Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version-file: go.mod

      - name: Initialize CodeQL
        uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
        with:
          languages: go, python

      - name: Build
        run: |
          make build

      - name: golangci-lint
        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
        with:
          version: v2.9
          args: --issues-exit-code=1 --timeout 10m
          only-new-issues: false

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1


================================================
FILE: .github/workflows/release-drafter.yml
================================================
name: Release Drafter

on:
  push:
    # branches to consider in the event; optional, defaults to all
    branches:
      - main

permissions:
  contents: read

jobs:
  update_release_draft:
    permissions:
      # write permission is required to create a github release
      contents: write
      # write permission is required for autolabeler
      # otherwise, read permission is required at least
      pull-requests: read
    runs-on: ubuntu-latest
    name: Update the release draft
    steps:
      # Drafts your next Release notes as Pull Requests are merged into "main"
      - uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
        with:
          config-name: release-drafter.yml
          # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
          # config-name: my-config.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/tests.yml
================================================
name: Build + tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

permissions:
  contents: read

jobs:
  build:
    name: "Build + tests"
    runs-on: ubuntu-latest

    steps:

    - name: Check out code into the Go module directory
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      with:
        fetch-depth: 0

    - name: Set up Go
      uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
      with:
        go-version-file: go.mod

    - name: Build
      run: |
        make build

    - name: Run unit tests
      run: |
        go install github.com/kyoh86/richgo@v0.3.12
        set -o pipefail
        make test | richgo testfilter
      env:
        RICHGO_FORCE_COLOR: 1

    - name: Install uv
      uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
      with:
        version: 0.5.24
        enable-cache: true
        cache-dependency-glob: "test/uv.lock"

    - name: "Set up Python"
      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
      with:
        python-version-file: "test/.python-version"

    - name: Install the project
      working-directory: ./test
      run: uv sync --all-extras --dev

    - name: Install functional test dependencies
      run: |
        sudo apt update
        sudo apt install -y nftables iptables ipset
        docker network create net-test

    - name: Run functional tests
      env:
        CROWDSEC_TEST_VERSION: dev
        CROWDSEC_TEST_FLAVORS: full
        CROWDSEC_TEST_NETWORK: net-test
        CROWDSEC_TEST_TIMEOUT: 60
        PYTEST_ADDOPTS: --durations=0 -vv --color=yes -m "not (deb or rpm)"
      working-directory: ./test
      run: |
        # everything except for
        # - install (requires root, ignored by default)
        # - backends (requires root, ignored by default)
        # - deb/rpm (on their own workflows)
        uv run pytest
        # these need root
        sudo -E $(which uv) run pytest ./tests/backends
        sudo -E $(which uv) run pytest ./tests/install/no_crowdsec
        # these need a running crowdsec
        docker run -d --name crowdsec -e CI_TESTING=true -e DISABLE_ONLINE_API=true -e CROWDSEC_BYPASS_DB_VOLUME_CHECK=true -ti crowdsecurity/crowdsec
        cat >/usr/local/bin/cscli <<'EOT'
        #!/bin/sh
        docker exec crowdsec cscli "$@"
        EOT
        chmod u+x /usr/local/bin/cscli
        sleep 5
        sudo -E $(which uv) run pytest ./tests/install/with_crowdsec

    - name: Lint
      working-directory: ./test
      run: |
        uv run ruff check
        uv run basedpyright



================================================
FILE: .github/workflows/tests_deb.yml
================================================
name: Test .deb packaging

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

permissions:
  contents: read

jobs:
  build:
    name: "Test .deb packages"
    runs-on: ubuntu-latest

    steps:

    - name: Check out code into the Go module directory
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      with:
        fetch-depth: 0

    - name: Set up Go
      uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
      with:
        go-version-file: go.mod

    - name: Install uv
      uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
      with:
        version: 0.5.24
        enable-cache: true
        cache-dependency-glob: "test/uv.lock"

    - name: "Set up Python"
      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
      with:
        python-version-file: "test/.python-version"

    - name: Install the project
      run: uv sync --all-extras --dev
      working-directory: ./test

    - name: Install functional test dependencies
      run: |
        sudo apt update
        sudo apt install -y nftables iptables ipset build-essential debhelper devscripts fakeroot lintian
        docker network create net-test

    - name: Run functional tests
      env:
        CROWDSEC_TEST_VERSION: dev
        CROWDSEC_TEST_FLAVORS: full
        CROWDSEC_TEST_NETWORK: net-test
        CROWDSEC_TEST_TIMEOUT: 60
        PYTEST_ADDOPTS: --durations=0 -vv --color=yes
      working-directory: ./test
      run: |
        uv run pytest ./tests/pkg/test_build_deb.py
        sudo -E $(which uv) run pytest -m deb ./tests/install/no_crowdsec


================================================
FILE: .gitignore
================================================
# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependencies are not vendored by default, but a tarball is created by "make vendor"
# and provided in the release. Used by freebsd, gentoo, etc.
vendor/
vendor.tgz

# Python
__pycache__/
*.py[cod]
*$py.class
venv/

# built by make
/crowdsec-firewall-bouncer
/crowdsec-firewall-bouncer-*
/crowdsec-firewall-bouncer.tgz

# built by dpkg-buildpackage
/debian/crowdsec-firewall-bouncer-iptables
/debian/crowdsec-firewall-bouncer-nftables
/debian/files
/debian/*.substvars
/debian/*.debhelper
/debian/*-stamp

# built by rpmbuild
/rpm/BUILD
/rpm/BUILDROOT
/rpm/RPMS
/rpm/SOURCES/*.tar.gz
/rpm/SRPMS

# nix generated dirs
.direnv
.devenv


================================================
FILE: .golangci.yml
================================================
version: "2"

linters:
  default: all
  disable:
    - cyclop    # revive
    - funlen    # revive
    - gocognit  # revive
    - gocyclo   # revive
    - lll       # revive
    - wsl       # wsl_v5

    - depguard
    - dupl
    - err113
    - exhaustruct
    - gochecknoglobals
    - goconst
    - godox
    - gosec
    - ireturn
    - mnd
    - nlreturn
    - paralleltest
    - tagliatelle
    - testpackage
    - unparam
    - varnamelen
    - whitespace
    - wrapcheck
    - funcorder
    - wsl_v5
    - noinlineerr
    - noctx
    - godoclint
    - prealloc
  settings:

    errcheck:
      check-type-assertions: false

    gocritic:
      enable-all: true
      disabled-checks:
        - appendCombine
        - paramTypeCombine
        - sloppyReassign
        - unnamedResult
        - importShadow

    govet:
      disable:
        - fieldalignment
      enable-all: true

    maintidx:
      # raise this after refactoring
      under: 23

    misspell:
      locale: US

    modernize:
      disable:
        - stringsseq

    nestif:
      # lower this after refactoring
      min-complexity: 13

    nlreturn:
      block-size: 4

    nolintlint:
      require-explanation: false
      require-specific: false
      allow-unused: false

    revive:
      severity: error
      enable-all-rules: true
      rules:
        - name: add-constant
          disabled: true
        - name: cognitive-complexity
          arguments:
            # lower this after refactoring
            - 49
        - name: comment-spacings
          disabled: true
        - name: confusing-results
          disabled: true
        - name: cyclomatic
          arguments:
            # lower this after refactoring
            - 29
        - name: enforce-switch-style
          disabled: true
        - name: flag-parameter
          disabled: true
        - name: function-length
          arguments:
            # lower this after refactoring
            - 74
            - 153
        - name: identical-switch-branches
          disabled: true
        - name: import-alias-naming
          disabled: true
        - name: import-shadowing
          disabled: true
        - name: line-length-limit
          disabled: true
        - name: nested-structs
          disabled: true
        - name: exported
          disabled: true
        - name: unexported-return
          disabled: true
        - name: unhandled-error
          arguments:
            - fmt.Print
            - fmt.Printf
            - fmt.Println
        - name: function-result-limit
          arguments:
            - 5
        - name: var-naming
          disabled: true
    staticcheck:
      checks:
        - all
    wsl:
      allow-trailing-comment: true
  exclusions:
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling
    rules:
      - linters:
          - govet
        text: 'shadow: declaration of "(err|ctx)" shadows declaration'

      - linters:
          - revive
        text: 'deep-exit: calls to flag.Parse only in main\(\) or init\(\) functions'
    paths:
      - third_party$
      - builtin$
      - examples$

issues:
  max-issues-per-linter: 0
  max-same-issues: 0

formatters:
  enable:
    - gci
    - gofumpt

  settings:
    gci:
      sections:
        - standard
        - default
        - prefix(github.com/crowdsecurity)
        - prefix(github.com/crowdsecurity/crowdsec)
        - prefix(github.com/crowdsecurity/cs-firewall-bouncer)
  exclusions:
    paths:
      - third_party$
      - builtin$
      - examples$


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020-2021 crowdsecurity

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: Makefile
================================================
GO = go
GOBUILD = $(GO) build
GOTEST = $(GO) test

BINARY_NAME=crowdsec-firewall-bouncer
TARBALL_NAME=$(BINARY_NAME).tgz

ifdef BUILD_STATIC
$(warning WARNING: The BUILD_STATIC variable is deprecated and has no effect. Builds are static by default now.)
endif

# Versioning information can be overridden in the environment
BUILD_VERSION?=$(shell git describe --tags)
BUILD_TIMESTAMP?=$(shell date +%F"_"%T)
BUILD_TAG?=$(shell git rev-parse HEAD)

LD_OPTS_VARS=\
-X 'github.com/crowdsecurity/go-cs-lib/version.Version=$(BUILD_VERSION)' \
-X 'github.com/crowdsecurity/go-cs-lib/version.BuildDate=$(BUILD_TIMESTAMP)' \
-X 'github.com/crowdsecurity/go-cs-lib/version.Tag=$(BUILD_TAG)'

ifneq (,$(DOCKER_BUILD))
LD_OPTS_VARS += -X 'github.com/crowdsecurity/go-cs-lib/version.System=docker'
endif

export CGO_ENABLED=0
export LD_OPTS=-ldflags "-s -extldflags '-static' $(LD_OPTS_VARS)" \
	-trimpath -tags netgo

.PHONY: all
all: build test

# same as "$(MAKE) -f debian/rules clean" but without the dependency on debhelper
.PHONY: clean-debian
clean-debian:
	@$(RM) -r debian/crowdsec-firewall-bouncer-iptables
	@$(RM) -r debian/crowdsec-firewall-bouncer-nftables
	@$(RM) -r debian/files
	@$(RM) -r debian/.debhelper
	@$(RM) -r debian/*.substvars
	@$(RM) -r debian/*-stamp

.PHONY: clean-rpm
clean-rpm:
	@$(RM) -r rpm/BUILD
	@$(RM) -r rpm/BUILDROOT
	@$(RM) -r rpm/RPMS
	@$(RM) -r rpm/SOURCES/*.tar.gz
	@$(RM) -r rpm/SRPMS

# Remove everything including all platform binaries and tarballs
.PHONY: clean
clean: clean-release-dir clean-debian clean-rpm
	@$(RM) $(BINARY_NAME)
	@$(RM) $(TARBALL_NAME)
	@$(RM) -r $(BINARY_NAME)-*	# platform binary name and leftover release dir
	@$(RM) $(BINARY_NAME)-*.tgz	# platform release file

#
# Build binaries
#

.PHONY: binary
binary:
	$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME)

.PHONY: build
build: clean binary

#
# Unit and integration tests
#

.PHONY: lint
lint:
	golangci-lint run

.PHONY: test
test:
	@$(GOTEST) $(LD_OPTS) ./...

.PHONY: func-tests
func-tests: build
	pipenv install --dev
	pipenv run pytest -v

#
# Build release tarballs
#

RELDIR = $(BINARY_NAME)-$(BUILD_VERSION)

.PHONY: vendor
vendor: vendor-remove
	$(GO) mod vendor
	tar czf vendor.tgz vendor
	tar --create --auto-compress --file=$(RELDIR)-vendor.tar.xz vendor

.PHONY: vendor-remove
vendor-remove:
	$(RM) -r vendor vendor.tgz *-vendor.tar.xz

# Called during platform-all, to reuse the directory for other platforms
.PHONY: clean-release-dir
clean-release-dir:
	@$(RM) -r $(RELDIR)

.PHONY: tarball
tarball: binary
	@if [ -z $(BUILD_VERSION) ]; then BUILD_VERSION="local" ; fi
	@if [ -d $(RELDIR) ]; then echo "$(RELDIR) already exists, please run 'make clean' and retry" ;  exit 1 ; fi
	@echo Building Release to dir $(RELDIR)
	@mkdir -p $(RELDIR)/scripts
	@cp $(BINARY_NAME) $(RELDIR)/
	@cp -R ./config $(RELDIR)/
	@cp ./scripts/install.sh $(RELDIR)/
	@cp ./scripts/uninstall.sh $(RELDIR)/
	@cp ./scripts/upgrade.sh $(RELDIR)/
	@cp ./scripts/_bouncer.sh $(RELDIR)/scripts/
	@chmod +x $(RELDIR)/install.sh
	@chmod +x $(RELDIR)/uninstall.sh
	@chmod +x $(RELDIR)/upgrade.sh
	@tar cvzf $(TARBALL_NAME) $(RELDIR)

.PHONY: release
release: clean tarball

#
# Build binaries and release tarballs for all platforms
#

.PHONY: platform-all
platform-all: clean
	python3 .github/release.py run-build $(BINARY_NAME)


================================================
FILE: README.md
================================================
<p align="center">
<img src="https://github.com/crowdsecurity/cs-firewall-bouncer/raw/main/docs/assets/crowdsec_linux_logo.png" alt="CrowdSec" title="CrowdSec" width="300" height="280" />
</p>
<p align="center">
<img src="https://img.shields.io/badge/build-pass-green">
<img src="https://img.shields.io/badge/tests-pass-green">
</p>
<p align="center">
&#x1F4DA; <a href="#installation">Documentation</a>
&#x1F4A0; <a href="https://hub.crowdsec.net">Hub</a>
&#128172; <a href="https://discourse.crowdsec.net">Discourse </a>
</p>


# crowdsec-firewall-bouncer
Crowdsec bouncer written in golang for firewalls.

crowdsec-firewall-bouncer will fetch new and old decisions from a CrowdSec API to add them in a blocklist used by supported firewalls.

Supported firewalls:
 - iptables (IPv4 :heavy_check_mark: / IPv6 :heavy_check_mark: )
 - nftables (IPv4 :heavy_check_mark: / IPv6 :heavy_check_mark: )
 - ipset only (IPv4 :heavy_check_mark: / IPv6 :heavy_check_mark: )
 - pf (IPV4 :heavy_check_mark: / IPV6 :heavy_check_mark: )

# Installation

Please follow the [official documentation](https://doc.crowdsec.net/docs/bouncers/firewall).


================================================
FILE: cmd/root.go
================================================
package cmd

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/signal"
	"slices"
	"strings"
	"syscall"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	log "github.com/sirupsen/logrus"
	"golang.org/x/sync/errgroup"

	csbouncer "github.com/crowdsecurity/go-cs-bouncer"
	"github.com/crowdsecurity/go-cs-lib/csdaemon"
	"github.com/crowdsecurity/go-cs-lib/csstring"
	"github.com/crowdsecurity/go-cs-lib/version"

	"github.com/crowdsecurity/crowdsec/pkg/models"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/backend"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/metrics"
)

const bouncerType = "crowdsec-firewall-bouncer"

var errSignalShutdown = errors.New("signal shutdown")

func backendCleanup(backend *backend.BackendCTX) {
	log.Info("Shutting down backend")

	if err := backend.ShutDown(); err != nil {
		log.Errorf("while shutting down backend: %s", err)
	}
}

func HandleSignals(ctx context.Context) error {
	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, syscall.SIGTERM, os.Interrupt)

	select {
	case s := <-signalChan:
		switch s {
		case syscall.SIGTERM:
			return errSignalShutdown
		case os.Interrupt: // cross-platform SIGINT
			return errSignalShutdown
		}
	case <-ctx.Done():
		return ctx.Err()
	}

	return nil
}

func deleteDecisions(backend *backend.BackendCTX, decisions []*models.Decision, config *cfg.BouncerConfig) {
	nbDeletedDecisions := 0

	for _, d := range decisions {
		if !slices.Contains(config.SupportedDecisionsTypes, strings.ToLower(*d.Type)) {
			log.Debugf("decisions for ip '%s' will not be deleted because its type is '%s'", *d.Value, *d.Type)
			continue
		}

		if err := backend.Delete(d); err != nil {
			if !strings.Contains(err.Error(), "netlink receive: no such file or directory") {
				log.Errorf("unable to delete decision for '%s': %s", *d.Value, err)
			}

			continue
		}

		log.Debugf("deleted %s", *d.Value)

		nbDeletedDecisions++
	}

	noun := "decisions"
	if nbDeletedDecisions == 1 {
		noun = "decision"
	}

	if nbDeletedDecisions > 0 {
		log.Debug("committing expired decisions")

		if err := backend.Commit(); err != nil {
			log.Errorf("unable to commit expired decisions %v", err)
			return
		}

		log.Debug("committed expired decisions")
		log.Infof("%d %s deleted", nbDeletedDecisions, noun)
	}
}

func addDecisions(backend *backend.BackendCTX, decisions []*models.Decision, config *cfg.BouncerConfig) {
	nbNewDecisions := 0

	for _, d := range decisions {
		if !slices.Contains(config.SupportedDecisionsTypes, strings.ToLower(*d.Type)) {
			log.Debugf("decisions for ip '%s' will not be added because its type is '%s'", *d.Value, *d.Type)
			continue
		}

		if err := backend.Add(d); err != nil {
			log.Errorf("unable to insert decision for '%s': %s", *d.Value, err)
			continue
		}

		log.Debugf("Adding '%s' for '%s'", *d.Value, *d.Duration)

		nbNewDecisions++
	}

	noun := "decisions"
	if nbNewDecisions == 1 {
		noun = "decision"
	}

	if nbNewDecisions > 0 {
		log.Debug("committing added decisions")

		if err := backend.Commit(); err != nil {
			log.Errorf("unable to commit add decisions %v", err)
			return
		}

		log.Debug("committed added decisions")
		log.Infof("%d %s added", nbNewDecisions, noun)
	}
}

func Execute() error {
	configPath := flag.String("c", "", "path to crowdsec-firewall-bouncer.yaml")
	verbose := flag.Bool("v", false, "set verbose mode")
	bouncerVersion := flag.Bool("V", false, "display version and exit (deprecated)")
	flag.BoolVar(bouncerVersion, "version", *bouncerVersion, "display version and exit")
	testConfig := flag.Bool("t", false, "test config and exit")
	showConfig := flag.Bool("T", false, "show full config (.yaml + .yaml.local) and exit")

	flag.Parse()

	if *bouncerVersion {
		fmt.Fprint(os.Stdout, version.FullString())
		return nil
	}

	if configPath == nil || *configPath == "" {
		return errors.New("configuration file is required")
	}

	configMerged, err := cfg.MergedConfig(*configPath)
	if err != nil {
		return fmt.Errorf("unable to read config file: %w", err)
	}

	if *showConfig {
		fmt.Fprintln(os.Stdout, string(configMerged))
		return nil
	}

	configExpanded := csstring.StrictExpand(string(configMerged), os.LookupEnv)

	config, err := cfg.NewConfig(strings.NewReader(configExpanded))
	if err != nil {
		return fmt.Errorf("unable to load configuration: %w", err)
	}

	if *verbose && !log.IsLevelEnabled(log.DebugLevel) {
		log.SetLevel(log.DebugLevel)
	}

	log.Infof("Starting %s %s", bouncerType, version.String())

	backend, err := backend.NewBackend(config)
	if err != nil {
		return err
	}

	if err = backend.Init(); err != nil {
		return err
	}

	defer backendCleanup(backend)

	bouncer := &csbouncer.StreamBouncer{}

	err = bouncer.ConfigReader(strings.NewReader(configExpanded))
	if err != nil {
		return err
	}

	bouncer.UserAgent = fmt.Sprintf("%s/%s", bouncerType, version.String())
	if err := bouncer.Init(); err != nil {
		return fmt.Errorf("unable to configure bouncer: %w", err)
	}

	if *testConfig {
		log.Info("config is valid")
		return nil
	}

	if bouncer.InsecureSkipVerify != nil {
		log.Debugf("InsecureSkipVerify is set to %t", *bouncer.InsecureSkipVerify)
	}

	g, ctx := errgroup.WithContext(context.Background())

	g.Go(func() error {
		return bouncer.Run(ctx)
	})

	mHandler := metrics.Handler{
		Backend: backend,
	}

	metricsProvider, err := csbouncer.NewMetricsProvider(bouncer.APIClient, bouncerType, mHandler.MetricsUpdater, log.StandardLogger())
	if err != nil {
		return fmt.Errorf("unable to create metrics provider: %w", err)
	}

	g.Go(func() error {
		return metricsProvider.Run(ctx)
	})

	if config.Mode == cfg.IptablesMode || config.Mode == cfg.NftablesMode || config.Mode == cfg.IpsetMode || config.Mode == cfg.PfMode {
		metrics.Map.MustRegisterAll()
	}

	prometheus.MustRegister(csbouncer.TotalLAPICalls, csbouncer.TotalLAPIError)

	if config.PrometheusConfig.Enabled {
		go func() {
			http.Handle("/metrics", mHandler.ComputeMetricsHandler(promhttp.Handler()))

			listenOn := net.JoinHostPort(
				config.PrometheusConfig.ListenAddress,
				config.PrometheusConfig.ListenPort,
			)
			log.Infof("Serving metrics at %s", listenOn+"/metrics")
			log.Error(http.ListenAndServe(listenOn, nil))
		}()
	}

	g.Go(func() error {
		log.Infof("Processing new and deleted decisions . . .")

		for {
			select {
			case <-ctx.Done():
				return nil
			case decisions := <-bouncer.Stream:
				if decisions == nil {
					continue
				}

				deleteDecisions(backend, decisions.Deleted, config)
				addDecisions(backend, decisions.New, config)
			}
		}
	})

	if config.Daemon != nil {
		if *config.Daemon {
			log.Debug("Ignoring deprecated 'daemonize' option")
		} else {
			log.Warn("The 'daemonize' config option is deprecated and treated as always true")
		}
	}

	_ = csdaemon.Notify(csdaemon.Ready, log.StandardLogger())

	g.Go(func() error {
		return HandleSignals(ctx)
	})

	if err := g.Wait(); err != nil {
		if errors.Is(err, errSignalShutdown) {
			log.Info("Received shutdown signal, exiting gracefully")
			return nil
		}

		return fmt.Errorf("process terminated with error: %w", err)
	}

	return nil
}


================================================
FILE: config/crowdsec-firewall-bouncer.service
================================================
[Unit]
Description=The firewall bouncer for CrowdSec
After=syslog.target network.target remote-fs.target nss-lookup.target crowdsec.service

[Service]
Type=notify
ExecStart=${BIN} -c ${CFG}/crowdsec-firewall-bouncer.yaml
ExecStartPre=${BIN} -c ${CFG}/crowdsec-firewall-bouncer.yaml -t
ExecStartPost=/bin/sleep 0.1
Restart=always
RestartSec=10
LimitNOFILE=65536
# don't send a termination signal to the children processes,
# because the iptables backend needs to run ipset multiple times to properly shutdown
KillMode=mixed

[Install]
WantedBy=multi-user.target


================================================
FILE: config/crowdsec-firewall-bouncer.yaml
================================================
mode: ${BACKEND}
update_frequency: 10s
log_mode: file
log_dir: /var/log/
log_level: info
log_compression: true
log_max_size: 100
log_max_backups: 3
log_max_age: 30
api_url: http://127.0.0.1:8080/
api_key: ${API_KEY}
## TLS Authentication
# cert_path: /etc/crowdsec/tls/cert.pem
# key_path: /etc/crowdsec/tls/key.pem
# ca_cert_path: /etc/crowdsec/tls/ca.crt
insecure_skip_verify: false
disable_ipv6: false
deny_action: DROP
deny_log: false
supported_decisions_types:
  - ban
#to change log prefix
#deny_log_prefix: "crowdsec: "
#to change the blacklists name
blacklists_ipv4: crowdsec-blacklists
blacklists_ipv6: crowdsec6-blacklists
#type of ipset to use
ipset_type: nethash
#if present, insert rule in those chains
iptables_chains:
  - INPUT
#  - FORWARD
#  - DOCKER-USER
iptables_add_rule_comments: true

## nftables
nftables:
  ipv4:
    enabled: true
    set-only: false
    table: crowdsec
    chain: crowdsec-chain
    priority: -10
  ipv6:
    enabled: true
    set-only: false
    table: crowdsec6
    chain: crowdsec6-chain
    priority: -10

nftables_hooks:
  - input
  - forward

# packet filter
pf:
  # an empty string disables the anchor
  anchor_name: ""

prometheus:
  enabled: false
  listen_addr: 127.0.0.1
  listen_port: 60601


================================================
FILE: debian/changelog
================================================
crowdsec-firewall-bouncer (1.0.12) UNRELEASED; urgency=medium

  * debian package
  * pf support

 -- Manuel Sabban <manuel@crowdsec.net>  Mon, 08 Feb 2021 09:38:06 +0100


================================================
FILE: debian/compat
================================================
11


================================================
FILE: debian/control
================================================
Source: crowdsec-firewall-bouncer
Maintainer: Crowdsec Team <debian@crowdsec.net>
Build-Depends: debhelper
Section: admin
Priority: optional

Package: crowdsec-firewall-bouncer-iptables
Architecture: any
Description: Firewall bouncer for Crowdsec (iptables+ipset)
Depends: gettext-base, iptables, ipset
Replaces: crowdsec-firewall-bouncer
Conflicts: crowdsec-firewall-bouncer-nftables

Package: crowdsec-firewall-bouncer-nftables
Architecture: any
Description: Firewall bouncer for Crowdsec (nftables)
Depends: gettext-base, nftables
Replaces: crowdsec-firewall-bouncer
Conflicts: crowdsec-firewall-bouncer-iptables


================================================
FILE: debian/crowdsec-firewall-bouncer-iptables.postinst
================================================
#!/bin/sh

systemctl daemon-reload

#shellcheck source=./scripts/_bouncer.sh
. "/usr/lib/$DPKG_MAINTSCRIPT_PACKAGE/_bouncer.sh"
START=1

if [ "$1" = "configure" ]; then
    if need_api_key; then
        if ! set_api_key; then
            START=0
        fi
    fi
fi

systemctl --quiet is-enabled "$SERVICE" || systemctl unmask "$SERVICE" && systemctl enable "$SERVICE"

set_local_port

if [ "$START" -eq 0 ]; then
    echo "no api key was generated, you can generate one on your LAPI server by running 'cscli bouncers add <bouncer_name>' and add it to '$CONFIG'" >&2
else
    systemctl start "$SERVICE"
fi


================================================
FILE: debian/crowdsec-firewall-bouncer-iptables.postrm
================================================
#!/bin/sh

set -eu

BOUNCER="crowdsec-firewall-bouncer"
CONFIG="/etc/crowdsec/bouncers/$BOUNCER.yaml"

if [ "$1" = "purge" ]; then
    if [ -f "$CONFIG.id" ]; then
        bouncer_id=$(cat "$CONFIG.id")
        cscli -oraw bouncers delete "$bouncer_id" 2>/dev/null || true
        rm -f "$CONFIG.id"
    fi
fi


================================================
FILE: debian/crowdsec-firewall-bouncer-iptables.preinst
================================================
#!/bin/sh

set -e

# Source debconf library.
. /usr/share/debconf/confmodule


================================================
FILE: debian/crowdsec-firewall-bouncer-iptables.prerm
================================================
#!/bin/sh

set -eu

BOUNCER="crowdsec-firewall-bouncer"

systemctl stop "$BOUNCER" || echo "cannot stop service"
systemctl disable "$BOUNCER" || echo "cannot disable service"


================================================
FILE: debian/crowdsec-firewall-bouncer-nftables.postinst
================================================
#!/bin/sh

systemctl daemon-reload

#shellcheck source=./scripts/_bouncer.sh
. "/usr/lib/$DPKG_MAINTSCRIPT_PACKAGE/_bouncer.sh"
START=1

if [ "$1" = "configure" ]; then
    if need_api_key; then
        if ! set_api_key; then
            START=0
        fi
    fi
fi

systemctl --quiet is-enabled "$SERVICE" || systemctl unmask "$SERVICE" && systemctl enable "$SERVICE"

set_local_port

if [ "$START" -eq 0 ]; then
    echo "no api key was generated, you can generate one on your LAPI server by running 'cscli bouncers add <bouncer_name>' and add it to '$CONFIG'" >&2
else
    systemctl start "$SERVICE"
fi


================================================
FILE: debian/crowdsec-firewall-bouncer-nftables.postrm
================================================
#!/bin/sh

set -eu

BOUNCER="crowdsec-firewall-bouncer"
CONFIG="/etc/crowdsec/bouncers/$BOUNCER.yaml"

if [ "$1" = "purge" ]; then
    if [ -f "$CONFIG.id" ]; then
        bouncer_id=$(cat "$CONFIG.id")
        cscli -oraw bouncers delete "$bouncer_id" 2>/dev/null || true
        rm -f "$CONFIG.id"
    fi
fi


================================================
FILE: debian/crowdsec-firewall-bouncer-nftables.preinst
================================================
#!/bin/sh

set -e

# Source debconf library.
. /usr/share/debconf/confmodule

echo "pre-inst (nftables)" 


================================================
FILE: debian/crowdsec-firewall-bouncer-nftables.prerm
================================================
#!/bin/sh

set -eu

BOUNCER="crowdsec-firewall-bouncer"

systemctl stop "$BOUNCER" || echo "cannot stop service"
systemctl disable "$BOUNCER" || echo "cannot disable service"


================================================
FILE: debian/rules
================================================
#!/usr/bin/make -f

export DEB_VERSION=$(shell dpkg-parsechangelog | grep -E '^Version:' | cut -f 2 -d ' ')
export BUILD_VERSION=v${DEB_VERSION}-debian-pragmatic

%:
	dh $@

override_dh_systemd_start:
	echo "Not running dh_systemd_start"
override_dh_auto_clean:
override_dh_auto_test:
override_dh_auto_build:
override_dh_auto_install:
	@make build

	@BOUNCER=crowdsec-firewall-bouncer; \
	for BACKEND in iptables nftables; do \
		PKG="$$BOUNCER-$$BACKEND"; \
		install -D $$BOUNCER -t "debian/$$PKG/usr/bin/"; \
		install -D scripts/_bouncer.sh -t "debian/$$PKG/usr/lib/$$PKG/"; \
		mkdir -p "debian/$$PKG/etc/crowdsec/bouncers"; \
		(umask 177 && BACKEND=$$BACKEND envsubst '$$BACKEND' < "config/$$BOUNCER.yaml" > "debian/$$PKG/etc/crowdsec/bouncers/$$BOUNCER.yaml"); \
		mkdir -p "debian/$$PKG/etc/systemd/system"; \
		BIN="/usr/bin/$$BOUNCER" CFG="/etc/crowdsec/bouncers" envsubst '$$BIN $$CFG' < "config/$$BOUNCER.service" > "debian/$$PKG/etc/systemd/system/$$BOUNCER.service"; \
		mkdir -p "debian/$$PKG/usr/sbin/"; \
		ln -s "/usr/bin/$$BOUNCER" "debian/$$PKG/usr/sbin/$$BOUNCER"; \
	done

execute_after_dh_fixperms:
	@BOUNCER=crowdsec-firewall-bouncer; \
	for BACKEND in iptables nftables; do \
		PKG="$$BOUNCER-$$BACKEND"; \
		chmod 0755 "debian/$$PKG/usr/bin/$$BOUNCER"; \
		chmod 0600 "debian/$$PKG/usr/lib/$$PKG/_bouncer.sh"; \
		chmod 0600 "debian/$$PKG/etc/crowdsec/bouncers/$$BOUNCER.yaml"; \
		chmod 0644 "debian/$$PKG/etc/systemd/system/$$BOUNCER.service"; \
	done


================================================
FILE: flake.nix
================================================
{
  description = "A Nix-flake-based Go 1.22 development environment";

  inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz";

  outputs = { self, nixpkgs }:
    let
      goVersion = 24; # Change this to update the whole stack

      supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
      forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ self.overlays.default ];
        };
      });
    in
    {
      overlays.default = final: prev: {
        go = final."go_1_${toString goVersion}";
      };

      devShells = forEachSupportedSystem ({ pkgs }: {
        default = pkgs.mkShell {
          packages = with pkgs; [
            # go (version is specified by overlay)
            go

            # goimports, godoc, etc.
            gotools

            # https://github.com/golangci/golangci-lint
            golangci-lint
            golangci-lint-langserver
            gopls
          ];
        };
      });
    };
}


================================================
FILE: go.mod
================================================
module github.com/crowdsecurity/cs-firewall-bouncer

go 1.25.2

require (
	github.com/crowdsecurity/crowdsec v1.7.7
	github.com/crowdsecurity/go-cs-bouncer v0.0.21
	github.com/crowdsecurity/go-cs-lib v0.0.25
	github.com/google/nftables v0.3.0
	github.com/prometheus/client_golang v1.23.2
	github.com/prometheus/client_model v0.6.2
	github.com/sirupsen/logrus v1.9.4
	github.com/stretchr/testify v1.11.1
	golang.org/x/sync v0.20.0
	golang.org/x/sys v0.42.0
	gopkg.in/natefinch/lumberjack.v2 v2.2.1
	gopkg.in/yaml.v2 v2.4.0
)

require (
	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/ebitengine/purego v0.8.4 // indirect
	github.com/go-ole/go-ole v1.2.6 // indirect
	github.com/go-openapi/analysis v0.23.0 // indirect
	github.com/go-openapi/errors v0.22.2 // indirect
	github.com/go-openapi/jsonpointer v0.21.1 // indirect
	github.com/go-openapi/jsonreference v0.21.0 // indirect
	github.com/go-openapi/loads v0.22.0 // indirect
	github.com/go-openapi/spec v0.21.0 // indirect
	github.com/go-openapi/strfmt v0.23.0 // indirect
	github.com/go-openapi/swag v0.23.1 // indirect
	github.com/go-openapi/validate v0.24.0 // indirect
	github.com/goccy/go-yaml v1.18.0 // indirect
	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
	github.com/google/go-cmp v0.7.0 // indirect
	github.com/google/go-querystring v1.1.0 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/josharian/intern v1.0.0 // indirect
	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
	github.com/mailru/easyjson v0.9.0 // indirect
	github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
	github.com/mdlayher/socket v0.5.1 // indirect
	github.com/mitchellh/mapstructure v1.5.0 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/oklog/ulid v1.3.1 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
	github.com/prometheus/common v0.66.1 // indirect
	github.com/prometheus/procfs v0.17.0 // indirect
	github.com/shirou/gopsutil/v4 v4.25.8 // indirect
	github.com/tklauser/go-sysconf v0.3.15 // indirect
	github.com/tklauser/numcpus v0.10.0 // indirect
	github.com/yusufpapurcu/wmi v1.2.4 // indirect
	go.mongodb.org/mongo-driver v1.17.4 // indirect
	go.yaml.in/yaml/v2 v2.4.2 // indirect
	golang.org/x/net v0.48.0 // indirect
	google.golang.org/protobuf v1.36.10 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/crowdsecurity/crowdsec v1.7.7 h1:sduZN763iXsrZodocWDrsR//7nLeffGu+RVkkIsbQkE=
github.com/crowdsecurity/crowdsec v1.7.7/go.mod h1:L1HLGPDnBYCcY+yfSFnuBbQ1G9DHEJN9c+Kevv9F+4Q=
github.com/crowdsecurity/go-cs-bouncer v0.0.21 h1:arPz0VtdVSaz+auOSfHythzkZVLyy18CzYvYab8UJDU=
github.com/crowdsecurity/go-cs-bouncer v0.0.21/go.mod h1:4JiH0XXA4KKnnWThItUpe5+heJHWzsLOSA2IWJqUDBA=
github.com/crowdsecurity/go-cs-lib v0.0.25 h1:Ov6VPW9yV+OPsbAIQk1iTkEWhwkpaG0v3lrBzeqjzj4=
github.com/crowdsecurity/go-cs-lib v0.0.25/go.mod h1:X0GMJY2CxdA1S09SpuqIKaWQsvRGxXmecUp9cP599dE=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: main.go
================================================
package main

import (
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/cs-firewall-bouncer/cmd"
)

func main() {
	err := cmd.Execute()
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: pkg/backend/backend.go
================================================
package backend

import (
	"errors"
	"fmt"
	"runtime"

	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/models"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/dryrun"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/iptables"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/nftables"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/pf"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/types"
)

type BackendCTX struct {
	firewall types.Backend
}

func (b *BackendCTX) Init() error {
	return b.firewall.Init()
}

func (b *BackendCTX) Commit() error {
	return b.firewall.Commit()
}

func (b *BackendCTX) ShutDown() error {
	return b.firewall.ShutDown()
}

func (b *BackendCTX) Add(decision *models.Decision) error {
	return b.firewall.Add(decision)
}

func (b *BackendCTX) Delete(decision *models.Decision) error {
	return b.firewall.Delete(decision)
}

func (b *BackendCTX) CollectMetrics() {
	log.Trace("Collecting backend-specific metrics")
	b.firewall.CollectMetrics()
}

func isPFSupported(runtimeOS string) bool {
	var supported bool

	switch runtimeOS {
	case "openbsd", "freebsd":
		supported = true
	default:
		supported = false
	}

	return supported
}

func NewBackend(config *cfg.BouncerConfig) (*BackendCTX, error) {
	var err error

	b := &BackendCTX{}

	log.Infof("backend type: %s", config.Mode)

	if config.DisableIPV6 {
		log.Info("IPV6 is disabled")
	}

	if config.DisableIPV4 {
		log.Info("IPV4 is disabled")
	}

	switch config.Mode {
	case cfg.IptablesMode, cfg.IpsetMode:
		if runtime.GOOS != "linux" {
			return nil, errors.New("iptables and ipset is linux only")
		}

		b.firewall, err = iptables.NewIPTables(config)
		if err != nil {
			return nil, err
		}
	case cfg.NftablesMode:
		if runtime.GOOS != "linux" {
			return nil, errors.New("nftables is linux only")
		}

		b.firewall, err = nftables.NewNFTables(config)
		if err != nil {
			return nil, err
		}
	case "pf":
		if !isPFSupported(runtime.GOOS) {
			log.Warning("pf mode can only work with openbsd and freebsd. It is available on other platforms only for testing purposes")
		}

		b.firewall, err = pf.NewPF(config)
		if err != nil {
			return nil, err
		}
	case "dry-run":
		b.firewall, err = dryrun.NewDryRun(config)
		if err != nil {
			return nil, err
		}
	default:
		return b, fmt.Errorf("firewall '%s' is not supported", config.Mode)
	}

	return b, nil
}


================================================
FILE: pkg/cfg/config.go
================================================
package cfg

import (
	"errors"
	"fmt"
	"io"

	log "github.com/sirupsen/logrus"
	"gopkg.in/yaml.v2"

	"github.com/crowdsecurity/go-cs-lib/csyaml"
	"github.com/crowdsecurity/go-cs-lib/ptr"
)

type PrometheusConfig struct {
	Enabled       bool   `yaml:"enabled"`
	ListenAddress string `yaml:"listen_addr"`
	ListenPort    string `yaml:"listen_port"`
}

type nftablesFamilyConfig struct {
	Enabled  *bool  `yaml:"enabled"`
	SetOnly  bool   `yaml:"set-only"`
	Table    string `yaml:"table"`
	Chain    string `yaml:"chain"`
	Priority int    `yaml:"priority"`
}

const (
	IpsetMode    = "ipset"
	IptablesMode = "iptables"
	NftablesMode = "nftables"
	PfMode       = "pf"
	DryRunMode   = "dry-run"
)

type BouncerConfig struct {
	Mode               string        `yaml:"mode"`    // ipset,iptables,tc
	PidDir             string        `yaml:"pid_dir"` // unused
	UpdateFrequency    string        `yaml:"update_frequency"`
	Daemon             *bool         `yaml:"daemonize"` // unused
	Logging            LoggingConfig `yaml:",inline"`
	DisableIPV6        bool          `yaml:"disable_ipv6"`
	DisableIPV4        bool          `yaml:"disable_ipv4"`
	DenyAction         string        `yaml:"deny_action"`
	DenyLog            bool          `yaml:"deny_log"`
	DenyLogPrefix      string        `yaml:"deny_log_prefix"`
	BlacklistsIpv4     string        `yaml:"blacklists_ipv4"`
	BlacklistsIpv6     string        `yaml:"blacklists_ipv6"`
	SetType            string        `yaml:"ipset_type"`
	SetSize            int           `yaml:"ipset_size"`
	SetDisableTimeouts bool          `yaml:"ipset_disable_timeouts"`

	// specific to iptables, following https://github.com/crowdsecurity/cs-firewall-bouncer/issues/19
	IptablesChains          []string `yaml:"iptables_chains"`
	IptablesV4Chains        []string `yaml:"iptables_v4_chains"`
	IptablesV6Chains        []string `yaml:"iptables_v6_chains"`
	IptablesAddRuleComments bool     `yaml:"iptables_add_rule_comments"`

	SupportedDecisionsTypes []string `yaml:"supported_decisions_types"`
	// specific to nftables, following https://github.com/crowdsecurity/cs-firewall-bouncer/issues/74
	Nftables struct {
		Ipv4 nftablesFamilyConfig `yaml:"ipv4"`
		Ipv6 nftablesFamilyConfig `yaml:"ipv6"`
	} `yaml:"nftables"`
	NftablesHooks []string `yaml:"nftables_hooks"`
	PF            struct {
		AnchorName string `yaml:"anchor_name"`
		BatchSize  int    `yaml:"batch_size"`
	} `yaml:"pf"`
	PrometheusConfig PrometheusConfig `yaml:"prometheus"`
}

// MergedConfig() returns the byte content of the patched configuration file (with .yaml.local).
func MergedConfig(configPath string) ([]byte, error) {
	patcher := csyaml.NewPatcher(configPath, ".local")

	data, err := patcher.MergedPatchContent()
	if err != nil {
		return nil, err
	}

	return data, nil
}

func NewConfig(reader io.Reader) (*BouncerConfig, error) {
	config := &BouncerConfig{
		IptablesAddRuleComments: true,
	}

	fcontent, err := io.ReadAll(reader)
	if err != nil {
		return nil, err
	}

	err = yaml.Unmarshal(fcontent, &config)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal: %w", err)
	}

	if err = config.Logging.setup("crowdsec-firewall-bouncer.log"); err != nil {
		return nil, fmt.Errorf("failed to setup logging: %w", err)
	}

	if config.Mode == "" {
		return nil, errors.New("config does not contain 'mode'")
	}

	if len(config.SupportedDecisionsTypes) == 0 {
		config.SupportedDecisionsTypes = []string{"ban"}
	}

	if config.PidDir != "" {
		log.Debug("Ignoring deprecated 'pid_dir' option")
	}

	if config.DenyLog && config.DenyLogPrefix == "" {
		config.DenyLogPrefix = "crowdsec drop: "
	}

	// for config file backward compatibility
	if config.BlacklistsIpv4 == "" {
		config.BlacklistsIpv4 = "crowdsec-blacklists"
	}

	if config.BlacklistsIpv6 == "" {
		config.BlacklistsIpv6 = "crowdsec6-blacklists"
	}

	if config.SetType == "" {
		config.SetType = "nethash"
	}

	if config.SetSize == 0 {
		config.SetSize = 131072
	}

	if config.DisableIPV4 && config.DisableIPV6 && config.Mode != NftablesMode {
		// we return an error for pf or iptables because nftables has it own way to handle this
		return nil, errors.New("both IPv4 and IPv6 disabled, doing nothing")
	}

	switch config.Mode {
	case NftablesMode:
		err := nftablesConfig(config)
		if err != nil {
			return nil, err
		}
	case IpsetMode, IptablesMode:
		// nothing specific to do
	case PfMode:
		err := pfConfig(config)
		if err != nil {
			return nil, err
		}
	case DryRunMode:
		// nothing specific to do
	default:
		log.Warningf("unexpected %s mode", config.Mode)
	}

	return config, nil
}

func pfConfig(config *BouncerConfig) error {
	if config.PF.BatchSize != 0 {
		log.Warning("Option pf.batch_size is deprecated and ignored, all IPs are loaded at once")
	}

	return nil
}

func nftablesConfig(config *BouncerConfig) error {
	// deal with defaults in a backward compatible way
	if config.Nftables.Ipv4.Enabled == nil {
		config.Nftables.Ipv4.Enabled = ptr.Of(!config.DisableIPV4)
	}

	if config.Nftables.Ipv6.Enabled == nil {
		config.Nftables.Ipv6.Enabled = ptr.Of(!config.DisableIPV6)
	}

	if *config.Nftables.Ipv4.Enabled {
		if config.Nftables.Ipv4.Table == "" {
			config.Nftables.Ipv4.Table = "crowdsec"
		}

		if config.Nftables.Ipv4.Chain == "" {
			config.Nftables.Ipv4.Chain = "crowdsec-chain"
		}
	}

	if *config.Nftables.Ipv6.Enabled {
		if config.Nftables.Ipv6.Table == "" {
			config.Nftables.Ipv6.Table = "crowdsec6"
		}

		if config.Nftables.Ipv6.Chain == "" {
			config.Nftables.Ipv6.Chain = "crowdsec6-chain"
		}
	}

	if !*config.Nftables.Ipv4.Enabled && !*config.Nftables.Ipv6.Enabled {
		return errors.New("both IPv4 and IPv6 disabled, doing nothing")
	}

	if len(config.NftablesHooks) == 0 {
		config.NftablesHooks = []string{"input"}
	}

	return nil
}


================================================
FILE: pkg/cfg/logging.go
================================================
package cfg

import (
	"errors"
	"io"
	"os"
	"path/filepath"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus/hooks/writer"
	"gopkg.in/natefinch/lumberjack.v2"

	"github.com/crowdsecurity/go-cs-lib/ptr"
)

type LoggingConfig struct {
	LogLevel     *log.Level `yaml:"log_level"`
	LogMode      string     `yaml:"log_mode"`
	LogDir       string     `yaml:"log_dir"`
	LogMaxSize   int        `yaml:"log_max_size,omitempty"`
	LogMaxFiles  int        `yaml:"log_max_files,omitempty"`
	LogMaxAge    int        `yaml:"log_max_age,omitempty"`
	CompressLogs *bool      `yaml:"compress_logs,omitempty"`
}

func (c *LoggingConfig) LoggerForFile(fileName string) (io.Writer, error) {
	if c.LogMode == "stdout" {
		return os.Stderr, nil
	}

	// default permissions will be 0600 from lumberjack
	// and are preserved if the file already exists

	l := &lumberjack.Logger{
		Filename:   filepath.Join(c.LogDir, fileName),
		MaxSize:    c.LogMaxSize,
		MaxBackups: c.LogMaxFiles,
		MaxAge:     c.LogMaxAge,
		Compress:   *c.CompressLogs,
	}

	return l, nil
}

func (c *LoggingConfig) setDefaults() {
	if c.LogMode == "" {
		c.LogMode = "stdout"
	}

	if c.LogDir == "" {
		c.LogDir = "/var/log/"
	}

	if c.LogLevel == nil {
		c.LogLevel = ptr.Of(log.InfoLevel)
	}

	if c.LogMaxSize == 0 {
		c.LogMaxSize = 500
	}

	if c.LogMaxFiles == 0 {
		c.LogMaxFiles = 3
	}

	if c.LogMaxAge == 0 {
		c.LogMaxAge = 30
	}

	if c.CompressLogs == nil {
		c.CompressLogs = ptr.Of(true)
	}
}

func (c *LoggingConfig) validate() error {
	if c.LogMode != "stdout" && c.LogMode != "file" {
		return errors.New("log_mode should be either 'stdout' or 'file'")
	}

	return nil
}

func (c *LoggingConfig) setup(fileName string) error {
	c.setDefaults()

	if err := c.validate(); err != nil {
		return err
	}

	log.SetLevel(*c.LogLevel)

	if c.LogMode == "stdout" {
		return nil
	}

	log.SetFormatter(&log.TextFormatter{TimestampFormat: time.RFC3339, FullTimestamp: true})

	logger, err := c.LoggerForFile(fileName)
	if err != nil {
		return err
	}

	log.SetOutput(logger)

	// keep stderr for panic/fatal, otherwise process failures
	// won't be visible enough
	log.AddHook(&writer.Hook{
		Writer: os.Stderr,
		LogLevels: []log.Level{
			log.PanicLevel,
			log.FatalLevel,
		},
	})

	return nil
}


================================================
FILE: pkg/dryrun/dryrun.go
================================================
package dryrun

import (
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/models"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/types"
)

type dryRun struct{}

func NewDryRun(_ *cfg.BouncerConfig) (types.Backend, error) {
	return &dryRun{}, nil
}

func (*dryRun) Init() error {
	log.Infof("backend.Init() called")
	return nil
}

func (*dryRun) Commit() error {
	log.Infof("backend.Commit() called")
	return nil
}

func (*dryRun) Add(decision *models.Decision) error {
	log.Infof("backend.Add() called with %s", *decision.Value)
	return nil
}

func (*dryRun) CollectMetrics() {
	log.Infof("backend.CollectMetrics() called")
}

func (*dryRun) Delete(decision *models.Decision) error {
	log.Infof("backend.Delete() called with %s", *decision.Value)
	return nil
}

func (*dryRun) ShutDown() error {
	log.Infof("backend.ShutDown() called")
	return nil
}


================================================
FILE: pkg/ipsetcmd/ipset.go
================================================
package ipsetcmd

import (
	"errors"
	"fmt"
	"os/exec"
	"strconv"
	"strings"

	log "github.com/sirupsen/logrus"
)

type IPSet struct {
	binaryPath string
	setName    string
}

type CreateOptions struct {
	Timeout         string
	MaxElem         string
	Family          string
	Type            string
	DisableTimeouts bool
}

const ipsetBinary = "ipset"

func NewIPSet(setName string) (*IPSet, error) {
	ipsetBin, err := exec.LookPath(ipsetBinary)
	if err != nil {
		return nil, errors.New("unable to find ipset")
	}

	return &IPSet{
		binaryPath: ipsetBin,
		setName:    setName,
	}, nil
}

// Wraps all the ipset commands.
func (i *IPSet) Create(opts CreateOptions) error {
	cmdArgs := []string{"create", i.setName}

	if opts.Type != "" {
		cmdArgs = append(cmdArgs, opts.Type)
	}

	if opts.Timeout != "" && !opts.DisableTimeouts {
		cmdArgs = append(cmdArgs, "timeout", opts.Timeout)
	}

	if opts.MaxElem != "" {
		cmdArgs = append(cmdArgs, "maxelem", opts.MaxElem)
	}

	if opts.Family != "" {
		cmdArgs = append(cmdArgs, "family", opts.Family)
	}

	cmd := exec.Command(i.binaryPath, cmdArgs...)

	log.Debugf("ipset create command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error creating ipset: %s", out)
	}

	return nil
}

func (i *IPSet) Add(entry string) error {
	cmd := exec.Command(i.binaryPath, "add", i.setName, entry)

	log.Debugf("ipset add command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error creating ipset: %s", out)
	}

	return nil
}

func (i *IPSet) DeleteEntry(entry string) error {
	cmd := exec.Command(i.binaryPath, "del", i.setName, entry)

	log.Debugf("ipset delete entry command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error creating ipset: %s", out)
	}

	return nil
}

func (i *IPSet) List() ([]string, error) {
	cmd := exec.Command(i.binaryPath, "list", i.setName)

	log.Debugf("ipset list command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("error listing ipset: %s", out)
	}

	return strings.Split(string(out), "\n"), nil
}

func (i *IPSet) Flush() error {
	cmd := exec.Command(i.binaryPath, "flush", i.setName)

	log.Debugf("ipset flush command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error flushing ipset: %s", out)
	}

	return nil
}

func (i *IPSet) Destroy() error {
	cmd := exec.Command(i.binaryPath, "destroy", i.setName)

	log.Debugf("ipset destroy command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error destroying ipset: %s", out)
	}

	return nil
}

func (i *IPSet) Rename(toSetName string) error {
	cmd := exec.Command(i.binaryPath, "rename", i.setName, toSetName)

	log.Debugf("ipset rename command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error renaming ipset: %s", out)
	}

	i.setName = toSetName

	return nil
}

func (i *IPSet) Test(entry string) error {
	cmd := exec.Command(i.binaryPath, "test", i.setName, entry)

	log.Debugf("ipset test command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error testing ipset: %s", out)
	}

	return nil
}

func (i *IPSet) Save() ([]string, error) {
	cmd := exec.Command(i.binaryPath, "save", i.setName)

	log.Debugf("ipset save command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("error saving ipset: %s", out)
	}

	return strings.Split(string(out), "\n"), nil
}

func (i *IPSet) Restore(filename string) error {
	cmd := exec.Command(i.binaryPath, "restore", "-file", filename)

	log.Debugf("ipset restore command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error restoring ipset: %s", out)
	}

	return nil
}

func (i *IPSet) Swap(toSetName string) error {
	cmd := exec.Command(i.binaryPath, "swap", i.setName, toSetName)

	log.Debugf("ipset swap command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("error swapping ipset: %s", out)
	}

	i.setName = toSetName

	return nil
}

func (i *IPSet) Name() string {
	return i.setName
}

func (i *IPSet) Exists() bool {
	cmd := exec.Command(i.binaryPath, "list", i.setName)

	err := cmd.Run()

	return err == nil
}

func (i *IPSet) Len() int {
	cmd := exec.Command(i.binaryPath, "list", i.setName)

	log.Debugf("ipset list command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return 0
	}

	for _, line := range strings.Split(string(out), "\n") {
		if !strings.Contains(strings.ToLower(line), "number of entries:") {
			continue
		}

		fields := strings.Split(line, ":")
		if len(fields) != 2 {
			continue
		}

		count, err := strconv.Atoi(strings.TrimSpace(fields[1]))
		if err != nil {
			return 0
		}

		return count
	}

	return 0
}

// Helpers.
func GetSetsStartingWith(name string) (map[string]*IPSet, error) {
	cmd := exec.Command(ipsetBinary, "list", "-name")

	log.Debugf("ipset list command: %v", cmd.String())

	out, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("error listing ipset: %s", out)
	}

	sets := make(map[string]*IPSet, 0)

	for _, line := range strings.Split(string(out), "\n") {
		if !strings.HasPrefix(line, name) {
			continue
		}

		fields := strings.Fields(line)
		if len(fields) != 1 {
			continue
		}

		set, err := NewIPSet(fields[0])
		if err != nil {
			return nil, err
		}

		sets[fields[0]] = set
	}

	return sets, nil
}


================================================
FILE: pkg/iptables/iptables.go
================================================
//go:build linux

package iptables

import (
	"errors"
	"fmt"
	"os/exec"
	"slices"
	"strings"

	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/models"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/ipsetcmd"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/types"
)

const (
	IPTablesDroppedPacketIdx = 0
	IPTablesDroppedByteIdx   = 1
)

type iptables struct {
	v4 *ipTablesContext
	v6 *ipTablesContext
}

func NewIPTables(config *cfg.BouncerConfig) (types.Backend, error) {
	var err error

	ret := &iptables{}

	defaultSet, err := ipsetcmd.NewIPSet("")
	if err != nil {
		return nil, err
	}

	allowedActions := []string{"DROP", "REJECT", "TARPIT", "LOG"}

	target := strings.ToUpper(config.DenyAction)
	if target == "" {
		target = "DROP"
	}

	log.Infof("using '%s' as deny_action", target)

	if !slices.Contains(allowedActions, target) {
		return nil, fmt.Errorf("invalid deny_action '%s', must be one of %s", config.DenyAction, strings.Join(allowedActions, ", "))
	}

	v4Sets := make(map[string]*ipsetcmd.IPSet)
	v6Sets := make(map[string]*ipsetcmd.IPSet)

	ipv4Ctx := &ipTablesContext{
		version:              "v4",
		SetName:              config.BlacklistsIpv4,
		SetType:              config.SetType,
		SetSize:              config.SetSize,
		ipsetDisableTimeouts: config.SetDisableTimeouts,
		Chains:               []string{},
		defaultSet:           defaultSet,
		target:               target,
		loggingEnabled:       config.DenyLog,
		loggingPrefix:        config.DenyLogPrefix,
		addRuleComments:      config.IptablesAddRuleComments,
	}
	ipv6Ctx := &ipTablesContext{
		version:              "v6",
		SetName:              config.BlacklistsIpv6,
		SetType:              config.SetType,
		SetSize:              config.SetSize,
		ipsetDisableTimeouts: config.SetDisableTimeouts,
		Chains:               []string{},
		defaultSet:           defaultSet,
		target:               target,
		loggingEnabled:       config.DenyLog,
		loggingPrefix:        config.DenyLogPrefix,
		addRuleComments:      config.IptablesAddRuleComments,
	}

	if !config.DisableIPV4 {
		ipv4Ctx.iptablesSaveBin, err = exec.LookPath("iptables-save")
		if err != nil {
			return nil, errors.New("unable to find iptables-save")
		}

		if config.Mode == cfg.IpsetMode {
			ipv4Ctx.ipsetContentOnly = true

			set, err := ipsetcmd.NewIPSet(config.BlacklistsIpv4)
			if err != nil {
				return nil, err
			}

			v4Sets["ipset"] = set
		} else {
			ipv4Ctx.iptablesBin, err = exec.LookPath("iptables")
			if err != nil {
				return nil, errors.New("unable to find iptables")
			}

			// Try to "adopt" any leftover sets from a previous run if we crashed
			// They will get flushed/deleted just after
			v4Sets, _ = ipsetcmd.GetSetsStartingWith(config.BlacklistsIpv4)

			config.IptablesV4Chains = append(config.IptablesV4Chains, config.IptablesChains...)
			ipv4Ctx.Chains = config.IptablesV4Chains
		}

		ipv4Ctx.ipsets = v4Sets
		ret.v4 = ipv4Ctx
	}

	if !config.DisableIPV6 {
		ipv6Ctx.iptablesSaveBin, err = exec.LookPath("ip6tables-save")
		if err != nil {
			return nil, errors.New("unable to find ip6tables-save")
		}

		if config.Mode == cfg.IpsetMode {
			ipv6Ctx.ipsetContentOnly = true

			set, err := ipsetcmd.NewIPSet(config.BlacklistsIpv6)
			if err != nil {
				return nil, err
			}

			v6Sets["ipset"] = set
		} else {
			ipv6Ctx.iptablesBin, err = exec.LookPath("ip6tables")
			if err != nil {
				return nil, errors.New("unable to find ip6tables")
			}

			v6Sets, _ = ipsetcmd.GetSetsStartingWith(config.BlacklistsIpv6)
			config.IptablesV6Chains = append(config.IptablesV6Chains, config.IptablesChains...)
			ipv6Ctx.Chains = config.IptablesV6Chains
		}

		ipv6Ctx.ipsets = v6Sets
		ret.v6 = ipv6Ctx
	}

	return ret, nil
}

func (ipt *iptables) Init() error {
	if ipt.v4 != nil {
		log.Info("iptables for ipv4 initiated")

		// flush before init
		if err := ipt.v4.shutDown(); err != nil {
			return fmt.Errorf("iptables shutdown failed: %w", err)
		}

		if !ipt.v4.ipsetContentOnly {
			ipt.v4.setupChain()
		}
	}

	if ipt.v6 != nil {
		log.Info("iptables for ipv6 initiated")

		if err := ipt.v6.shutDown(); err != nil {
			return fmt.Errorf("iptables shutdown failed: %w", err)
		}

		if !ipt.v6.ipsetContentOnly {
			ipt.v6.setupChain()
		}
	}

	return nil
}

func (ipt *iptables) Commit() error {
	if ipt.v4 != nil {
		err := ipt.v4.commit()
		if err != nil {
			return fmt.Errorf("ipset for ipv4 commit failed: %w", err)
		}
	}

	if ipt.v6 != nil {
		err := ipt.v6.commit()
		if err != nil {
			return fmt.Errorf("ipset for ipv6 commit failed: %w", err)
		}
	}

	return nil
}

func (ipt *iptables) Add(decision *models.Decision) error {
	if strings.HasPrefix(*decision.Type, "simulation:") {
		log.Debugf("measure against '%s' is in simulation mode, skipping it", *decision.Value)
		return nil
	}

	if strings.Contains(*decision.Value, ":") {
		if ipt.v6 == nil {
			log.Debugf("not adding '%s' because ipv6 is disabled", *decision.Value)
			return nil
		}

		ipt.v6.add(decision)
	} else {
		if ipt.v4 == nil {
			log.Debugf("not adding '%s' because ipv4 is disabled", *decision.Value)
			return nil
		}
		ipt.v4.add(decision)
	}

	return nil
}

func (ipt *iptables) ShutDown() error {
	if ipt.v4 != nil {
		if err := ipt.v4.shutDown(); err != nil {
			return fmt.Errorf("iptables for ipv4 shutdown failed: %w", err)
		}
	}

	if ipt.v6 != nil {
		if err := ipt.v6.shutDown(); err != nil {
			return fmt.Errorf("iptables for ipv6 shutdown failed: %w", err)
		}
	}

	return nil
}

func (ipt *iptables) Delete(decision *models.Decision) error {
	done := false

	if strings.Contains(*decision.Value, ":") {
		if ipt.v6 == nil {
			log.Debugf("not deleting '%s' because ipv6 is disabled", *decision.Value)
			return nil
		}

		if err := ipt.v6.delete(decision); err != nil {
			return errors.New("failed deleting ban")
		}

		done = true
	}

	if strings.Contains(*decision.Value, ".") {
		if ipt.v4 == nil {
			log.Debugf("not deleting '%s' because ipv4 is disabled", *decision.Value)
			return nil
		}

		if err := ipt.v4.delete(decision); err != nil {
			return errors.New("failed deleting ban")
		}

		done = true
	}

	if !done {
		return fmt.Errorf("failed deleting ban: ip %s was not recognized", *decision.Value)
	}

	return nil
}


================================================
FILE: pkg/iptables/iptables_context.go
================================================
//go:build linux

package iptables

import (
	"fmt"
	"os"
	"os/exec"
	"slices"
	"strconv"
	"strings"
	"time"

	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/models"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/ipsetcmd"
)

const (
	chainName           = "CROWDSEC_CHAIN"
	loggingChainName    = "CROWDSEC_LOG"
	dockerUserChainName = "DOCKER-USER"
	maxBanSeconds       = 2147483
	defaultTimeout      = "300"
)

type ipTablesContext struct {
	version              string
	iptablesBin          string
	iptablesSaveBin      string
	SetName              string // crowdsec-netfilter
	SetType              string
	SetSize              int
	ipsetContentOnly     bool
	ipsetDisableTimeouts bool
	Chains               []string

	target string

	ipsets     map[string]*ipsetcmd.IPSet
	defaultSet *ipsetcmd.IPSet // This one is only used to restore the content, as the file will contain the name of the set for each decision

	toAdd []*models.Decision
	toDel []*models.Decision

	// To avoid issues with set name length (ipsest name length is limited to 31 characters)
	// Store the origin of the decisions, and use the index in the slice as the name
	// This is not stable (ie, between two runs, the index of a set can change), but it's (probably) not an issue
	originSetMapping []string

	loggingEnabled bool
	loggingPrefix  string

	addRuleComments bool
}

func (ctx *ipTablesContext) chainExist(chainName string) bool {
	cmd := []string{"-L", chainName, "-t", "filter"}
	c := exec.Command(ctx.iptablesBin, cmd...)

	if _, err := c.CombinedOutput(); err != nil {
		return false
	}

	return true
}

func (ctx *ipTablesContext) setupChain() {
	cmd := []string{"-N", chainName, "-t", "filter"}

	c := exec.Command(ctx.iptablesBin, cmd...)

	log.Infof("Creating chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

	if out, err := c.CombinedOutput(); err != nil {
		log.Errorf("error while creating chain : %v --> %s", err, string(out))
		return
	}

	for _, chain := range ctx.Chains {
		cmd = []string{"-I", chain, "-j", chainName}

		c = exec.Command(ctx.iptablesBin, cmd...)

		log.Infof("Adding rule : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

		if out, err := c.CombinedOutput(); err != nil {
			log.Errorf("error while adding rule : %v --> %s", err, string(out))
			continue
		}
	}

	if ctx.loggingEnabled {
		// Create the logging chain
		cmd = []string{"-N", loggingChainName, "-t", "filter"}

		c = exec.Command(ctx.iptablesBin, cmd...)

		log.Infof("Creating logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

		if out, err := c.CombinedOutput(); err != nil {
			log.Errorf("error while creating logging chain : %v --> %s", err, string(out))
			return
		}

		// Insert the logging rule
		cmd = []string{"-I", loggingChainName, "-j", "LOG", "--log-prefix", ctx.loggingPrefix}

		c = exec.Command(ctx.iptablesBin, cmd...)

		log.Infof("Adding logging rule : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

		if out, err := c.CombinedOutput(); err != nil {
			log.Errorf("error while adding logging rule : %v --> %s", err, string(out))
		}

		// Add the desired target to the logging chain

		cmd = []string{"-A", loggingChainName, "-j", ctx.target}

		c = exec.Command(ctx.iptablesBin, cmd...)

		log.Infof("Adding target rule to logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

		if out, err := c.CombinedOutput(); err != nil {
			log.Errorf("error while setting logging chain policy : %v --> %s", err, string(out))
		}
	}

	if ctx.chainExist(dockerUserChainName) && !slices.Contains(ctx.Chains, dockerUserChainName) {
		// if the DOCKER-USER chain exists, but is not configured by the user, warn them as their containers will not be protected
		log.Warnf("The %s chain exists, but is not configured for use by the bouncer. The bouncer will not block traffic destined for your containers", dockerUserChainName)
	}
}

func (ctx *ipTablesContext) deleteChain() {
	for _, chain := range ctx.Chains {
		cmd := []string{"-D", chain, "-j", chainName}

		c := exec.Command(ctx.iptablesBin, cmd...)

		log.Infof("Deleting rule : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

		if out, err := c.CombinedOutput(); err != nil {
			log.Errorf("error while removing rule : %v --> %s", err, string(out))
		}
	}

	cmd := []string{"-F", chainName}

	c := exec.Command(ctx.iptablesBin, cmd...)

	log.Infof("Flushing chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

	if out, err := c.CombinedOutput(); err != nil {
		log.Errorf("error while flushing chain : %v --> %s", err, string(out))
	}

	cmd = []string{"-X", chainName}

	c = exec.Command(ctx.iptablesBin, cmd...)

	log.Infof("Deleting chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

	if out, err := c.CombinedOutput(); err != nil {
		log.Errorf("error while deleting chain : %v --> %s", err, string(out))
	}

	if ctx.loggingEnabled {
		cmd = []string{"-F", loggingChainName}

		c = exec.Command(ctx.iptablesBin, cmd...)

		log.Infof("Flushing logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

		if out, err := c.CombinedOutput(); err != nil {
			log.Errorf("error while flushing logging chain : %v --> %s", err, string(out))
		}

		cmd = []string{"-X", loggingChainName}

		c = exec.Command(ctx.iptablesBin, cmd...)

		log.Infof("Deleting logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

		if out, err := c.CombinedOutput(); err != nil {
			log.Errorf("error while deleting logging chain : %v --> %s", err, string(out))
		}
	}
}

func (ctx *ipTablesContext) createRule(setName string, origin string) {
	target := ctx.target

	if ctx.loggingEnabled {
		target = loggingChainName
	}

	cmd := []string{"-I", chainName, "-m", "set", "--match-set", setName, "src", "-j", target}

	if ctx.addRuleComments {
		cmd = append(cmd, "-m", "comment", "--comment", "CrowdSec: "+origin)
	}

	c := exec.Command(ctx.iptablesBin, cmd...)

	log.Infof("Creating rule : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

	if out, err := c.CombinedOutput(); err != nil {
		log.Errorf("error while inserting set entry in iptables : %v --> %s", err, string(out))
	}
}

func (ctx *ipTablesContext) commit() error {
	tmpFile, err := os.CreateTemp("", "cs-firewall-bouncer-ipset-")
	if err != nil {
		return err
	}

	defer func() {
		tmpFile.Close()
		os.Remove(tmpFile.Name())

		ctx.toAdd = nil
		ctx.toDel = nil
	}()

	for _, decision := range ctx.toDel {
		var (
			set *ipsetcmd.IPSet
			ok  bool
		)

		// Decisions coming from lists will have "lists" as origin, and the scenario will be the list name
		// We use those to build a custom origin because we want to track metrics per list
		// In case of other origin (crowdsec, cscli, ...), we do not really care about the scenario, it would be too noisy
		origin := *decision.Origin
		if origin == "lists" {
			origin = origin + ":" + *decision.Scenario
		}

		if ctx.ipsetContentOnly {
			set = ctx.ipsets["ipset"]
		} else {
			set, ok = ctx.ipsets[origin]
			if !ok {
				// No set for this origin, skip, as there's nothing to delete
				continue
			}
		}

		delCmd := fmt.Sprintf("del %s %s -exist\n", set.Name(), *decision.Value)

		log.Debugf("%s", delCmd)

		_, err = tmpFile.WriteString(delCmd)
		if err != nil {
			log.Errorf("error while writing to temp file : %s", err)
			continue
		}
	}

	for _, decision := range ctx.toAdd {
		banDuration, err := time.ParseDuration(*decision.Duration)
		if err != nil {
			log.Errorf("error while parsing ban duration : %s", err)
			continue
		}

		var (
			set *ipsetcmd.IPSet
			ok  bool
		)

		if banDuration.Seconds() > maxBanSeconds {
			log.Warnf("Ban duration too long (%d seconds), maximum for ipset is %d, setting duration to %d", int(banDuration.Seconds()), maxBanSeconds, maxBanSeconds-1)
			banDuration = time.Duration(maxBanSeconds-1) * time.Second
		}

		origin := *decision.Origin

		if origin == "lists" {
			origin = origin + ":" + *decision.Scenario
		}

		if ctx.ipsetContentOnly {
			set = ctx.ipsets["ipset"]
		} else {
			set, ok = ctx.ipsets[origin]

			if !ok {
				idx := slices.Index(ctx.originSetMapping, origin)

				if idx == -1 {
					ctx.originSetMapping = append(ctx.originSetMapping, origin)
					idx = len(ctx.originSetMapping) - 1
				}

				setName := fmt.Sprintf("%s-%d", ctx.SetName, idx)

				log.Infof("Using %s as set for origin %s", setName, origin)

				set, err = ipsetcmd.NewIPSet(setName)
				if err != nil {
					log.Errorf("error while creating ipset : %s", err)
					continue
				}

				family := "inet"

				if ctx.version == "v6" {
					family = "inet6"
				}

				err = set.Create(ipsetcmd.CreateOptions{
					Family:          family,
					Timeout:         defaultTimeout,
					MaxElem:         strconv.Itoa(ctx.SetSize),
					Type:            ctx.SetType,
					DisableTimeouts: ctx.ipsetDisableTimeouts,
				})
				// Ignore errors if the set already exists
				if err != nil {
					log.Errorf("error while creating ipset : %s", err)
					continue
				}

				ctx.ipsets[origin] = set

				if !ctx.ipsetContentOnly {
					// Create the rule to use the set
					ctx.createRule(set.Name(), origin)
				}
			}
		}

		var addCmd string
		if ctx.ipsetDisableTimeouts {
			addCmd = fmt.Sprintf("add %s %s -exist\n", set.Name(), *decision.Value)
		} else {
			addCmd = fmt.Sprintf("add %s %s timeout %d -exist\n", set.Name(), *decision.Value, int(banDuration.Seconds()))
		}

		log.Debugf("%s", addCmd)

		_, err = tmpFile.WriteString(addCmd)
		if err != nil {
			log.Errorf("error while writing to temp file : %s", err)
			continue
		}
	}

	if len(ctx.toAdd) == 0 && len(ctx.toDel) == 0 {
		return nil
	}

	return ctx.defaultSet.Restore(tmpFile.Name())
}

func (ctx *ipTablesContext) add(decision *models.Decision) {
	ctx.toAdd = append(ctx.toAdd, decision)
}

func (ctx *ipTablesContext) shutDown() error {
	// Remove rules
	if !ctx.ipsetContentOnly {
		ctx.deleteChain()
	}

	time.Sleep(1 * time.Second)

	// Clean sets
	for _, set := range ctx.ipsets {
		if ctx.ipsetContentOnly {
			err := set.Flush()
			if err != nil {
				log.Errorf("error while flushing ipset : %s", err)
			}
		} else {
			err := set.Destroy()
			if err != nil {
				log.Errorf("error while destroying set %s : %s", set.Name(), err)
			}
		}
	}

	if !ctx.ipsetContentOnly {
		// In case we are starting, just reset the map
		ctx.ipsets = make(map[string]*ipsetcmd.IPSet)
	}

	return nil
}

func (ctx *ipTablesContext) delete(decision *models.Decision) error {
	ctx.toDel = append(ctx.toDel, decision)
	return nil
}


================================================
FILE: pkg/iptables/iptables_stub.go
================================================
//go:build !linux

package iptables

import (
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/types"
)

func NewIPTables(config *cfg.BouncerConfig) (types.Backend, error) {
	return nil, nil
}


================================================
FILE: pkg/iptables/metrics.go
================================================
//go:build linux

package iptables

import (
	"bufio"
	"os/exec"
	"regexp"
	"strconv"
	"strings"

	"github.com/prometheus/client_golang/prometheus"
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/metrics"
)

// iptables does not provide a "nice" way to get the counters for a rule, so we have to parse the output of iptables-save
// chainRegexp is just used to get the counters for the chain CROWDSEC_CHAIN (the chain managed by the bouncer that will contains our rules) from the JUMP rule
// ruleRegexp is used to get the counters for the rules we have added that will actually block the traffic
// Example output of iptables-save :
// [2080:13210403] -A INPUT -j CROWDSEC_CHAIN
// ...
// [0:0] -A CROWDSEC_CHAIN -m set --match-set test-set-ipset-mode-0 src -j DROP
// First number is the number of packets, second is the number of bytes
// In case of a jump, the counters represent the number of packets and bytes that have been processed by the chain (ie, whether the packets have been accepted or dropped)
// In case of a rule, the counters represent the number of packets and bytes that have been matched by the rule (ie, the packets that have been dropped).

var (
	chainRegexp = regexp.MustCompile(`^\[(\d+):(\d+)\]`)
	ruleRegexp  = regexp.MustCompile(`^\[(\d+):(\d+)\] -A [0-9A-Za-z_-]+ -m set --match-set (.*) src .*-j \w+`)
)

// In ipset mode, we have to track the numbers of processed bytes/packets at the chain level
// This is not really accurate, as a rule *before* the crowdsec rule could impact the numbers, but we don't have any other way.

var (
	ipsetChainDeclaration = regexp.MustCompile(`^:([0-9A-Za-z_-]+) ([0-9A-Za-z_-]+) \[(\d+):(\d+)\]`)
	ipsetRule             = regexp.MustCompile(`^\[(\d+):(\d+)\] -A ([0-9A-Za-z_-]+)`)
)

func (ctx *ipTablesContext) collectMetricsIptables(scanner *bufio.Scanner) (map[string]uint64, map[string]uint64, uint64, uint64) {
	processedBytes := uint64(0)
	processedPackets := uint64(0)

	droppedBytes := make(map[string]uint64)
	droppedPackets := make(map[string]uint64)

	for scanner.Scan() {
		line := scanner.Text()

		if line == "" {
			continue
		}

		// Ignore chain declaration
		if line[0] == ':' {
			continue
		}

		// Jump to our chain, we can get the processed packets and bytes
		if strings.Contains(line, "-j "+chainName) {
			matches := chainRegexp.FindStringSubmatch(line)
			if len(matches) != 3 {
				log.Errorf("error while parsing counters : %s | not enough matches", line)
				continue
			}

			val, err := strconv.ParseUint(matches[1], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s | %s", line, err)
				continue
			}

			processedPackets += val

			val, err = strconv.ParseUint(matches[2], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s | %s", line, err)
				continue
			}

			processedBytes += val

			continue
		}

		// This is a rule
		if strings.Contains(line, "-A "+chainName) {
			matches := ruleRegexp.FindStringSubmatch(line)
			if len(matches) != 4 {
				log.Errorf("error while parsing counters : %s | not enough matches", line)
				continue
			}

			originIDStr, found := strings.CutPrefix(matches[3], ctx.SetName+"-")
			if !found {
				log.Errorf("error while parsing counters : %s | no origin found", line)
				continue
			}

			originID, err := strconv.Atoi(originIDStr)
			if err != nil {
				log.Errorf("error while parsing counters : %s | %s", line, err)
				continue
			}

			if len(ctx.originSetMapping) <= originID {
				log.Errorf("Found unknown origin id : %d", originID)
				continue
			}

			origin := ctx.originSetMapping[originID]

			val, err := strconv.ParseUint(matches[1], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s | %s", line, err)
				continue
			}

			droppedPackets[origin] += val

			val, err = strconv.ParseUint(matches[2], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s | %s", line, err)
				continue
			}

			droppedBytes[origin] += val
		}
	}

	return droppedPackets, droppedBytes, processedPackets, processedBytes
}

type chainCounters struct {
	bytes   uint64
	packets uint64
}

// In ipset mode, we only get dropped packets and bytes by matching on the set name in the rule
// It's probably not perfect, but good enough for most users.
func (ctx *ipTablesContext) collectMetricsIpset(scanner *bufio.Scanner) (map[string]uint64, map[string]uint64, uint64, uint64) {
	processedBytes := uint64(0)
	processedPackets := uint64(0)

	droppedBytes := make(map[string]uint64)
	droppedPackets := make(map[string]uint64)

	// We need to store the counters for all chains
	// As we don't know in which chain the user has setup the rules
	// We'll resolve the value laters.
	chainsCounter := make(map[string]chainCounters)

	// Hardcode the origin to ipset as we cannot know it based on the rule.
	droppedBytes["ipset"] = 0
	droppedPackets["ipset"] = 0

	for scanner.Scan() {
		line := scanner.Text()

		if line == "" {
			continue
		}

		// Chain declaration
		if line[0] == ':' {
			matches := ipsetChainDeclaration.FindStringSubmatch(line)
			if len(matches) != 5 {
				log.Errorf("error while parsing counters : %s | not enough matches", line)
				continue
			}

			log.Debugf("Found chain %s with matches %+v", matches[1], matches)

			c, ok := chainsCounter[matches[1]]
			if !ok {
				c = chainCounters{}
			}

			val, err := strconv.ParseUint(matches[3], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s", line)
				continue
			}

			c.packets += val

			val, err = strconv.ParseUint(matches[4], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s", line)
				continue
			}

			c.bytes += val
			chainsCounter[matches[1]] = c

			continue
		}

		// Assume that if a line contains the set name, it's a rule we are interested in.
		if strings.Contains(line, ctx.SetName) {
			matches := ipsetRule.FindStringSubmatch(line)
			if len(matches) != 4 {
				log.Errorf("error while parsing counters : %s | not enough matches", line)
				continue
			}

			val, err := strconv.ParseUint(matches[1], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s", line)
				continue
			}

			droppedPackets["ipset"] += val

			val, err = strconv.ParseUint(matches[2], 10, 64)
			if err != nil {
				log.Errorf("error while parsing counters : %s", line)
				continue
			}

			droppedBytes["ipset"] += val

			// Resolve the chain counters
			c, ok := chainsCounter[matches[3]]
			if !ok {
				log.Errorf("error while parsing counters : %s | chain not found", line)
				continue
			}

			processedPackets += c.packets
			processedBytes += c.bytes
		}
	}

	return droppedPackets, droppedBytes, processedPackets, processedBytes
}

func (ctx *ipTablesContext) collectMetrics() (map[string]uint64, map[string]uint64, uint64, uint64, error) {
	//-c is required to get the counters
	cmd := []string{ctx.iptablesSaveBin, "-c", "-t", "filter"}
	saveCmd := exec.Command(cmd[0], cmd[1:]...)

	out, err := saveCmd.CombinedOutput()
	if err != nil {
		log.Errorf("error while getting iptables rules with cmd %+v : %v --> %s", cmd, err, string(out))
		return nil, nil, 0, 0, err
	}

	var (
		processedBytes   uint64
		processedPackets uint64
		droppedBytes     map[string]uint64
		droppedPackets   map[string]uint64
	)

	scanner := bufio.NewScanner(strings.NewReader(string(out)))

	if !ctx.ipsetContentOnly {
		droppedPackets, droppedBytes, processedPackets, processedBytes = ctx.collectMetricsIptables(scanner)
	} else {
		droppedPackets, droppedBytes, processedPackets, processedBytes = ctx.collectMetricsIpset(scanner)
	}

	log.Debugf("Processed %d packets and %d bytes", processedPackets, processedBytes)
	log.Debugf("Dropped packets : %v", droppedPackets)
	log.Debugf("Dropped bytes : %v", droppedBytes)

	return droppedPackets, droppedBytes, processedPackets, processedBytes, nil
}

func (ipt *iptables) CollectMetrics() {
	if ipt.v4 != nil {
		for origin, set := range ipt.v4.ipsets {
			metrics.Map[metrics.ActiveBannedIPs].Gauge.With(prometheus.Labels{"ip_type": "ipv4", "origin": origin}).Set(float64(set.Len()))
		}

		ipv4DroppedPackets, ipv4DroppedBytes, ipv4ProcessedPackets, ipv4ProcessedBytes, err := ipt.v4.collectMetrics()
		if err != nil {
			log.Errorf("can't collect dropped packets for ipv4 from iptables: %s", err)
		} else {
			metrics.Map[metrics.ProcessedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv4"}).Set(float64(ipv4ProcessedPackets))
			metrics.Map[metrics.ProcessedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv4"}).Set(float64(ipv4ProcessedBytes))

			for origin, count := range ipv4DroppedPackets {
				metrics.Map[metrics.DroppedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv4", "origin": origin}).Set(float64(count))
			}

			for origin, count := range ipv4DroppedBytes {
				metrics.Map[metrics.DroppedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv4", "origin": origin}).Set(float64(count))
			}
		}
	}

	if ipt.v6 != nil {
		for origin, set := range ipt.v6.ipsets {
			metrics.Map[metrics.ActiveBannedIPs].Gauge.With(prometheus.Labels{"ip_type": "ipv6", "origin": origin}).Set(float64(set.Len()))
		}

		ipv6DroppedPackets, ipv6DroppedBytes, ipv6ProcessedPackets, ipv6ProcessedBytes, err := ipt.v6.collectMetrics()
		if err != nil {
			log.Errorf("can't collect dropped packets for ipv6 from iptables: %s", err)
		} else {
			metrics.Map[metrics.ProcessedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv6"}).Set(float64(ipv6ProcessedPackets))
			metrics.Map[metrics.ProcessedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv6"}).Set(float64(ipv6ProcessedBytes))

			for origin, count := range ipv6DroppedPackets {
				metrics.Map[metrics.DroppedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv6", "origin": origin}).Set(float64(count))
			}

			for origin, count := range ipv6DroppedBytes {
				metrics.Map[metrics.DroppedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv6", "origin": origin}).Set(float64(count))
			}
		}
	}
}


================================================
FILE: pkg/metrics/metrics.go
================================================
package metrics

import (
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	io_prometheus_client "github.com/prometheus/client_model/go"
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/go-cs-lib/ptr"

	"github.com/crowdsecurity/crowdsec/pkg/models"
)

const CollectionInterval = time.Second * 10

type metricName string

const (
	DroppedPackets   metricName = "fw_bouncer_dropped_packets"
	DroppedBytes     metricName = "fw_bouncer_dropped_bytes"
	ProcessedPackets metricName = "fw_bouncer_processed_packets"
	ProcessedBytes   metricName = "fw_bouncer_processed_bytes"
	ActiveBannedIPs  metricName = "fw_bouncer_banned_ips"
)

type backendCollector interface {
	CollectMetrics()
}

type Handler struct {
	Backend backendCollector
}

type metricConfig struct {
	Name         string
	Unit         string
	Gauge        *prometheus.GaugeVec
	LabelKeys    []string
	LastValueMap map[string]float64 // keep last value to send deltas -- nil if absolute
	KeyFunc      func(labels []*io_prometheus_client.LabelPair) string
}

type metricMap map[metricName]*metricConfig

func (m metricMap) MustRegisterAll() {
	for _, met := range m {
		prometheus.MustRegister(met.Gauge)
	}
}

var Map = metricMap{
	ActiveBannedIPs: {
		Name: "active_decisions",
		Unit: "ip",
		Gauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Name: string(ActiveBannedIPs),
			Help: "Denotes the number of IPs which are currently banned",
		}, []string{"origin", "ip_type"}),
		LabelKeys:    []string{"origin", "ip_type"},
		LastValueMap: nil,
		KeyFunc:      func([]*io_prometheus_client.LabelPair) string { return "" },
	},
	DroppedBytes: {
		Name: "dropped",
		Unit: "byte",
		Gauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Name: string(DroppedBytes),
			Help: "Denotes the number of total dropped bytes because of rule(s) created by crowdsec",
		}, []string{"origin", "ip_type"}),
		LabelKeys:    []string{"origin", "ip_type"},
		LastValueMap: make(map[string]float64),
		KeyFunc: func(labels []*io_prometheus_client.LabelPair) string {
			return getLabelValue(labels, "origin") + getLabelValue(labels, "ip_type")
		},
	},
	DroppedPackets: {
		Name: "dropped",
		Unit: "packet",
		Gauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Name: string(DroppedPackets),
			Help: "Denotes the number of total dropped packets because of rule(s) created by crowdsec",
		}, []string{"origin", "ip_type"}),
		LabelKeys:    []string{"origin", "ip_type"},
		LastValueMap: make(map[string]float64),
		KeyFunc: func(labels []*io_prometheus_client.LabelPair) string {
			return getLabelValue(labels, "origin") + getLabelValue(labels, "ip_type")
		},
	},
	ProcessedBytes: {
		Name: "processed",
		Unit: "byte",
		Gauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Name: string(ProcessedBytes),
			Help: "Denotes the number of total processed bytes by the rules created by crowdsec",
		}, []string{"ip_type"}),
		LabelKeys:    []string{"ip_type"},
		LastValueMap: make(map[string]float64),
		KeyFunc: func(labels []*io_prometheus_client.LabelPair) string {
			return getLabelValue(labels, "ip_type")
		},
	},
	ProcessedPackets: {
		Name: "processed",
		Unit: "packet",
		Gauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Name: string(ProcessedPackets),
			Help: "Denotes the number of total processed packets by the rules created by crowdsec",
		}, []string{"ip_type"}),
		LabelKeys:    []string{"ip_type"},
		LastValueMap: make(map[string]float64),
		KeyFunc: func(labels []*io_prometheus_client.LabelPair) string {
			return getLabelValue(labels, "ip_type")
		},
	},
}

func getLabelValue(labels []*io_prometheus_client.LabelPair, key string) string {
	for _, label := range labels {
		if label.GetName() == key {
			return label.GetValue()
		}
	}

	return ""
}

// MetricsUpdater receives a metrics struct with basic data and populates it with the current metrics.
func (m Handler) MetricsUpdater(met *models.RemediationComponentsMetrics, updateInterval time.Duration) {
	log.Debugf("Updating metrics")

	m.Backend.CollectMetrics()

	// Most of the common fields are set automatically by the metrics provider
	// We only need to care about the metrics themselves

	promMetrics, err := prometheus.DefaultGatherer.Gather()
	if err != nil {
		log.Errorf("unable to gather prometheus metrics: %s", err)
		return
	}

	met.Metrics = append(met.Metrics, &models.DetailedMetrics{
		Meta: &models.MetricsMeta{
			UtcNowTimestamp:   ptr.Of(time.Now().Unix()),
			WindowSizeSeconds: ptr.Of(int64(updateInterval.Seconds())),
		},
		Items: make([]*models.MetricsDetailItem, 0),
	})

	for _, metricFamily := range promMetrics {
		cfg, ok := Map[metricName(metricFamily.GetName())]
		if !ok {
			continue
		}

		for _, metric := range metricFamily.GetMetric() {
			labels := metric.GetLabel()
			value := metric.GetGauge().GetValue()

			labelMap := make(map[string]string)
			for _, key := range cfg.LabelKeys {
				labelMap[key] = getLabelValue(labels, key)
			}

			finalValue := value

			if cfg.LastValueMap == nil {
				// always send absolute values
				log.Debugf("Sending %s for %+v %f", cfg.Name, labelMap, finalValue)
			} else {
				// the final value to send must be relative, and never negative
				// because the firewall counter may have been reset since last collection.
				key := cfg.KeyFunc(labels)

				// no need to guard access to LastValueMap, as we are in the main thread -- it's
				// the gauge that is updated by the requests
				finalValue = value - cfg.LastValueMap[key]

				if finalValue < 0 {
					finalValue = -finalValue

					log.Warningf("metric value for %s %+v is negative, assuming external counter was reset", cfg.Name, labelMap)
				}

				cfg.LastValueMap[key] = value
				log.Debugf("Sending %s for %+v %f | current value: %f | previous value: %f", cfg.Name, labelMap, finalValue, value, cfg.LastValueMap[key])
			}

			met.Metrics[0].Items = append(met.Metrics[0].Items, &models.MetricsDetailItem{
				Name:   ptr.Of(cfg.Name),
				Value:  &finalValue,
				Labels: labelMap,
				Unit:   ptr.Of(cfg.Unit),
			})
		}
	}
}

func (m Handler) ComputeMetricsHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		m.Backend.CollectMetrics()
		next.ServeHTTP(w, r)
	})
}


================================================
FILE: pkg/nftables/metrics.go
================================================
//go:build linux

package nftables

import (
	"fmt"
	"strings"
	"time"

	"github.com/google/nftables"
	"github.com/google/nftables/expr"
	"github.com/prometheus/client_golang/prometheus"
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/metrics"
)

func (c *nftContext) collectDroppedPackets() (map[string]uint64, map[string]uint64, uint64, uint64, error) {
	droppedPackets := make(map[string]uint64)
	droppedBytes := make(map[string]uint64)
	processedPackets := uint64(0)
	processedBytes := uint64(0)

	objs, err := c.conn.GetNamedObjects(c.table)
	if err != nil {
		return nil, nil, 0, 0, fmt.Errorf("can't get named objects for table %s: %w", c.table.Name, err)
	}

	for _, obj := range objs {
		o, ok := obj.(*nftables.NamedObj)
		if !ok {
			continue
		}

		if o.Type != nftables.ObjTypeCounter {
			continue
		}

		counterObj, ok := o.Obj.(*expr.Counter)
		if !ok {
			continue
		}

		if o.Name == "processed" {
			processedPackets = counterObj.Packets
			processedBytes = counterObj.Bytes

			continue
		}

		origin, found := strings.CutPrefix(o.Name, c.blacklists+"-")
		if !found || origin == "" {
			continue
		}

		droppedPackets[origin] += counterObj.Packets
		droppedBytes[origin] += counterObj.Bytes
	}

	return droppedPackets, droppedBytes, processedPackets, processedBytes, nil
}

func (c *nftContext) collectActiveBannedIPs() (map[string]int, error) {
	// Find the size of the set we have created
	ret := make(map[string]int)

	for origin, set := range c.sets {
		setContent, err := c.conn.GetSetElements(set)
		if err != nil {
			return nil, fmt.Errorf("can't get set elements for %s: %w", set.Name, err)
		}

		if c.setOnly {
			ret[c.blacklists] = len(setContent)
		} else {
			ret[origin] = len(setContent)
		}

		return ret, nil
	}

	return ret, nil
}

func (c *nftContext) collectDropped() (map[string]uint64, map[string]uint64, uint64, uint64, map[string]int) {
	if c.conn == nil {
		return nil, nil, 0, 0, nil
	}

	droppedPackets, droppedBytes, processedPackets, processedBytes, err := c.collectDroppedPackets()
	if err != nil {
		log.Errorf("can't collect dropped packets for ip%s from nft: %s", c.version, err)
	}

	banned, err := c.collectActiveBannedIPs()
	if err != nil {
		log.Errorf("can't collect total banned IPs for ip%s from nft: %s", c.version, err)
	}

	return droppedPackets, droppedBytes, processedPackets, processedBytes, banned
}

func getOriginForList(origin string) string {
	if !strings.HasPrefix(origin, "lists-") {
		return origin
	}

	return strings.Replace(origin, "-", ":", 1)
}

func (n *nft) CollectMetrics() {
	startTime := time.Now()
	ip4DroppedPackets, ip4DroppedBytes, ip4ProcessedPackets, ip4ProcessedBytes, bannedIP4 := n.v4.collectDropped()
	ip6DroppedPackets, ip6DroppedBytes, ip6ProcessedPackets, ip6ProcessedBytes, bannedIP6 := n.v6.collectDropped()

	log.Debugf("metrics collection took %s", time.Since(startTime))
	log.Debugf("ip4: dropped packets: %+v, dropped bytes: %+v, banned IPs: %+v, proccessed packets: %d, processed bytes: %d", ip4DroppedPackets, ip4DroppedBytes, bannedIP4, ip4ProcessedPackets, ip4ProcessedBytes)
	log.Debugf("ip6: dropped packets: %+v, dropped bytes: %+v, banned IPs: %+v, proccessed packets: %d, processed bytes: %d", ip6DroppedPackets, ip6DroppedBytes, bannedIP6, ip6ProcessedPackets, ip6ProcessedBytes)

	metrics.Map[metrics.ProcessedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv4"}).Set(float64(ip4ProcessedPackets))
	metrics.Map[metrics.ProcessedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv4"}).Set(float64(ip4ProcessedBytes))

	metrics.Map[metrics.ProcessedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv6"}).Set(float64(ip6ProcessedPackets))
	metrics.Map[metrics.ProcessedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv6"}).Set(float64(ip6ProcessedBytes))

	for origin, count := range bannedIP4 {
		origin = getOriginForList(origin)
		metrics.Map[metrics.ActiveBannedIPs].Gauge.With(prometheus.Labels{"origin": origin, "ip_type": "ipv4"}).Set(float64(count))
	}

	for origin, count := range bannedIP6 {
		origin = getOriginForList(origin)
		metrics.Map[metrics.ActiveBannedIPs].Gauge.With(prometheus.Labels{"origin": origin, "ip_type": "ipv6"}).Set(float64(count))
	}

	for origin, count := range ip4DroppedPackets {
		origin = getOriginForList(origin)
		metrics.Map[metrics.DroppedPackets].Gauge.With(prometheus.Labels{"origin": origin, "ip_type": "ipv4"}).Set(float64(count))
	}

	for origin, count := range ip6DroppedPackets {
		origin = getOriginForList(origin)
		metrics.Map[metrics.DroppedPackets].Gauge.With(prometheus.Labels{"origin": origin, "ip_type": "ipv6"}).Set(float64(count))
	}

	for origin, count := range ip4DroppedBytes {
		origin = getOriginForList(origin)
		metrics.Map[metrics.DroppedBytes].Gauge.With(prometheus.Labels{"origin": origin, "ip_type": "ipv4"}).Set(float64(count))
	}

	for origin, count := range ip6DroppedBytes {
		origin = getOriginForList(origin)
		metrics.Map[metrics.DroppedBytes].Gauge.With(prometheus.Labels{"origin": origin, "ip_type": "ipv6"}).Set(float64(count))
	}
}


================================================
FILE: pkg/nftables/nftables.go
================================================
//go:build linux

package nftables

import (
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/google/nftables"
	"github.com/google/nftables/binaryutil"
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/models"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
)

const (
	chunkSize      = 200
	defaultTimeout = "4h"
)

type nft struct {
	v4                *nftContext
	v6                *nftContext
	decisionsToAdd    []*models.Decision
	decisionsToDelete []*models.Decision
	DenyAction        string
	DenyLog           bool
	DenyLogPrefix     string
	Hooks             []string
}

func NewNFTables(config *cfg.BouncerConfig) (*nft, error) {
	ret := &nft{
		v4:            NewNFTV4Context(config),
		v6:            NewNFTV6Context(config),
		DenyAction:    config.DenyAction,
		DenyLog:       config.DenyLog,
		DenyLogPrefix: config.DenyLogPrefix,
		Hooks:         config.NftablesHooks,
	}

	return ret, nil
}

func (n *nft) Init() error {
	log.Debug("nftables: Init()")

	if err := n.v4.init(n.Hooks); err != nil {
		return err
	}

	if err := n.v6.init(n.Hooks); err != nil {
		return err
	}

	log.Infof("nftables initiated")

	return nil
}

func (n *nft) Add(decision *models.Decision) error {
	n.decisionsToAdd = append(n.decisionsToAdd, decision)
	return nil
}

func (n *nft) getBannedState() (map[string]struct{}, error) {
	banned := make(map[string]struct{})
	if err := n.v4.setBanned(banned); err != nil {
		return nil, err
	}

	if err := n.v6.setBanned(banned); err != nil {
		return nil, err
	}

	return banned, nil
}

func (n *nft) reset() {
	n.decisionsToAdd = make([]*models.Decision, 0)
	n.decisionsToDelete = make([]*models.Decision, 0)
}

func (n *nft) commitDeletedDecisions() error {
	banned, err := n.getBannedState()
	if err != nil {
		return fmt.Errorf("failed to get current state: %w", err)
	}

	ip4 := []nftables.SetElement{}
	ip6 := []nftables.SetElement{}

	n.decisionsToDelete = normalizedDecisions(n.decisionsToDelete)

	for _, decision := range n.decisionsToDelete {
		ip := net.ParseIP(*decision.Value)
		if _, ok := banned[ip.String()]; !ok {
			log.Debugf("not deleting %s since it's not in the set", ip)
			continue
		}

		if strings.Contains(ip.String(), ":") {
			if n.v6.conn != nil {
				log.Tracef("adding %s to buffer", ip)

				ip6 = append(ip6, nftables.SetElement{Key: ip.To16()})
			}

			continue
		}

		if n.v4.conn != nil {
			log.Tracef("adding %s to buffer", ip)

			ip4 = append(ip4, nftables.SetElement{Key: ip.To4()})
		}
	}

	if len(ip4) > 0 {
		log.Debugf("removing %d ip%s elements from set", len(ip4), n.v4.version)

		if err := n.v4.deleteElements(ip4); err != nil {
			return err
		}
	}

	if len(ip6) > 0 {
		log.Debugf("removing %d ip%s elements from set", len(ip6), n.v6.version)

		if err := n.v6.deleteElements(ip6); err != nil {
			return err
		}
	}

	return nil
}

func (n *nft) createSetAndRuleForOrigin(ctx *nftContext, origin string) error {
	if _, ok := ctx.sets[origin]; !ok {
		// First time we see this origin, create the rule/set for all hooks
		set := &nftables.Set{
			Name:         fmt.Sprintf("%s-%s", ctx.blacklists, origin),
			Table:        ctx.table,
			KeyType:      ctx.typeIPAddr,
			KeyByteOrder: binaryutil.BigEndian,
			HasTimeout:   true,
		}

		ctx.sets[origin] = set

		if err := ctx.conn.AddSet(set, []nftables.SetElement{}); err != nil {
			return err
		}

		for _, chain := range ctx.chains {
			rule, err := ctx.createRule(chain, set, n.DenyLog, n.DenyLogPrefix, n.DenyAction)
			if err != nil {
				return err
			}

			ctx.conn.AddRule(rule)
			log.Infof("Created set and rule for origin %s and type %s in chain %s", origin, ctx.typeIPAddr.Name, chain.Name)
		}
	}

	return nil
}

func (n *nft) commitAddedDecisions() error {
	banned, err := n.getBannedState()
	if err != nil {
		return fmt.Errorf("failed to get current state: %w", err)
	}

	ip4 := make(map[string][]nftables.SetElement, 0)
	ip6 := make(map[string][]nftables.SetElement, 0)

	n.decisionsToAdd = normalizedDecisions(n.decisionsToAdd)

	for _, decision := range n.decisionsToAdd {
		ip := net.ParseIP(*decision.Value)
		if _, ok := banned[ip.String()]; ok {
			log.Debugf("not adding %s since it's already in the set", ip)
			continue
		}

		t, _ := time.ParseDuration(*decision.Duration)

		origin := *decision.Origin

		if origin == "lists" {
			origin = origin + "-" + *decision.Scenario
		}

		if strings.Contains(ip.String(), ":") {
			if n.v6.conn != nil {
				if n.v6.setOnly {
					origin = n.v6.blacklists
				}

				log.Tracef("adding %s to buffer", ip)

				if _, ok := ip6[origin]; !ok {
					ip6[origin] = make([]nftables.SetElement, 0)
				}

				ip6[origin] = append(ip6[origin], nftables.SetElement{Timeout: t, Key: ip.To16()})

				if !n.v6.setOnly {
					err := n.createSetAndRuleForOrigin(n.v6, origin)
					if err != nil {
						return err
					}
				}
			}

			continue
		}

		if n.v4.conn != nil {
			if n.v4.setOnly {
				origin = n.v4.blacklists
			}

			log.Tracef("adding %s to buffer", ip)

			if _, ok := ip4[origin]; !ok {
				ip4[origin] = make([]nftables.SetElement, 0)
			}

			ip4[origin] = append(ip4[origin], nftables.SetElement{Timeout: t, Key: ip.To4()})

			if !n.v4.setOnly {
				err := n.createSetAndRuleForOrigin(n.v4, origin)
				if err != nil {
					return err
				}
			}
		}
	}

	if err := n.v4.addElements(ip4); err != nil {
		return err
	}

	return n.v6.addElements(ip6)
}

func (n *nft) Commit() error {
	defer n.reset()

	if err := n.commitDeletedDecisions(); err != nil {
		return err
	}

	return n.commitAddedDecisions()
}

type tmpDecisions struct {
	duration time.Duration
	origin   string
	scenario string
}

// remove duplicates, normalize decision timeouts, keep the longest decision when dups are present.
func normalizedDecisions(decisions []*models.Decision) []*models.Decision {
	vals := make(map[string]tmpDecisions)
	finalDecisions := make([]*models.Decision, 0)

	for _, d := range decisions {
		t, err := time.ParseDuration(*d.Duration)
		if err != nil {
			t, _ = time.ParseDuration(defaultTimeout)
		}

		*d.Value = strings.Split(*d.Value, "/")[0]
		if longest, ok := vals[*d.Value]; !ok || t > longest.duration {
			vals[*d.Value] = tmpDecisions{
				duration: t,
				origin:   *d.Origin,
				scenario: *d.Scenario,
			}
		}
	}

	for ip, decision := range vals {
		d := decision.duration.String()
		i := ip // copy it because we don't same value for all decisions as `ip` is same pointer :)
		origin := decision.origin
		scenario := decision.scenario

		finalDecisions = append(finalDecisions, &models.Decision{
			Duration: &d,
			Value:    &i,
			Origin:   &origin,
			Scenario: &scenario,
		})
	}

	return finalDecisions
}

func (n *nft) Delete(decision *models.Decision) error {
	n.decisionsToDelete = append(n.decisionsToDelete, decision)
	return nil
}

func (n *nft) ShutDown() error {
	if err := n.v4.shutDown(); err != nil {
		return err
	}

	return n.v6.shutDown()
}


================================================
FILE: pkg/nftables/nftables_context.go
================================================
//go:build linux

package nftables

import (
	"fmt"
	"net"
	"strings"

	"github.com/google/nftables"
	"github.com/google/nftables/binaryutil"
	"github.com/google/nftables/expr"
	log "github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"

	"github.com/crowdsecurity/go-cs-lib/slicetools"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
)

var HookNameToHookID = map[string]nftables.ChainHook{
	"prerouting":  *nftables.ChainHookPrerouting,
	"input":       *nftables.ChainHookInput,
	"forward":     *nftables.ChainHookForward,
	"output":      *nftables.ChainHookOutput,
	"postrouting": *nftables.ChainHookPostrouting,
	"ingress":     *nftables.ChainHookIngress,
}

type nftContext struct {
	chains        map[string]*nftables.Chain
	conn          *nftables.Conn
	sets          map[string]*nftables.Set
	table         *nftables.Table
	tableFamily   nftables.TableFamily
	typeIPAddr    nftables.SetDatatype
	version       string
	payloadOffset uint32
	payloadLength uint32
	priority      int
	blacklists    string
	chainName     string
	tableName     string
	setOnly       bool
}

// convert a binary representation of an IP (4 or 16 bytes) to a string.
func reprIP(ip []byte) string {
	return net.IP(ip).String()
}

func NewNFTV4Context(config *cfg.BouncerConfig) *nftContext {
	if !*config.Nftables.Ipv4.Enabled {
		log.Debug("nftables: ipv4 disabled")

		return &nftContext{}
	}

	log.Debug("nftables: ipv4 enabled")

	ret := &nftContext{
		conn:          &nftables.Conn{},
		version:       "v4",
		tableFamily:   nftables.TableFamilyIPv4,
		typeIPAddr:    nftables.TypeIPAddr,
		payloadOffset: 12,
		payloadLength: 4,
		tableName:     config.Nftables.Ipv4.Table,
		chainName:     config.Nftables.Ipv4.Chain,
		blacklists:    config.BlacklistsIpv4,
		setOnly:       config.Nftables.Ipv4.SetOnly,
		priority:      config.Nftables.Ipv4.Priority,
		sets:          make(map[string]*nftables.Set),
	}

	log.Debugf("nftables: ipv4: %t, table: %s, chain: %s, blacklist: %s, set-only: %t",
		*config.Nftables.Ipv4.Enabled, ret.tableName, ret.chainName, ret.blacklists, ret.setOnly)

	return ret
}

func NewNFTV6Context(config *cfg.BouncerConfig) *nftContext {
	if !*config.Nftables.Ipv6.Enabled {
		log.Debug("nftables: ipv6 disabled")

		return &nftContext{}
	}

	log.Debug("nftables: ipv6 enabled")

	ret := &nftContext{
		conn:          &nftables.Conn{},
		version:       "v6",
		tableFamily:   nftables.TableFamilyIPv6,
		typeIPAddr:    nftables.TypeIP6Addr,
		payloadOffset: 8,
		payloadLength: 16,
		tableName:     config.Nftables.Ipv6.Table,
		chainName:     config.Nftables.Ipv6.Chain,
		blacklists:    config.BlacklistsIpv6,
		setOnly:       config.Nftables.Ipv6.SetOnly,
		priority:      config.Nftables.Ipv6.Priority,
		sets:          make(map[string]*nftables.Set),
	}

	log.Debugf("nftables: ipv6: %t, table6: %s, chain6: %s, blacklist: %s, set-only6: %t",
		*config.Nftables.Ipv6.Enabled, ret.tableName, ret.chainName, ret.blacklists, ret.setOnly)

	return ret
}

// setBanned retrieves the list of banned IPs from the nftables set and adds them to the banned map.
func (c *nftContext) setBanned(banned map[string]struct{}) error {
	if c.conn == nil {
		return nil
	}

	for _, set := range c.sets {
		elements, err := c.conn.GetSetElements(set)
		if err != nil {
			return err
		}

		for i := range elements {
			banned[net.IP(elements[i].Key).String()] = struct{}{}
		}
	}

	return nil
}

func (c *nftContext) initSetOnly() error {
	var err error

	// Use existing nftables configuration
	log.Debugf("nftables: ip%s set-only", c.version)

	c.table, err = c.lookupTable()
	if err != nil {
		return err
	}

	set, err := c.conn.GetSetByName(c.table, c.blacklists)
	if err != nil {
		log.Debugf("nftables: could not find ip%s blacklist '%s' in table '%s': creating...", c.version, c.blacklists, c.tableName)

		set = &nftables.Set{
			Name:         c.blacklists,
			Table:        c.table,
			KeyType:      c.typeIPAddr,
			KeyByteOrder: binaryutil.BigEndian,
			HasTimeout:   true,
		}

		if err := c.conn.AddSet(set, []nftables.SetElement{}); err != nil {
			return err
		}

		if err := c.conn.Flush(); err != nil {
			return err
		}
	}

	c.sets[c.blacklists] = set
	log.Debugf("nftables: ip%s set '%s' configured", c.version, c.blacklists)

	return nil
}

func (c *nftContext) initOwnTable(hooks []string) error {
	log.Debugf("nftables: ip%s own table", c.version)

	c.table = c.conn.AddTable(&nftables.Table{
		Family: c.tableFamily,
		Name:   c.tableName,
	})

	for _, hook := range hooks {
		hooknum := HookNameToHookID[hook]
		priority := nftables.ChainPriority(c.priority)
		chain := c.conn.AddChain(&nftables.Chain{
			Name:     c.chainName + "-" + hook,
			Table:    c.table,
			Type:     nftables.ChainTypeFilter,
			Hooknum:  &hooknum,
			Priority: &priority,
		})

		namedCounter := nftables.NamedObj{
			Table: c.table,
			Name:  "processed",
			Type:  nftables.ObjTypeCounter,
			Obj:   &expr.Counter{},
		}

		c.conn.AddObject(&namedCounter)

		// We flush here because we need to create a reference to the counter in the rule
		err := c.conn.Flush()
		if err != nil {
			return fmt.Errorf("nftables: failed to flush conn: %w", err)
		}

		r := &nftables.Rule{
			Table: c.table,
			Chain: chain,
			Exprs: []expr.Any{
				&expr.Objref{
					Type: int(nftables.ObjTypeCounter), // The nftables library does use the ObjType enum here, just cast it
					Name: namedCounter.Name,
				},
			},
		}

		c.conn.AddRule(r)

		c.chains[hook] = chain

		log.Debugf("nftables: ip%s chain '%s' created", c.version, chain.Name)
		// Rules and sets are created on the fly when we detect a new origin
	}

	if err := c.conn.Flush(); err != nil {
		return err
	}

	log.Debugf("nftables: ip%s table created", c.version)

	return nil
}

func (c *nftContext) init(hooks []string) error {
	if c.conn == nil {
		return nil
	}

	if c.chains == nil {
		c.chains = make(map[string]*nftables.Chain)
	}

	log.Debugf("nftables: ip%s init starting", c.version)

	var err error

	if c.setOnly {
		err = c.initSetOnly()
	} else {
		err = c.initOwnTable(hooks)
	}

	if err != nil && strings.Contains(err.Error(), "out of range") {
		return fmt.Errorf("nftables: %w. Please check the name length of tables, sets and chains. "+
			"Some legacy systems have 32 or 15 character limits. "+
			"For example, use 'crowdsec-set' instead of 'crowdsec-blacklists'", err)
	}

	return err
}

func (c *nftContext) lookupTable() (*nftables.Table, error) {
	tables, err := c.conn.ListTables()
	if err != nil {
		return nil, err
	}

	for _, t := range tables {
		if t.Name == c.tableName {
			return t, nil
		}
	}

	return nil, fmt.Errorf("nftables: could not find table '%s'", c.tableName)
}

func (c *nftContext) createRule(chain *nftables.Chain, set *nftables.Set,
	denyLog bool, denyLogPrefix string, denyAction string,
) (*nftables.Rule, error) {
	namedCounter := nftables.NamedObj{
		Table: c.table,
		Name:  set.Name,
		Type:  nftables.ObjTypeCounter,
		Obj:   &expr.Counter{},
	}

	c.conn.AddObject(&namedCounter)

	// We flush here because we need to create a reference to the counter in the rule
	if err := c.conn.Flush(); err != nil {
		return nil, fmt.Errorf("nftables: failed to flush conn: %w", err)
	}

	r := &nftables.Rule{
		Table:    c.table,
		Chain:    chain,
		Exprs:    []expr.Any{},
		UserData: []byte(set.Name),
	}
	// [ payload load 4b @ network header + 16 => reg 1 ]
	r.Exprs = append(r.Exprs, &expr.Payload{
		DestRegister: 1,
		Base:         expr.PayloadBaseNetworkHeader,
		Offset:       c.payloadOffset,
		Len:          c.payloadLength,
	})
	// [ lookup reg 1 set whitelist ]
	r.Exprs = append(r.Exprs, &expr.Lookup{
		SourceRegister: 1,
		SetName:        set.Name,
		SetID:          set.ID,
	})

	r.Exprs = append(r.Exprs, &expr.Objref{
		Type: int(nftables.ObjTypeCounter), // The nftables library does use the ObjType enum here, just cast it
		Name: namedCounter.Name,
	})

	if denyLog {
		r.Exprs = append(r.Exprs, &expr.Log{
			Key:  1 << unix.NFTA_LOG_PREFIX,
			Data: []byte(denyLogPrefix),
		})
	}

	action := strings.ToUpper(denyAction)
	if action == "" {
		action = "DROP"
	}

	switch action {
	case "DROP":
		r.Exprs = append(r.Exprs, &expr.Verdict{
			Kind: expr.VerdictDrop,
		})
	case "REJECT":
		r.Exprs = append(r.Exprs, &expr.Reject{
			Type: unix.NFT_REJECT_ICMP_UNREACH,
			Code: unix.NFT_REJECT_ICMPX_ADMIN_PROHIBITED,
		})
	default:
		return nil, fmt.Errorf("invalid deny_action '%s', must be one of DROP, REJECT", action)
	}

	log.Tracef("using '%s' as deny_action", action)

	return r, nil
}

func (c *nftContext) deleteElementChunk(els []nftables.SetElement) error {
	// FIXME: only delete IPs from the set they are in
	// But this could lead to strange behavior if we have duplicate decisions with different origins
	for _, set := range c.sets {
		log.Debugf("removing %d ip%s elements from set %s", len(els), c.version, set.Name)

		if err := c.conn.SetDeleteElements(set, els); err != nil {
			return fmt.Errorf("failed to remove ip%s elements from set: %w", c.version, err)
		}

		if err := c.conn.Flush(); err != nil {
			if len(els) == 1 {
				log.Debugf("deleting %s, failed to flush: %s", reprIP(els[0].Key), err)
				continue
			}

			log.Debugf("failed to flush chunk of %d elements, will retry each one: %s", len(els), err)

			for i := range els {
				if err := c.deleteElementChunk([]nftables.SetElement{els[i]}); err != nil {
					return err
				}
			}
		}
	}

	return nil
}

func (c *nftContext) deleteElements(els []nftables.SetElement) error {
	if len(els) <= chunkSize {
		return c.deleteElementChunk(els)
	}

	log.Debugf("splitting %d elements into chunks of %d", len(els), chunkSize)

	for _, chunk := range slicetools.Chunks(els, chunkSize) {
		if err := c.deleteElementChunk(chunk); err != nil {
			return err
		}
	}

	return nil
}

func (c *nftContext) addElements(els map[string][]nftables.SetElement) error {
	var setName string

	for origin, set := range c.sets {
		if c.setOnly {
			setName = c.blacklists
		} else {
			setName = fmt.Sprintf("%s-%s", c.blacklists, origin)
		}

		log.Debugf("Using %s as origin | len of IPs: %d | set name is %s", origin, len(els[origin]), setName)

		for _, chunk := range slicetools.Chunks(els[origin], chunkSize) {
			log.Debugf("adding %d ip%s elements to set %s", len(chunk), c.version, setName)

			if err := c.conn.SetAddElements(set, chunk); err != nil {
				return fmt.Errorf("failed to add ip%s elements to set: %w", c.version, err)
			}

			if err := c.conn.Flush(); err != nil {
				return fmt.Errorf("failed to flush ip%s conn: %w", c.version, err)
			}
		}
	}

	return nil
}

func (c *nftContext) shutDown() error {
	if c.conn == nil {
		return nil
	}

	if c.setOnly {
		// Flush blacklist4 set empty
		log.Infof("flushing '%s' set in '%s' table", c.sets[c.blacklists].Name, c.table.Name)
		c.conn.FlushSet(c.sets[c.blacklists])
	} else {
		log.Infof("removing '%s' table", c.table.Name)
		c.conn.DelTable(c.table)
	}

	return c.conn.Flush()
}


================================================
FILE: pkg/nftables/nftables_stub.go
================================================
//go:build !linux

package nftables

import (
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/types"
)

func NewNFTables(config *cfg.BouncerConfig) (types.Backend, error) {
	return nil, nil
}


================================================
FILE: pkg/pf/metrics.go
================================================
package pf

import (
	"bufio"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"github.com/prometheus/client_golang/prometheus"
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/metrics"
)

type counter struct {
	packets uint64
	bytes   uint64
}

var (
	// table names can contain _ or - characters.
	rexpTable   = regexp.MustCompile(`^block .* from <(?P<table>[^ ]+)> .*"$`)
	rexpMetrics = regexp.MustCompile(`^\s+\[.*Packets: (?P<packets>\d+)\s+Bytes: (?P<bytes>\d+).*\]$`)
)

func parseMetrics(reader *strings.Reader, tables []string) map[string]counter {
	ret := make(map[string]counter)

	// scan until we find a table name between <>
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		line := scanner.Text()
		// parse the line and extract the table name
		match := rexpTable.FindStringSubmatch(line)
		if len(match) == 0 {
			continue
		}

		table := match[1]
		// if the table is not in the list of tables we want to parse, skip it
		if !slices.Contains(tables, table) {
			continue
		}

		// parse the line with the actual metrics
		if !scanner.Scan() {
			break
		}

		line = scanner.Text()

		match = rexpMetrics.FindStringSubmatch(line)
		if len(match) == 0 {
			log.Errorf("failed to parse metrics: %s", line)
			continue
		}

		packets, err := strconv.ParseUint(match[1], 10, 64)
		if err != nil {
			log.Errorf("failed to parse metrics - dropped packets: %s", err)

			packets = 0
		}

		bytes, err := strconv.ParseUint(match[2], 10, 64)
		if err != nil {
			log.Errorf("failed to parse metrics - dropped bytes: %s", err)

			bytes = 0
		}

		ret[table] = counter{
			packets: packets,
			bytes:   bytes,
		}
	}

	return ret
}

// countIPs returns the number of IPs in a table.
func countIPs(table string) int {
	cmd := execPfctl("", "-T", "show", "-t", table)

	out, err := cmd.Output()
	if err != nil {
		log.Errorf("failed to run 'pfctl -T show -t %s': %s", table, err)
		return 0
	}

	// one IP per line
	return strings.Count(string(out), "\n")
}

// CollectMetrics collects metrics from pfctl.
// In pf mode the firewall rules are not controlled by the bouncer, so we can only
// trust they are set up correctly, and retrieve stats from the pfctl tables.
func (pf *pf) CollectMetrics() {
	tables := []string{}

	if pf.inet != nil {
		tables = append(tables, pf.inet.table)
	}

	if pf.inet6 != nil {
		tables = append(tables, pf.inet6.table)
	}

	cmd := execPfctl("", "-v", "-sr")

	out, err := cmd.Output()
	if err != nil {
		log.Errorf("failed to run 'pfctl -v -sr': %s", err)
		return
	}

	reader := strings.NewReader(string(out))
	stats := parseMetrics(reader, tables)

	for _, table := range tables {
		st, ok := stats[table]
		if !ok {
			continue
		}

		droppedPackets := float64(st.packets)
		droppedBytes := float64(st.bytes)
		bannedIPs := countIPs(table)

		if pf.inet != nil && table == pf.inet.table {
			metrics.Map[metrics.DroppedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv4", "origin": ""}).Set(droppedPackets)
			metrics.Map[metrics.DroppedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv4", "origin": ""}).Set(droppedBytes)
			metrics.Map[metrics.ActiveBannedIPs].Gauge.With(prometheus.Labels{"ip_type": "ipv4", "origin": ""}).Set(float64(bannedIPs))
		} else if pf.inet6 != nil && table == pf.inet6.table {
			metrics.Map[metrics.DroppedPackets].Gauge.With(prometheus.Labels{"ip_type": "ipv6", "origin": ""}).Set(droppedPackets)
			metrics.Map[metrics.DroppedBytes].Gauge.With(prometheus.Labels{"ip_type": "ipv6", "origin": ""}).Set(droppedBytes)
			metrics.Map[metrics.ActiveBannedIPs].Gauge.With(prometheus.Labels{"ip_type": "ipv6", "origin": ""}).Set(float64(bannedIPs))
		}
	}
}


================================================
FILE: pkg/pf/metrics_test.go
================================================
package pf

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestParseMetrics(t *testing.T) {
	metricsInput := `block drop in quick inet from <crowdsec_blacklists> to any label "CrowdSec IPv4"
  [ Evaluations: 1519      Packets: 16         Bytes: 4096           States: 0     ]
  [ Inserted: uid 0 pid 14219 State Creations: 0     ]
block drop in quick inet6 from <crowdsec6_blacklists> to any label "CrowdSec IPv6"
  [ Evaluations: 914       Packets: 8          Bytes: 2048           States: 0     ]
  [ Inserted: uid 0 pid 14219 State Creations: 0     ]`

	reader := strings.NewReader(metricsInput)
	tables := []string{"crowdsec_blacklists", "crowdsec6_blacklists"}

	metrics := parseMetrics(reader, tables)

	require.Contains(t, metrics, "crowdsec_blacklists")
	require.Contains(t, metrics, "crowdsec6_blacklists")

	ip4Metrics := metrics["crowdsec_blacklists"]
	assert.Equal(t, uint64(16), ip4Metrics.packets)
	assert.Equal(t, uint64(4096), ip4Metrics.bytes)

	ip6Metrics := metrics["crowdsec6_blacklists"]
	assert.Equal(t, uint64(8), ip6Metrics.packets)
	assert.Equal(t, uint64(2048), ip6Metrics.bytes)
}


================================================
FILE: pkg/pf/pf.go
================================================
package pf

import (
	"fmt"
	"os"
	"os/exec"
	"strings"

	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/models"

	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
	"github.com/crowdsecurity/cs-firewall-bouncer/pkg/types"
)

type pf struct {
	inet              *pfContext
	inet6             *pfContext
	decisionsToAdd    []*models.Decision
	decisionsToDelete []*models.Decision
}

const (
	pfctlCmd = "/sbin/pfctl"
	pfDevice = "/dev/pf"
)

func NewPF(config *cfg.BouncerConfig) (types.Backend, error) {
	ret := &pf{}

	inetCtx := &pfContext{
		table:   config.BlacklistsIpv4,
		proto:   "inet",
		anchor:  config.PF.AnchorName,
		version: "ipv4",
	}

	inet6Ctx := &pfContext{
		table:   config.BlacklistsIpv6,
		proto:   "inet6",
		anchor:  config.PF.AnchorName,
		version: "ipv6",
	}

	if !config.DisableIPV4 {
		ret.inet = inetCtx
	}

	if !config.DisableIPV6 {
		ret.inet6 = inet6Ctx
	}

	return ret, nil
}

// execPfctl runs a pfctl command by prepending the anchor name if needed.
// Some commands return an error if an anchor is specified.
func execPfctl(anchor string, arg ...string) *exec.Cmd {
	if anchor != "" {
		arg = append([]string{"-a", anchor}, arg...)
	}

	log.Debugf("Running: %s %s", pfctlCmd, arg)

	return exec.Command(pfctlCmd, arg...)
}

func (pf *pf) Init() error {
	if _, err := os.Stat(pfDevice); err != nil {
		return fmt.Errorf("%s device not found: %w", pfDevice, err)
	}

	if _, err := exec.LookPath(pfctlCmd); err != nil {
		return fmt.Errorf("%s command not found: %w", pfctlCmd, err)
	}

	if pf.inet != nil {
		if err := pf.inet.init(); err != nil {
			return err
		}
	}

	if pf.inet6 != nil {
		if err := pf.inet6.init(); err != nil {
			return err
		}
	}

	return nil
}

func (pf *pf) Commit() error {
	defer pf.reset()

	if err := pf.commitDeletedDecisions(); err != nil {
		return err
	}

	return pf.commitAddedDecisions()
}

func (pf *pf) Add(decision *models.Decision) error {
	pf.decisionsToAdd = append(pf.decisionsToAdd, decision)
	return nil
}

func (pf *pf) reset() {
	pf.decisionsToAdd = make([]*models.Decision, 0)
	pf.decisionsToDelete = make([]*models.Decision, 0)
}

func (pf *pf) commitDeletedDecisions() error {
	ipv4decisions := make([]*models.Decision, 0)
	ipv6decisions := make([]*models.Decision, 0)

	for _, d := range pf.decisionsToDelete {
		if strings.Contains(*d.Value, ":") && pf.inet6 != nil {
			ipv6decisions = append(ipv6decisions, d)
		} else if pf.inet != nil {
			ipv4decisions = append(ipv4decisions, d)
		}
	}

	if len(ipv6decisions) > 0 {
		if pf.inet6 == nil {
			log.Debugf("not removing '%d' decisions because ipv6 is disabled", len(ipv6decisions))
		} else if err := pf.inet6.delete(ipv6decisions); err != nil {
			return err
		}
	}

	if len(ipv4decisions) > 0 {
		if pf.inet == nil {
			log.Debugf("not removing '%d' decisions because ipv4 is disabled", len(ipv4decisions))
		} else if err := pf.inet.delete(ipv4decisions); err != nil {
			return err
		}
	}

	return nil
}

func (pf *pf) commitAddedDecisions() error {
	ipv4decisions := make([]*models.Decision, 0)
	ipv6decisions := make([]*models.Decision, 0)

	for _, d := range pf.decisionsToAdd {
		if strings.Contains(*d.Value, ":") && pf.inet6 != nil {
			ipv6decisions = append(ipv6decisions, d)
		} else if pf.inet != nil {
			ipv4decisions = append(ipv4decisions, d)
		}
	}

	if len(ipv6decisions) > 0 {
		if pf.inet6 == nil {
			log.Debugf("not adding '%d' decisions because ipv6 is disabled", len(ipv6decisions))
		} else if err := pf.inet6.add(ipv6decisions); err != nil {
			return err
		}
	}

	if len(ipv4decisions) > 0 {
		if pf.inet == nil {
			log.Debugf("not adding '%d' decisions because ipv4 is disabled", len(ipv4decisions))
		} else if err := pf.inet.add(ipv4decisions); err != nil {
			return err
		}
	}

	return nil
}

func (pf *pf) Delete(decision *models.Decision) error {
	pf.decisionsToDelete = append(pf.decisionsToDelete, decision)
	return nil
}

func (pf *pf) ShutDown() error {
	log.Infof("flushing 'crowdsec' table(s)")

	if pf.inet != nil {
		if err := pf.inet.shutDown(); err != nil {
			return fmt.Errorf("unable to flush %s table (%s): ", pf.inet.version, pf.inet.table)
		}
	}

	if pf.inet6 != nil {
		if err := pf.inet6.shutDown(); err != nil {
			return fmt.Errorf("unable to flush %s table (%s): ", pf.inet6.version, pf.inet6.table)
		}
	}

	return nil
}


================================================
FILE: pkg/pf/pf_context.go
================================================
package pf

import (
	"bufio"
	"fmt"
	"maps"
	"os"
	"os/exec"
	"slices"
	"strings"

	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/crowdsec/pkg/models"
)

type pfContext struct {
	proto   string
	anchor  string
	table   string
	version string
}

const backendName = "pf"

func decisionsToIPs(decisions []*models.Decision) []string {
	ips := make([]string, 0, len(decisions))

	for i, d := range decisions {
		if d == nil || d.Value == nil {
			continue
		}

		ips[i] = *d.Value
	}

	return ips
}

func writeIPsToFile(ips []string) (string, error) {
	f, err := os.CreateTemp("", "crowdsec-ips-*.txt")
	if err != nil {
		return "", err
	}

	name := f.Name()
	done := false

	defer func() {
		if !done {
			_ = f.Close()
			_ = os.Remove(name)
		}
	}()

	w := bufio.NewWriter(f)
	for _, ip := range ips {
		if _, err = w.WriteString(ip + "\n"); err != nil {
			return "", err
		}
	}

	if err = w.Flush(); err != nil {
		return "", err
	}

	if err = f.Close(); err != nil {
		return "", err
	}

	done = true

	return name, nil
}

func (ctx *pfContext) checkTable() error {
	log.Infof("Checking pf table: %s", ctx.table)

	cmd := execPfctl(ctx.anchor, "-s", "Tables")

	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("pfctl error: %s - %w", out, err)
	}

	if !strings.Contains(string(out), ctx.table) {
		if ctx.anchor != "" {
			return fmt.Errorf("table %s in anchor %s doesn't exist", ctx.table, ctx.anchor)
		}

		return fmt.Errorf("table %s doesn't exist", ctx.table)
	}

	return nil
}

func (ctx *pfContext) shutDown() error {
	cmd := execPfctl(ctx.anchor, "-t", ctx.table, "-T", "flush")
	log.Infof("pf table clean-up: %s", cmd)

	if out, err := cmd.CombinedOutput(); err != nil {
		log.Errorf("Error while flushing table (%s): %v --> %s", cmd, err, out)
	}

	return nil
}

// getStateIPs returns a list of IPs that are currently in the state table.
func getStateIPs() (map[string]bool, error) {
	ret := make(map[string]bool)

	cmd := exec.Command(pfctlCmd, "-s", "states")

	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	scanner := bufio.NewScanner(strings.NewReader(string(out)))
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) < 6 {
			continue
		}

		// don't bother to parse the direction, we'll block both anyway

		// right side
		ip := fields[4]
		if strings.Contains(ip, ":") {
			ip = strings.Split(ip, ":")[0]
		}

		ret[ip] = true

		// left side
		ip = fields[2]
		if strings.Contains(ip, ":") {
			ip = strings.Split(ip, ":")[0]
		}

		ret[ip] = true
	}

	log.Debugf("Found IPs in state table: %v", len(ret))

	return ret, nil
}

func (ctx *pfContext) add(decisions []*models.Decision) error {
	log.Debugf("Adding %d decisions", len(decisions))

	ips := decisionsToIPs(decisions)

	file, err := writeIPsToFile(ips)
	if err != nil {
		return fmt.Errorf("writing decisions to temp file: %w", err)
	}
	defer os.Remove(file)

	cmd := execPfctl(ctx.anchor, "-t", ctx.table, "-T", "add", "-f", file)
	if out, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("error while adding to table (%s): %w --> %s", cmd, err, out)
	}

	bannedIPs := make(map[string]bool, len(ips))
	for _, ip := range ips {
		bannedIPs[ip] = true
	}

	if len(bannedIPs) == 0 {
		log.Debugf("No new banned IPs")
		return nil
	}

	if log.IsLevelEnabled(log.DebugLevel) {
		keys := slices.Collect(maps.Keys(bannedIPs))
		slices.Sort(keys)
		log.Debugf("New banned IPs: %v", keys)
	}

	stateIPs, err := getStateIPs()
	if err != nil {
		return fmt.Errorf("error while getting state IPs: %w", err)
	}

	// Reset the states of connections coming from or going to an IP if it's both in stateIPs and bannedIPs

	for ip := range bannedIPs {
		if stateIPs[ip] {
			// incoming
			cmd := execPfctl("", "-k", ip)
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Errorf("Error while flushing state (%s): %v --> %s", cmd, err, out)
			}

			// outgoing
			cmd = execPfctl("", "-k", "0.0.0.0/0", "-k", ip)
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Errorf("Error while flushing state (%s): %v --> %s", cmd, err, out)
			}
		}
	}

	return nil
}

func (ctx *pfContext) delete(decisions []*models.Decision) error {
	log.Debugf("Removing %d decisions", len(decisions))

	ips := decisionsToIPs(decisions)

	file, err := writeIPsToFile(ips)
	if err != nil {
		return fmt.Errorf("writing decisions to temp file: %w", err)
	}
	defer os.Remove(file)

	cmd := execPfctl(ctx.anchor, "-t", ctx.table, "-T", "delete", "-f", file)
	if out, err := cmd.CombinedOutput(); err != nil {
		log.Infof("Error while deleting from table (%s): %v --> %s", cmd, err, out)
	}

	return nil
}

func (ctx *pfContext) init() error {
	if err := ctx.shutDown(); err != nil {
		return fmt.Errorf("pf table flush failed: %w", err)
	}

	if err := ctx.checkTable(); err != nil {
		return fmt.Errorf("pf init failed: %w", err)
	}

	log.Infof("%s initiated for %s", backendName, ctx.version)

	return nil
}


================================================
FILE: pkg/types/types.go
================================================
package types

import (
	"github.com/crowdsecurity/crowdsec/pkg/models"
)

type Backend interface {
	Init() error
	ShutDown() error
	Add(decision *models.Decision) error
	Delete(decision *models.Decision) error
	Commit() error
	CollectMetrics()
}


================================================
FILE: rpm/SOURCES/80-crowdsec-firewall-bouncer.preset
================================================
# This file is part of crowdsec-firewall-bouncer

enable crowdsec-firewall-bouncer.service


================================================
FILE: rpm/SPECS/crowdsec-firewall-bouncer.spec
================================================
Name:      crowdsec-firewall-bouncer-iptables
Version:   %(echo $VERSION)
Release:   %(echo $PACKAGE_NUMBER)%{?dist}
Summary:   Firewall bouncer for Crowdsec (iptables+ipset configuration)

License:   MIT
URL:       https://crowdsec.net
Source0:   https://github.com/crowdsecurity/%{name}/archive/v%(echo $VERSION).tar.gz
Source1:   80-crowdsec-firewall-bouncer.preset
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)

BuildRequires: make
%{?fc33:BuildRequires: systemd-rpm-macros}

Requires: gettext,iptables,ipset,ipset-libs

%define debug_package %{nil}

%define version_number %(echo $VERSION)
%define releasever %(echo $RELEASEVER)
%global local_version v%{version_number}-%{releasever}-rpm
%global name crowdsec-firewall-bouncer
%global __mangle_shebangs_exclude_from /usr/bin/env

%prep
%setup -q -T -b 0 -n %{name}-%{version_number}

%build
BUILD_VERSION=%{local_version} make

%install
rm -rf %{buildroot}

install -m 755 -D %{name} %{buildroot}%{_bindir}/%{name}
install -m 600 -D config/%{name}.yaml %{buildroot}/etc/crowdsec/bouncers/%{name}.yaml
install -m 600 -D scripts/_bouncer.sh %{buildroot}/usr/lib/%{name}/_bouncer.sh

mkdir -p %{buildroot}%{_unitdir}
BIN=%{_bindir}/%{name} CFG=/etc/crowdsec/bouncers envsubst '$BIN $CFG' < config/%{name}.service > %{buildroot}%{_unitdir}/%{name}.service

install -D -m 644 %{SOURCE1} %{buildroot}%{_presetdir}/80-crowdsec-firewall-bouncer.preset

%clean
rm -rf %{buildroot}

%changelog
* Tue Feb 16 2021 Manuel Sabban <manuel@crowdsec.net>
- First initial packaging

# ------------------------------------
# iptables
# ------------------------------------

%description -n %{name}-iptables

%files -n %{name}-iptables
%defattr(-,root,root,-)
%{_bindir}/%{name}
/usr/lib/%{name}/_bouncer.sh
%{_unitdir}/%{name}.service
%config(noreplace) /etc/crowdsec/bouncers/%{name}.yaml
%config(noreplace) %{_presetdir}/80-crowdsec-firewall-bouncer.preset

%post -n %{name}-iptables
systemctl daemon-reload

. /usr/lib/%{name}/_bouncer.sh
START=1

if grep -q '${BACKEND}' "$CONFIG"; then
  newconfig=$(BACKEND="iptables" envsubst '$BACKEND' < "$CONFIG")
  (umask 177 && echo "$newconfig" > "$CONFIG")
fi

if [ "$1" = "1" ]; then
    if need_api_key; then
        if ! set_api_key; then
            START=0
        fi
    fi
fi

set_local_port

if [ ! -e /usr/sbin/crowdsec-firewall-bouncer ]; then
    if [ ! -L /usr/sbin ]; then
        ln -s ../bin/crowdsec-firewall-bouncer /usr/sbin/crowdsec-firewall-bouncer
    fi
fi


%systemd_post %{name}.service

if [ "$START" -eq 0 ]; then
    echo "no api key was generated, you can generate one on your LAPI Server by running 'cscli bouncers add <bouncer_name>' and add it to '$CONFIG'" >&2
else
    %if 0%{?fc35}
    systemctl enable "$SERVICE"
    %endif
    systemctl start "$SERVICE"
fi

echo "$BOUNCER has been successfully installed"

%preun -n %{name}-iptables
. /usr/lib/%{name}/_bouncer.sh

if [ "$1" = "0" ]; then
    systemctl stop "$SERVICE" || echo "cannot stop service"
    systemctl disable "$SERVICE" || echo "cannot disable service"
    delete_bouncer
fi

%postun -n %{name}-iptables
if [ "$1" = "1" ]; then
    systemctl restart %{name} || echo "cannot restart service"
fi

if [ -L /usr/sbin/crowdsec-firewall-bouncer ]; then
    rm -f /usr/sbin/crowdsec-firewall-bouncer
fi


# ------------------------------------
# nftables
# ------------------------------------

%package -n %{name}-nftables
Summary:  Firewall bouncer for Crowdsec (nftables configuration)
Requires: nftables,gettext

%description -n %{name}-nftables

%files -n %{name}-nftables
%defattr(-,root,root,-)
%{_bindir}/%{name}
/usr/lib/%{name}/_bouncer.sh
%{_unitdir}/%{name}.service
%config(noreplace) /etc/crowdsec/bouncers/%{name}.yaml
%config(noreplace) %{_presetdir}/80-crowdsec-firewall-bouncer.preset

%post -n %{name}-nftables
systemctl daemon-reload

. /usr/lib/%{name}/_bouncer.sh
START=1

if grep -q '${BACKEND}' "$CONFIG"; then
  newconfig=$(BACKEND="nftables" envsubst '$BACKEND' < "$CONFIG")
  (umask 177 && echo "$newconfig" > "$CONFIG")
fi

if [ "$1" = "1" ]; then
    if need_api_key; then
        if ! set_api_key; then
            START=0
        fi
    fi
fi

set_local_port

if [ ! -e /usr/sbin/crowdsec-firewall-bouncer ]; then
    if [ ! -L /usr/sbin ]; then
        ln -s ../bin/crowdsec-firewall-bouncer /usr/sbin/crowdsec-firewall-bouncer
    fi
fi

%systemd_post %{name}.service

if [ "$START" -eq 0 ]; then
    echo "no api key was generated, you can generate one on your LAPI Server by running 'cscli bouncers add <bouncer_name>' and add it to '$CONFIG'" >&2
else
    %if 0%{?fc35}
    systemctl enable "$SERVICE"
    %endif
    systemctl start "$SERVICE"
fi

echo "$BOUNCER has been successfully installed"

%preun -n %{name}-nftables
. /usr/lib/%{name}/_bouncer.sh

if [ "$1" = "0" ]; then
    systemctl stop "$SERVICE" || echo "cannot stop service"
    systemctl disable "$SERVICE" || echo "cannot disable service"
    delete_bouncer
fi

%postun -n %{name}-nftables
if [ "$1" = "1" ]; then
    systemctl restart %{name} || echo "cannot restart service"
fi

if [ -L /usr/sbin/crowdsec-firewall-bouncer ]; then
    rm -f /usr/sbin/crowdsec-firewall-bouncer
fi


================================================
FILE: scripts/_bouncer.sh
================================================
#!/bin/sh
#shellcheck disable=SC3043

set -eu

BOUNCER="crowdsec-firewall-bouncer"
BOUNCER_PREFIX=$(echo "$BOUNCER" | sed 's/crowdsec-/cs-/g')

# This is a library of functions that can be sourced by other scripts
# to install and configure bouncers.
#
# While not requiring bash, it is not strictly POSIX-compliant because
# it uses local variables, but it should work with every modern shell.
#
# Since passing/parsing arguments in posix sh is tricky, we share
# some environment variables with the functions. It's a matter of
# readability balance between shorter vs cleaner code.

if [ -n "${NO_COLOR-}" ] || [ ! -t 1 ]; then
    # terminal is not interactive; no colors
    FG_RED=""
    FG_GREEN=""
    FG_YELLOW=""
    FG_CYAN=""
    RESET=""
elif [ -n "${TERM-}" ] && tput sgr0 >/dev/null 2>&1; then
    # terminfo
    FG_RED=$(tput setaf 1)
    FG_GREEN=$(tput setaf 2)
    FG_YELLOW=$(tput setaf 3)
    FG_CYAN=$(tput setaf 6)
    RESET=$(tput sgr0)
else
    FG_RED=$(printf '%b' '\033[31m')
    FG_GREEN=$(printf '%b' '\033[32m')
    FG_YELLOW=$(printf '%b' '\033[33m')
    FG_CYAN=$(printf '%b' '\033[36m')
    RESET=$(printf '%b' '\033[0m')
fi

msg() {
    case "$1" in
        info) echo "${FG_CYAN}$2${RESET}" >&2 ;;
        warn) echo "${FG_YELLOW}WARN:${RESET} $2" >&2 ;;
        err) echo "${FG_RED}ERR:${RESET} $2" >&2 ;;
        succ) echo "${FG_GREEN}$2${RESET}" >&2 ;;
        *) echo "$1" >&2 ;;
    esac
}

require() {
    set | grep -q "^$1=" || { msg err "missing required variable \$$1"; exit 1; }
    shift
    [ "$#" -eq 0 ] || require "$@"
}

# shellcheck disable=SC2034
{
SERVICE="$BOUNCER.service"
BIN_PATH_INSTALLED="/usr/local/bin/$BOUNCER"
BIN_PATH="./$BOUNCER"
CONFIG_DIR="/etc/crowdsec/bouncers"
CONFIG_FILE="$BOUNCER.yaml"
CONFIG="$CONFIG_DIR/$CONFIG_FILE"
SYSTEMD_PATH_FILE="/etc/systemd/system/$SERVICE"
}

assert_root() {
    #shellcheck disable=SC2312
    if [ "$(id -u)" -ne 0 ]; then
        msg err "This script must be run as root"
        exit 1
    fi
}

# Check if the configuration file contains a variable
# which has not yet been interpolated, like "$API_KEY",
# and return true if it does.
config_not_set() {
    require 'CONFIG'
    local varname before after

    varname=$1
    if [ "$varname" = "" ]; then
        msg err "missing required variable name"
        exit 1
    fi

    before=$("$BOUNCER" -c "$CONFIG" -T)
    # shellcheck disable=SC2016
    after=$(echo "$before" | envsubst "\$$varname")

    if [ "$before" = "$after" ]; then
        return 1
    fi
    return 0
}

need_api_key() {
    if config_not_set 'API_KEY'; then
        return 0
    fi
    return 1
}

# Interpolate a variable in the config file with a value.
set_config_var_value() {
    require 'CONFIG'
    local varname value before

    varname=$1
    if [ "$varname" = "" ]; then
        msg err "missing required variable name"
        exit 1
    fi

    value=$2
    if [ "$value" = "" ]; then
        msg err "missing required variable value"
        exit 1
    fi

    before=$(cat "$CONFIG")
    (umask 177 && echo "$before" | \
        env "$varname=$value" envsubst "\$$varname" >"$CONFIG")
}

set_api_key() {
    require 'CONFIG' 'BOUNCER_PREFIX'
    local api_key ret bouncer_id before
    # if we can't set the key, the user will take care of it
    ret=0

    if command -v cscli >/dev/null; then
        echo "cscli/crowdsec is present, generating API key" >&2
        bouncer_id="$BOUNCER_PREFIX-$(date +%s)"
        api_key=$(cscli -oraw bouncers add "$bouncer_id" || true)
        if [ "$api_key" = "" ]; then
            echo "failed to create API key" >&2
            api_key="<API_KEY>"
            ret=1
        else
            echo "API Key successfully created" >&2
            echo "$bouncer_id" > "$CONFIG.id"
        fi
    else
        echo "cscli/crowdsec is not present, please set the API key manually" >&2
        api_key="<API_KEY>"
        ret=1
    fi

    if [ "$api_key" != "" ]; then
        set_config_var_value 'API_KEY' "$api_key"
    fi

    return "$ret"
}

set_local_port() {
    require 'CONFIG'
    local port
    command -v cscli >/dev/null || return 0
    # the following will fail with a non-LAPI local crowdsec, leaving empty port
    port=$(cscli config show -oraw --key "Config.API.Server.ListenURI" 2>/dev/null | cut -d ":" -f2 || true)
    if [ "$port" != "" ]; then
        sed -i "s/localhost:8080/127.0.0.1:$port/g" "$CONFIG"
        sed -i "s/127.0.0.1:8080/127.0.0.1:$port/g" "$CONFIG"
    fi
}

set_local_lapi_url() {
    require 'CONFIG'
    local port before varname
    # $varname is the name of the variable to interpolate
    # in the config file with the URL of the LAPI server,
    # assuming it is running on the same host as the
    # bouncer.
    varname=$1
    if [ "$varname" = "" ]; then
        msg err "missing required variable name"
        exit 1
    fi
    command -v cscli >/dev/null || return 0

    port=$(cscli config show -oraw --key "Config.API.Server.ListenURI" 2>/dev/null | cut -d ":" -f2 || true)
    if [ "$port" = "" ]; then
        port=8080
    fi

    set_config_var_value "$varname" "http://127.0.0.1:$port"
}

delete_bouncer() {
    require 'CONFIG'
    local bouncer_id
    if [ -f "$CONFIG.id" ]; then
        bouncer_id=$(cat "$CONFIG.id")
        cscli -oraw bouncers delete "$bouncer_id" 2>/dev/null || true
        rm -f "$CONFIG.id"
    fi
}

upgrade_bin() {
    require 'BIN_PATH' 'BIN_PATH_INSTALLED'
    rm "$BIN_PATH_INSTALLED"
    install -v -m 0755 -D "$BIN_PATH" "$BIN_PATH_INSTALLED"
}


================================================
FILE: scripts/install.sh
================================================
#!/bin/sh

set -eu

. ./scripts/_bouncer.sh

assert_root

# --------------------------------- #

API_KEY="<API_KEY>"

install_pkg() {
    pkg="$1"
    if [ -f /etc/redhat-release ]; then
        yum install -y "$pkg"
    elif grep -q "Amazon Linux release 2 (Karoo)" /etc/system-release 2>/dev/null; then
        yum install -y "$pkg"
    elif grep -q "suse" /etc/os-release 2>/dev/null; then
        zypper install -y "$pkg"
    elif [ -f /etc/debian_version ]; then
        apt install -y "$pkg"
    else
        msg warn "This distribution is not supported"
        return 1
    fi
    msg succ "$pkg successfully installed"
    return 0
}

check_firewall() {
    # Default firewall backend is nftables
    FW_BACKEND="nftables"

    iptables="true"
    if command -v iptables >/dev/null; then 
        FW_BACKEND="iptables"
        msg info "iptables found"
    else
        msg warn "iptables not found"
        iptables="false"
    fi

    nftables="true"
    if command -v nft >/dev/null; then 
        FW_BACKEND="nftables"
        msg info "nftables found"
    else
        msg warn "nftables not found"
        nftables="false"
    fi

    if [ "$nftables" = "false" ] && [ "$iptables" = "false" ]; then
        printf '%s ' "No firewall found, do you want to install nftables (Y/n) ?"
        read -r answer
        if echo "$answer" | grep -iq '^n'; then
            msg err "unable to continue without nftables. Please install nftables or iptables to use this bouncer."
            exit 1
        fi
        # shellcheck disable=SC2310
        install_pkg nftables || ( msg err "Cannot install nftables, please install it manually"; exit 1 )
    fi

    if [ "$nftables" = "true" ] && [ "$iptables" = "true" ]; then
        printf '%s ' "Found nftables (default) and iptables, which firewall do you want to use (nftables/iptables) ?"
        read -r answer
        if [ "$answer" = "iptables" ]; then
            FW_BACKEND="iptables"
        fi
    fi

    if [ "$FW_BACKEND" = "iptables" ]; then
        check_ipset
    fi
}

check_ipset() {
    if ! command -v ipset >/dev/null; then
        printf '%s ' "ipset not found, do you want to install it (Y/n) ?"
        read -r answer
        if echo "$answer" | grep -iq '^n'; then
            msg err "unable to continue without ipset. Exiting"
            exit 1
        fi
        # shellcheck disable=SC2310
        install_pkg ipset || ( msg err "Cannot install ipset, please install it manually"; exit 1 )
    fi
}

gen_apikey() {
    if command -v cscli >/dev/null; then
        msg succ "cscli found, generating bouncer api key."
        bouncer_id="$BOUNCER_PREFIX-$(date +%s)"
        API_KEY=$(cscli -oraw bouncers add "$bouncer_id")
        echo "$bouncer_id" > "$CONFIG.id"
        msg info "API Key: $API_KEY"
        READY="yes"
    else
        msg warn "cscli not found, you will need to generate an api key."
        READY="no"
    fi
}

gen_config_file() {
    # shellcheck disable=SC2016
    (umask 177 && API_KEY="$API_KEY" BACKEND="$FW_BACKEND" envsubst '$API_KEY $BACKEND' <"./config/$CONFIG_FILE" > "$CONFIG")
}

install_bouncer() {
    if [ ! -f "$BIN_PATH" ]; then
        msg err "$BIN_PATH not found, exiting."
        exit 1
    fi
    if [ -e "$BIN_PATH_INSTALLED" ]; then
        msg err "$BIN_PATH_INSTALLED is already installed. Exiting"
        exit 1
    fi
    msg info "Installing $BOUNCER"
    check_firewall
    install -v -m 0755 -D "$BIN_PATH" "$BIN_PATH_INSTALLED"
    mkdir -p "$(dirname "$CONFIG")"
    # shellcheck disable=SC2016
    CFG=${CONFIG_DIR} BIN=${BIN_PATH_INSTALLED} envsubst '$CFG $BIN' <"./config/$SERVICE" >"$SYSTEMD_PATH_FILE"
    systemctl daemon-reload
    gen_apikey
    gen_config_file
    set_local_port
}

# --------------------------------- #

install_bouncer

systemctl enable "$SERVICE"
if [ "$READY" = "yes" ]; then
    systemctl start "$SERVICE"
else
    msg warn "service not started. You need to get an API key and configure it in $CONFIG"
fi

msg succ "The $BOUNCER service has been installed."
exit 0


================================================
FILE: scripts/uninstall.sh
================================================
#!/bin/sh

set -eu

. ./scripts/_bouncer.sh

assert_root

# --------------------------------- #

uninstall() {
    systemctl stop "$SERVICE" || true
    delete_bouncer
    rm -f "$CONFIG"
    rm -f "$SYSTEMD_PATH_FILE"
    rm -f "$BIN_PATH_INSTALLED"
    rm -f "/var/log/$BOUNCER.log"
}

uninstall
msg succ "$BOUNCER has been successfully uninstalled"
exit 0


================================================
FILE: scripts/upgrade.sh
================================================
#!/bin/sh

set -eu

. ./scripts/_bouncer.sh

assert_root

# --------------------------------- #

systemctl stop "$SERVICE"

if ! upgrade_bin; then
    msg err "failed to upgrade $BOUNCER"
    exit 1
fi

systemctl start "$SERVICE" || msg warn "$SERVICE failed to start, please check the systemd logs"

msg succ "$BOUNCER upgraded successfully."
exit 0


================================================
FILE: test/.python-version
================================================
3.12


================================================
FILE: test/README.md
================================================


================================================
FILE: test/default.env
================================================
CROWDSEC_TEST_VERSION="dev"
CROWDSEC_TEST_FLAVORS="full"
CROWDSEC_TEST_NETWORK="net-test"


================================================
FILE: test/pyproject.toml
================================================
[project]
name = "cs-firewall-bouncer-tests"
version = "0.1.0"
description = "Tests for cs-firewall-bouncer"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "flask>=3.1.0",
    "pexpect>=4.9.0",
    "psutil>=6.1.1",
    "pytest>=8.3.4",
    "pytest-cs>=0.7.24",
    "pytest-dependency>=0.6.0",
    "pytest-dotenv>=0.5.2",
    "pytimeparse>=1.1.8",
    "zxcvbn>=4.4.28",
]

[tool.uv.sources]
pytest-cs = { git = "https://github.com/crowdsecurity/pytest-cs" }

[dependency-groups]
dev = [
    "basedpyright>=1.26.0",
    "ipdb>=0.13.13",
    "ruff>=0.9.4",
]

#[tool.uv.sources]
#pytest-cs = { path = "../../../pytest-cs", editable = true }


[tool.ruff]

line-length = 120

[tool.ruff.lint]
select = [
  "ALL"
]

ignore = [
    "ANN",      # Missing type annotations
    "A002",     # Function argument `id` is shadowing a Python builtin
    "ARG001",   # Unused function argument: `...`
    "COM812",   # Trailing comma missing
    "D100",     # Missing docstring in public module
    "D101",     # Missing docstring in public class
    "D102",     # Missing docstring in public method
    "D103",     # Missing docstring in public function
    "D104",     # Missing docstring in public package
    "D107",     # Missing docstring in __init__
    "D203",     # incorrect-blank-line-before-class
    "D212",     # Multi-line docstring summary should start at the first line
    "D212",     # Multi-line docstring summary should start at the first line
    "D400",     # First line should end with a period
    "D415",     # First line should end with a period, question mark, or exclamation point
    "DTZ005",   # `datetime.datetime.now()` called without a `tz` argument
    "DTZ901",   # Use of `datetime.datetime.min` without timezone information
    "EM102",    # Exception must not use an f-string literal, assign to variable first
    "ERA001",   # Found commented-out code
    "FBT002",   # Boolean default positional argument in function definition
    "FIX002",   # Line contains TODO, consider resolving the issue
    "FIX003",   # Line contains XXX, consider resolving the issue
    "N802",     # Function name `testLogging` should be lowercase
    "PLW1510",  # `subprocess.run` without explicit `check` argument
    "S101",     # Use of 'assert' detected
    "S104",     # Possible binding to all interfaces
    "S314",     # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents
    "S603",     # `subprocess` call: check for execution of untrusted input
    "S604",     # Function call with `shell=True` parameter identified, security issue
    "S607",     # Starting a process with a partial executable path
    "SIM108",   # Use ternary operator `...` instead of `if`-`else`-block
    "TD001",    # Invalid TODO tag: `XXX`
    "TD002",    # Missing author in TODO
    "TD003",    # Missing issue link for this TODO
    "TRY003",   # Avoid specifying long messages outside the exception class
    "PLR2004",  # Magic value used in comparison, consider replacing `...` with a constant variable
    "PLR0913",  # Too many arguments in function definition (6 > 5)
    "PTH107",   # `os.remove()` should be replaced by `Path.unlink()`
    "PTH108",   # `os.unlink()` should be replaced by `Path.unlink()`
    "PTH110",   # `os.path.exists()` should be replaced by `Path.exists()`
    "PTH116",   # `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()`
    "PTH120",   # `os.path.dirname()` should be replaced by `Path.parent`
    "PTH123",   # `open()` should be replaced by `Path.open()`
    "PT009",    # Use a regular `assert` instead of unittest-style `assertEqual`
    "PT022",    # No teardown in fixture `fw_cfg_factory`, use `return` instead of `yield`
    "TID252",   # Prefer absolute imports over relative imports from parent modules
    "UP022",    # Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
]

[tool.basedpyright]
reportAny = "none"
reportArgumentType = "none"
reportAttributeAccessIssue = "none"
reportImplicitOverride = "none"
reportImplicitStringConcatenation = "none"
reportMissingParameterType = "none"
reportMissingTypeStubs = "none"
reportOptionalMemberAccess = "none"
reportUnannotatedClassAttribute = "none"
reportUninitializedInstanceVariable = "none"
reportUnknownArgumentType = "none"
reportUnknownMemberType = "none"
reportUnknownParameterType = "none"
reportUnknownVariableType = "none"
reportUnusedCallResult = "none"
reportUnusedParameter = "none"


================================================
FILE: test/pytest.ini
================================================
[pytest]
addopts =
    --pdbcls=IPython.terminal.debugger:Pdb
    --ignore=tests/install
    --ignore=tests/backends
    --strict-markers
markers:
    deb: mark tests related to deb packaging
    rpm: mark tests related to rpm packaging
    systemd_debug: dump systemd status and journal on test failure
env_files =
    .env
    default.env


================================================
FILE: test/tests/__init__.py
================================================


================================================
FILE: test/tests/backends/__init__.py
================================================


================================================
FILE: test/tests/backends/iptables/__init__.py
================================================


================================================
FILE: test/tests/backends/iptables/crowdsec-firewall-bouncer-logging.yaml
================================================
mode: iptables
update_frequency: 0.1s
log_mode: stdout
log_dir: ./
log_level: info
api_url: http://127.0.0.1:8081/
api_key: 1237adaf7a1724ac68a3288828820a67
disable_ipv6: false
deny_action: DROP
deny_log: true
deny_log_prefix: "blocked by crowdsec"
supported_decisions_types:
  - ban
iptables_chains:
  - INPUT


================================================
FILE: test/tests/backends/iptables/crowdsec-firewall-bouncer.yaml
================================================
mode: iptables
update_frequency: 0.1s
log_mode: stdout
log_dir: ./
log_level: info
api_url: http://127.0.0.1:8081/
api_key: 1237adaf7a1724ac68a3288828820a67
disable_ipv6: false
deny_action: DROP
deny_log: false
supported_decisions_types:
  - ban
iptables_chains:
  - INPUT


================================================
FILE: test/tests/backends/iptables/test_iptables.py
================================================
import os
import subprocess
import unittest
import xml.etree.ElementTree as ET
from ipaddress import ip_address
from pathlib import Path
from time import sleep

from ..mock_lapi import MockLAPI
from ..utils import generate_n_decisions, new_decision, run_cmd

SCRIPT_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
PROJECT_ROOT = SCRIPT_DIR.parent.parent.parent.parent
BINARY_PATH = PROJECT_ROOT.joinpath("crowdsec-firewall-bouncer")
CONFIG_PATH = SCRIPT_DIR.joinpath("crowdsec-firewall-bouncer.yaml")
CONFIG_PATH_LOGGING = SCRIPT_DIR.joinpath("crowdsec-firewall-bouncer-logging.yaml")

SET_NAME_IPV4 = "crowdsec-blacklists-0"
SET_NAME_IPV6 = "crowdsec6-blacklists-0"

RULES_CHAIN_NAME = "CROWDSEC_CHAIN"
LOGGING_CHAIN_NAME = "CROWDSEC_LOG"
CHAIN_NAME = "INPUT"


class TestIPTables(unittest.TestCase):
    def setUp(self):
        self.fb = subprocess.Popen([BINARY_PATH, "-c", CONFIG_PATH])
        self.lapi = MockLAPI()
        self.lapi.start()
        return super().setUp()

    def tearDown(self):
        self.fb.kill()
        self.fb.wait()
        self.lapi.stop()

    def test_table_rule_set_are_created(self):
        d1 = generate_n_decisions(3)
        d2 = generate_n_decisions(1, ipv4=False)
        self.lapi.ds.insert_decisions(d1 + d2)
        sleep(3)

        # IPV4 Chain
        # Check the rules with the sets
        output = run_cmd("iptables", "-L", RULES_CHAIN_NAME)
        rules = [line for line in output.split("\n") if SET_NAME_IPV4 in line]

        self.assertEqual(len(rules), 1)
        assert f"match-set {SET_NAME_IPV4} src" in rules[0]

        # Check the JUMP to CROWDSEC_CHAIN
        output = run_cmd("iptables", "-L", CHAIN_NAME)
        rules = [line for line in output.split("\n") if RULES_CHAIN_NAME in line]

        self.assertEqual(len(rules), 1)
        assert f"{RULES_CHAIN_NAME}" in rules[0]

        # IPV6 Chain
        output = run_cmd("ip6tables", "-L", RULES_CHAIN_NAME)
        rules = [line for line in output.split("\n") if SET_NAME_IPV6 in line]

        self.assertEqual(len(rules), 1)
        assert f"match-set {SET_NAME_IPV6} src" in rules[0]

        # Check the JUMP to CROWDSEC_CHAIN
        output = run_cmd("ip6tables", "-L", CHAIN_NAME)
        rules = [line for line in output.split("\n") if RULES_CHAIN_NAME in line]

        self.assertEqual(len(rules), 1)
        assert f"{RULES_CHAIN_NAME}" in rules[0]

        output = run_cmd("ipset", "list")

        assert SET_NAME_IPV6 in output
        assert SET_NAME_IPV4 in output

    def test_duplicate_decisions_across_decision_stream(self):
        d1, d2, d3 = generate_n_decisions(3, dup_count=1)
        self.lapi.ds.insert_decisions([d1])
        sleep(3)
        res = get_set_elements(SET_NAME_IPV4)
        self.assertEqual(res, {"0.0.0.0"})

        self.lapi.ds.insert_decisions([d2, d3])
        sleep(3)
        assert self.fb.poll() is None
        self.assertEqual(get_set_elements(SET_NAME_IPV4), {"0.0.0.0", "0.0.0.1"})

        self.lapi.ds.delete_decision_by_id(d1["id"])
        self.lapi.ds.delete_decision_by_id(d2["id"])
        sleep(3)
        self.assertEqual(get_set_elements(SET_NAME_IPV4), set())
        assert self.fb.poll() is None

        self.lapi.ds.delete_decision_by_id(d3["id"])
        sleep(3)
        self.assertEqual(get_set_elements(SET_NAME_IPV6), set())
        assert self.fb.poll() is None

    def test_decision_insertion_deletion_ipv4(self):
        total_decisions, duplicate_decisions = 100, 23
        decisions = generate_n_decisions(total_decisions, dup_count=duplicate_decisions)
        self.lapi.ds.insert_decisions(decisions)
        sleep(3)  # let the bouncer insert the decisions

        set_elements = get_set_elements(SET_NAME_IPV4)
        self.assertEqual(len(set_elements), total_decisions - duplicate_decisions)
        self.assertEqual({i["value"] for i in decisions}, set_elements)
        self.assertIn("0.0.0.0", set_elements)

        self.lapi.ds.delete_decisions_by_ip("0.0.0.0")
        sleep(3)

        set_elements = get_set_elements(SET_NAME_IPV4)
        self.assertEqual({i["value"] for i in decisions if i["value"] != "0.0.0.0"}, set_elements)
        self.assertEqual(len(set_elements), total_decisions - duplicate_decisions - 1)
        self.assertNotIn("0.0.0.0", set_elements)

    def test_decision_insertion_deletion_ipv6(self):
        total_decisions, duplicate_decisions = 100, 23
        decisions = generate_n_decisions(total_decisions, dup_count=duplicate_decisions, ipv4=False)
        self.lapi.ds.insert_decisions(decisions)
        sleep(3)

        set_elements = get_set_elements(SET_NAME_IPV6)
        set_elements = set(map(ip_address, set_elements))
        self.assertEqual(len(set_elements), total_decisions - duplicate_decisions)
        self.assertEqual({ip_address(i["value"]) for i in decisions}, set_elements)
        self.assertIn(ip_address("::1:0:3"), set_elements)

        self.lapi.ds.delete_decisions_by_ip("::1:0:3")
        sleep(3)

        set_elements = get_set_elements(SET_NAME_IPV6)
        set_elements = set(map(ip_address, set_elements))
        self.assertEqual(len(set_elements), total_decisions - duplicate_decisions - 1)
        self.assertEqual(
            {ip_address(i["value"]) for i in decisions if ip_address(i["value"]) != ip_address("::1:0:3")},
            set_elements,
        )
        self.assertNotIn(ip_address("::1:0:3"), set_elements)

    def test_longest_decision_insertion(self):
        decisions = [
            {
                "value": "123.45.67.12",
                "scope": "ip",
                "type": "ban",
                "origin": "script",
                "duration": f"{i}h",
                "reason": "for testing",
            }
            for i in range(1, 201)
        ]
        self.lapi.ds.insert_decisions(decisions)
        sleep(3)
        elems = get_set_elements(SET_NAME_IPV4, with_timeout=True)
        self.assertEqual(len(elems), 1)
        elems = list(elems)
        self.assertEqual(elems[0][0], "123.45.67.12")
        self.assertLessEqual(abs(elems[0][1] - 200 * 60 * 60), 15)


def get_set_elements(set_name, with_timeout=False):
    output = run_cmd("ipset", "list", "-o", "xml")
    root = ET.fromstring(output)
    elements = set()
    for member in root.findall(f"ipset[@name='{set_name}']/members/member"):
        if with_timeout:
            to_add = (member.find("elem").text, int(member.find("timeout").text))
        else:
            to_add = member.find("elem").text
        elements.add(to_add)
    return elements


class TestIPTablesLogging(unittest.TestCase):
    def setUp(self):
        self.fb = subprocess.Popen([BINARY_PATH, "-c", CONFIG_PATH_LOGGING])
        self.lapi = MockLAPI()
        self.lapi.start()
        return super().setUp()

    def tearDown(self):
        self.fb.kill()
        self.fb.wait()
        self.lapi.stop()

    def testLogging(self):
        # We use 1.1.1.1 because we want to see some dropped packets in the logs
        # We know this IP responds to ping, and the response will be dropped by the firewall
        d = new_decision("1.1.1.1")
        self.lapi.ds.insert_decisions([d])
        sleep(3)

        # Check if our logging chain is in place

        output = run_cmd("iptables", "-L", LOGGING_CHAIN_NAME)
        rules = [line for line in output.split("\n") if "anywhere" in line]

        # 2 rules: one logging, one generic drop
        self.assertEqual(len(rules), 2)

        # Check if the logging chain is called from the main chain
        output = run_cmd("iptables", "-L", CHAIN_NAME)

        rules = [line for line in output.split("\n") if RULES_CHAIN_NAME in line]

        self.assertEqual(len(rules), 1)

        # Check if logging/drop chain is called from the rules chain
        output = run_cmd("iptables", "-L", RULES_CHAIN_NAME)

        rules = [line for line in output.split("\n") if LOGGING_CHAIN_NAME in line]

        self.assertEqual(len(rules), 1)

        # Now, try to ping the IP

        output = run_cmd(
            "curl", "--connect-timeout", "1", "1.1.1.1", ignore_error=True
        )  # We don't care about the output, we just want to trigger the rule

        # Check if the firewall has logged the dropped response

        output = run_cmd("dmesg | tail -n 10", shell=True)

        assert "blocked by crowdsec" in output


================================================
FILE: test/tests/backends/mock_lapi.py
================================================
import datetime
import logging
from datetime import timedelta
from ipaddress import ip_address
from threading import Thread
from time import sleep

from flask import Flask, abort, request
from pytimeparse.timeparse import timeparse
from werkzeug.serving import make_server


# This is the "database" of our dummy LAPI
class DataStore:
    def __init__(self) -> None:
        self.id = 0
        self.decisions = []
        self.bouncer_lastpull_by_api_key = {}

    def insert_decisions(self, decisions):
        for i, _ in enumerate(decisions):
            decisions[i]["created_at"] = datetime.datetime.now()
            decisions[i]["deleted_at"] = self.get_decision_expiry_time(decisions[i])
            decisions[i]["id"] = self.id
            self.id += 1
        self.decisions.extend(decisions)

    # This methods can be made more generic by taking lambda expr as input for filtering
    # decisions to delete
    def delete_decisions_by_ip(self, ip):
        for i, decision in enumerate(self.decisions):
            if ip_address(decision["value"]) == ip_address(ip):
                self.decisions[i]["deleted_at"] = datetime.datetime.now()

    def delete_decision_by_id(self, id):
        for i, decision in enumerate(self.decisions):
            if decision["id"] == id:
                self.decisions[i]["deleted_at"] = datetime.datetime.now()
                break

    def update_bouncer_pull(self, api_key):
        self.bouncer_lastpull_by_api_key[api_key] = datetime.datetime.now()

    def get_active_and_expired_decisions_since(self, since):
        expired_decisions = []
        active_decisions = []

        for decision in self.decisions:
            # decision["deleted_at"] > datetime.datetime.now()  means that decision hasn't yet expired
            if decision["deleted_at"] > since and decision["deleted_at"] < datetime.datetime.now():
                expired_decisions.append(decision)

            elif decision["created_at"] > since:
                active_decisions.append(decision)
        return active_decisions, expired_decisions

    def get_decisions_for_bouncer(self, api_key, startup=False):
        if startup or api_key not in self.bouncer_lastpull_by_api_key:
            since = datetime.datetime.min
            self.bouncer_lastpull_by_api_key[api_key] = since
        else:
            since = self.bouncer_lastpull_by_api_key[api_key]

        self.update_bouncer_pull(api_key)
        return self.get_active_and_expired_decisions_since(since)

    @staticmethod
    def get_decision_expiry_time(decision):
        return decision["created_at"] + timedelta(seconds=timeparse(decision["duration"]))


class MockLAPI:
    def __init__(self) -> None:
        self.app = Flask(__name__)
        self.app.add_url_rule("/v1/decisions/stream", view_func=self.decisions)
        log = logging.getLogger("werkzeug")
        log.setLevel(logging.ERROR)
        self.app.logger.disabled = True
        log.disabled = True
        self.ds = DataStore()

    def decisions(self):
        api_key = request.headers.get("x-api-key")
        if not api_key:
            abort(404)
        startup = request.args.get("startup") == "true"
        active_decisions, expired_decisions = self.ds.get_decisions_for_bouncer(api_key, startup)
        return {
            "new": formatted_decisions(active_decisions),
            "deleted": formatted_decisions(expired_decisions),
        }

    def start(self, port=8081):
        self.server_thread = ServerThread(self.app, port=port)
        self.server_thread.start()

    def stop(self):
        self.server_thread.shutdown()


def formatted_decisions(decisions):
    formatted_decisions = []
    for decision in decisions:
        expiry_time = decision["created_at"] + timedelta(seconds=timeparse(decision["duration"]))
        duration = expiry_time - datetime.datetime.now()
        formatted_decisions.append(
            {
                "duration": f"{duration.total_seconds()}s",
                "id": decision["id"],
                "origin": decision["origin"],
                "scenario": "cscli",
                "scope": decision["scope"],
                "type": decision["type"],
                "value": decision["value"],
            }
        )
    return formatted_decisions


# Copied from https://stackoverflow.com/a/45017691 .
# We run server inside thread instead of process to avoid
# huge complexity of sharing objects
class ServerThread(Thread):
    def __init__(self, app, port=8081):
        Thread.__init__(self)
        self.server = make_server("127.0.0.1", port, app)
        self.ctx = app.app_context()
        self.ctx.push()

    def run(self):
        self.server.serve_forever()

    def shutdown(self):
        self.server.shutdown()


if __name__ == "__main__":
    MockLAPI().start()
    sleep(100)


================================================
FILE: test/tests/backends/nftables/__init__.py
================================================


================================================
FILE: test/tests/backends/nftables/crowdsec-firewall-bouncer.yaml
================================================
mode: nftables
update_frequency: 0.01s
log_mode: stdout
log_dir: ./
log_level: info
api_url: http://127.0.0.1:8081/
api_key: 1237adaf7a1724ac68a3288828820a67
disable_ipv6: false
deny_action: DROP
deny_log: false
supported_decisions_types:
  - ban
iptables_chains:
  - INPUT

nftables_hooks:
  - input
  - forward

nftables:
  ipv4:
    enabled: true
    set-only: false
    table: crowdsec
    chain: crowdsec-chain
  ipv6:
    enabled: true
    set-only: false
    table: crowdsec6
    chain: crowdsec6-chain


================================================
FILE: test/tests/backends/nftables/test_nftables.py
================================================
import json
import os
import subprocess
import unittest
from ipaddress import ip_address
from pathlib import Path
from time import sleep

from ..mock_lapi import MockLAPI
from ..utils import generate_n_decisions, run_cmd

SCRIPT_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
PROJECT_ROOT = SCRIPT_DIR.parent.parent.parent.parent
BINARY_PATH = PROJECT_ROOT.joinpath("crowdsec-firewall-bouncer")
CONFIG_PATH = SCRIPT_DIR.joinpath("crowdsec-firewall-bouncer.yaml")


class TestNFTables(unittest.TestCase):
    def setUp(self):
        self.fb = subprocess.Popen([BINARY_PATH, "-c", CONFIG_PATH])
        self.lapi = MockLAPI()
        self.lapi.start()
        return super().setUp()

    def tearDown(self):
        self.fb.kill()
        self.fb.wait()
        self.lapi.stop()
        run_cmd("nft", "delete", "table", "ip", "crowdsec", ignore_error=True)
        run_cmd("nft", "delete", "table", "ip6", "crowdsec6", ignore_error=True)

    def test_table_rule_set_are_created(self):
        d1 = generate_n_decisions(3)
        d2 = generate_n_decisions(1, ipv4=False)
        self.lapi.ds.insert_decisions(d1 + d2)
        sleep(1)
        output = json.loads(run_cmd("nft", "-j", "list", "tables"))
        tables = {(node["table"]["family"], node["table"]["name"]) for node in output["nftables"] if "table" in node}
        assert ("ip6", "crowdsec6") in tables
        assert ("ip", "crowdsec") in tables

        # IPV4
        output = json.loads(run_cmd("nft", "-j", "list", "table", "ip", "crowdsec"))
        sets = {
            (node["set"]["family"], node["set"]["name"], node["set"]["type"])
            for node in output["nftables"]
            if "set" in node
        }
        assert ("ip", "crowdsec-blacklists-script", "ipv4_addr") in sets
        rules = {node["rule"]["chain"] for node in output["nftables"] if "rule" in node}  # maybe stricter check ?
        assert "crowdsec-chain-forward" in rules
        assert "crowdsec-chain-input" in rules

        # IPV6
        output = json.loads(run_cmd("nft", "-j", "list", "table", "ip6", "crowdsec6"))
        sets = {
            (node["set"]["family"], node["set"]["name"], node["set"]["type"])
            for node in output["nftables"]
            if "set" in node
        }
        assert ("ip6", "crowdsec6-blacklists-script", "ipv6_addr") in sets

        rules = {node["rule"]["chain"] for node in output["nftables"] if "rule" in node}  # maybe stricter check ?
        assert "crowdsec6-chain-input" in rules
        assert "crowdsec6-chain-forward" in rules

    def test_duplicate_decisions_across_decision_stream(self):
        d1, d2, d3 = generate_n_decisions(3, dup_count=1)
        self.lapi.ds.insert_decisions([d1])
        sleep(1)
        self.assertEqual(
            get_set_elements("ip", "crowdsec", "crowdsec-blacklists-script"),
            {"0.0.0.0"},
        )

        self.lapi.ds.insert_decisions([d2, d3])
        sleep(1)
        assert self.fb.poll() is None
        self.assertEqual(
            get_set_elements("ip", "crowdsec", "crowdsec-blacklists-script"),
            {"0.0.0.0", "0.0.0.1"},
        )

        self.lapi.ds.delete_decision_by_id(d1["id"])
        self.lapi.ds.delete_decision_by_id(d2["id"])
        sleep(1)
        self.assertEqual(get_set_elements("ip", "crowdsec", "crowdsec-blacklists-script"), set())
        assert self.fb.poll() is None

        self.lapi.ds.delete_decision_by_id(d3["id"])
        sleep(1)
        self.assertEqual(get_set_elements("ip", "crowdsec", "crowdsec-blacklists-script"), set())
        assert self.fb.poll() is None

    def test_decision_insertion_deletion_ipv4(self):
        total_decisions, duplicate_decisions = 100, 23
        decisions = generate_n_decisions(total_decisions, dup_count=duplicate_decisions)
        self.lapi.ds.insert_decisions(decisions)
        sleep(1)  # let the bouncer insert the decisions

        set_elements = get_set_elements("ip", "crowdsec", "crowdsec-blacklists-script")
        self.assertEqual(len(set_elements), total_decisions - duplicate_decisions)
        assert {i["value"] for i in decisions} == set_elements
        assert "0.0.0.0" in set_elements

        self.lapi.ds.delete_decisions_by_ip("0.0.0.0")
        sleep(1)

        set_elements = get_set_elements("ip", "crowdsec", "crowdsec-blacklists-script")
        assert {i["value"] for i in decisions if i["value"] != "0.0.0.0"} == set_elements
        assert len(set_elements) == total_decisions - duplicate_decisions - 1
        assert "0.0.0.0" not in set_elements

    def test_decision_insertion_deletion_ipv6(self):
        total_decisions, duplicate_decisions = 100, 23
        decisions = generate_n_decisions(total_decisions, dup_count=duplicate_decisions, ipv4=False)
        self.lapi.ds.insert_decisions(decisions)
        sleep(1)

        set_elements = get_set_elements("ip6", "crowdsec6", "crowdsec6-blacklists-script")
        set_elements = set(map(ip_address, set_elements))
        assert len(set_elements) == total_decisions - duplicate_decisions
        assert {ip_address(i["value"]) for i in decisions} == set_elements
        assert ip_address("::1:0:3") in set_elements

        self.lapi.ds.delete_decisions_by_ip("::1:0:3")
        sleep(1)

        set_elements = get_set_elements("ip6", "crowdsec6", "crowdsec6-blacklists-script")
        set_elements = set(map(ip_address, set_elements))
        self.assertEqual(len(set_elements), total_decisions - duplicate_decisions - 1)
        assert (
            {ip_address(i["value"]) for i in decisions if ip_address(i["value"]) != ip_address("::1:0:3")}
        ) == set_elements
        assert ip_address("::1:0:3") not in set_elements

    def test_longest_decision_insertion(self):
        decisions = [
            {
                "value": "123.45.67.12",
                "scope": "ip",
                "type": "ban",
                "origin": "script",
                "duration": f"{i}h",
                "reason": "for testing",
            }
            for i in range(1, 201)
        ]
        self.lapi.ds.insert_decisions(decisions)
        sleep(1)
        elems = get_set_elements("ip", "crowdsec", "crowdsec-blacklists-script", with_timeout=True)
        assert len(elems) == 1
        elems = list(elems)
        assert elems[0][0] == "123.45.67.12"
        assert abs(elems[0][1] - 200 * 60 * 60) <= 3


def get_set_elements(family, table_name, set_name, with_timeout=False):
    output = json.loads(run_cmd("nft", "-j", "list", "set", family, table_name, set_name))
    for node in output["nftables"]:
        if "set" not in node or "elem" not in node["set"]:
            continue
        if not isinstance(node["set"]["elem"][0], dict):
            return set(node["set"]["elem"])

        if not with_timeout:
            return {elem["elem"]["val"] for elem in node["set"]["elem"]}
        return {(elem["elem"]["val"], elem["elem"]["timeout"]) for elem in node["set"]["elem"]}
    return set()


================================================
FILE: test/tests/backends/utils.py
================================================
import subprocess
from ipaddress import ip_address


def run_cmd(*cmd, ignore_error=False, shell=False):
    p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, shell=shell)
    if not ignore_error and p.returncode:
        raise SystemExit(f"{cmd} exited with non-zero code with following logs:\n {p.stdout}")

    return p.stdout


def generate_n_decisions(n: int, action="ban", dup_count=0, ipv4=True, duration="4h"):
    if dup_count >= n:
        raise SystemExit(f"generate_n_decisions got dup_count={dup_count} which is >=n")

    unique_decision_count = n - dup_count
    decisions = []
    for i in range(unique_decision_count):
        if ipv4:
            ip = ip_address(i)
        else:
            ip = ip_address(2**32 + i)
        decisions.append(
            {
                "value": ip.__str__(),
                "scope": "ip",
                "type": action,
                "origin": "script",
                "duration": duration,
                "reason": "for testing",
            }
        )
    decisions += decisions[: n % unique_decision_count]
    decisions *= n // unique_decision_count
    return decisions


def new_decision(ip: str):
    return {
        "value": ip,
        "scope": "ip",
        "type": "ban",
        "origin": "script",
        "duration": "4h",
        "reason": "for testing",
    }


================================================
FILE: test/tests/bouncer/__init__.py
================================================


================================================
FILE: test/tests/bouncer/test_firewall_bouncer.py
================================================
import json


def test_backend_mode(bouncer, fw_cfg_factory):
    cfg = fw_cfg_factory()

    del cfg["mode"]

    with bouncer(cfg) as fw:
        fw.wait_for_lines_fnmatch(
            [
                "*unable to load configuration: config does not contain 'mode'*",
            ]
        )
        fw.proc.wait(timeout=0.2)
        assert not fw.proc.is_running()

    cfg["mode"] = "whatever"

    with bouncer(cfg) as fw:
        fw.wait_for_lines_fnmatch(
            [
                "*firewall 'whatever' is not supported*",
            ]
        )
        fw.proc.wait(timeout=0.2)
        assert not fw.proc.is_running()

    cfg["mode"] = "dry-run"

    with bouncer(cfg) as fw:
        fw.wait_for_lines_fnmatch(
            [
                "*Starting c
Download .txt
gitextract_hkmsgkt_/

├── .envrc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   ├── config.yml
│   │   └── feature_request.yaml
│   ├── dependabot.yml
│   ├── governance.yml
│   ├── release-drafter.yml
│   ├── release.py
│   └── workflows/
│       ├── build-binary-package.yml
│       ├── governance-bot.yaml
│       ├── lint.yml
│       ├── release-drafter.yml
│       ├── tests.yml
│       └── tests_deb.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── Makefile
├── README.md
├── cmd/
│   └── root.go
├── config/
│   ├── crowdsec-firewall-bouncer.service
│   └── crowdsec-firewall-bouncer.yaml
├── debian/
│   ├── changelog
│   ├── compat
│   ├── control
│   ├── crowdsec-firewall-bouncer-iptables.postinst
│   ├── crowdsec-firewall-bouncer-iptables.postrm
│   ├── crowdsec-firewall-bouncer-iptables.preinst
│   ├── crowdsec-firewall-bouncer-iptables.prerm
│   ├── crowdsec-firewall-bouncer-nftables.postinst
│   ├── crowdsec-firewall-bouncer-nftables.postrm
│   ├── crowdsec-firewall-bouncer-nftables.preinst
│   ├── crowdsec-firewall-bouncer-nftables.prerm
│   └── rules
├── flake.nix
├── go.mod
├── go.sum
├── main.go
├── pkg/
│   ├── backend/
│   │   └── backend.go
│   ├── cfg/
│   │   ├── config.go
│   │   └── logging.go
│   ├── dryrun/
│   │   └── dryrun.go
│   ├── ipsetcmd/
│   │   └── ipset.go
│   ├── iptables/
│   │   ├── iptables.go
│   │   ├── iptables_context.go
│   │   ├── iptables_stub.go
│   │   └── metrics.go
│   ├── metrics/
│   │   └── metrics.go
│   ├── nftables/
│   │   ├── metrics.go
│   │   ├── nftables.go
│   │   ├── nftables_context.go
│   │   └── nftables_stub.go
│   ├── pf/
│   │   ├── metrics.go
│   │   ├── metrics_test.go
│   │   ├── pf.go
│   │   └── pf_context.go
│   └── types/
│       └── types.go
├── rpm/
│   ├── SOURCES/
│   │   └── 80-crowdsec-firewall-bouncer.preset
│   └── SPECS/
│       └── crowdsec-firewall-bouncer.spec
├── scripts/
│   ├── _bouncer.sh
│   ├── install.sh
│   ├── uninstall.sh
│   └── upgrade.sh
└── test/
    ├── .python-version
    ├── README.md
    ├── default.env
    ├── pyproject.toml
    ├── pytest.ini
    └── tests/
        ├── __init__.py
        ├── backends/
        │   ├── __init__.py
        │   ├── iptables/
        │   │   ├── __init__.py
        │   │   ├── crowdsec-firewall-bouncer-logging.yaml
        │   │   ├── crowdsec-firewall-bouncer.yaml
        │   │   └── test_iptables.py
        │   ├── mock_lapi.py
        │   ├── nftables/
        │   │   ├── __init__.py
        │   │   ├── crowdsec-firewall-bouncer.yaml
        │   │   └── test_nftables.py
        │   └── utils.py
        ├── bouncer/
        │   ├── __init__.py
        │   ├── test_firewall_bouncer.py
        │   ├── test_iptables_deny_action.py
        │   ├── test_tls.py
        │   └── test_yaml_local.py
        ├── conftest.py
        ├── install/
        │   ├── __init__.py
        │   ├── no_crowdsec/
        │   │   ├── __init__.py
        │   │   ├── test_no_crowdsec_deb.py
        │   │   └── test_no_crowdsec_scripts.py
        │   └── with_crowdsec/
        │       ├── __init__.py
        │       ├── test_crowdsec_deb.py
        │       └── test_crowdsec_scripts.py
        └── pkg/
            ├── __init__.py
            ├── test_build_deb.py
            ├── test_build_rpm.py
            └── test_scripts_nonroot.py
Download .txt
SYMBOL INDEX (245 symbols across 38 files)

FILE: .github/release.py
  function _goos (line 11) | def _goos():
  function _goarch (line 16) | def _goarch(goos):
  function _goarm (line 27) | def _goarm(goarch):
  function _build_tarball (line 35) | def _build_tarball(os):
  function filename_for_entry (line 42) | def filename_for_entry(prog_name, entry):
  function matrix (line 52) | def matrix(prog_name):
  function print_matrix (line 69) | def print_matrix(prog_name):
  function run_build (line 93) | def run_build(prog_name):
  function main (line 141) | def main():

FILE: cmd/root.go
  constant bouncerType (line 33) | bouncerType = "crowdsec-firewall-bouncer"
  function backendCleanup (line 37) | func backendCleanup(backend *backend.BackendCTX) {
  function HandleSignals (line 45) | func HandleSignals(ctx context.Context) error {
  function deleteDecisions (line 64) | func deleteDecisions(backend *backend.BackendCTX, decisions []*models.De...
  function addDecisions (line 104) | func addDecisions(backend *backend.BackendCTX, decisions []*models.Decis...
  function Execute (line 141) | func Execute() error {

FILE: main.go
  function main (line 9) | func main() {

FILE: pkg/backend/backend.go
  type BackendCTX (line 20) | type BackendCTX struct
    method Init (line 24) | func (b *BackendCTX) Init() error {
    method Commit (line 28) | func (b *BackendCTX) Commit() error {
    method ShutDown (line 32) | func (b *BackendCTX) ShutDown() error {
    method Add (line 36) | func (b *BackendCTX) Add(decision *models.Decision) error {
    method Delete (line 40) | func (b *BackendCTX) Delete(decision *models.Decision) error {
    method CollectMetrics (line 44) | func (b *BackendCTX) CollectMetrics() {
  function isPFSupported (line 49) | func isPFSupported(runtimeOS string) bool {
  function NewBackend (line 62) | func NewBackend(config *cfg.BouncerConfig) (*BackendCTX, error) {

FILE: pkg/cfg/config.go
  type PrometheusConfig (line 15) | type PrometheusConfig struct
  type nftablesFamilyConfig (line 21) | type nftablesFamilyConfig struct
  constant IpsetMode (line 30) | IpsetMode    = "ipset"
  constant IptablesMode (line 31) | IptablesMode = "iptables"
  constant NftablesMode (line 32) | NftablesMode = "nftables"
  constant PfMode (line 33) | PfMode       = "pf"
  constant DryRunMode (line 34) | DryRunMode   = "dry-run"
  type BouncerConfig (line 37) | type BouncerConfig struct
  function MergedConfig (line 75) | func MergedConfig(configPath string) ([]byte, error) {
  function NewConfig (line 86) | func NewConfig(reader io.Reader) (*BouncerConfig, error) {
  function pfConfig (line 165) | func pfConfig(config *BouncerConfig) error {
  function nftablesConfig (line 173) | func nftablesConfig(config *BouncerConfig) error {

FILE: pkg/cfg/logging.go
  type LoggingConfig (line 17) | type LoggingConfig struct
    method LoggerForFile (line 27) | func (c *LoggingConfig) LoggerForFile(fileName string) (io.Writer, err...
    method setDefaults (line 46) | func (c *LoggingConfig) setDefaults() {
    method validate (line 76) | func (c *LoggingConfig) validate() error {
    method setup (line 84) | func (c *LoggingConfig) setup(fileName string) error {

FILE: pkg/dryrun/dryrun.go
  type dryRun (line 12) | type dryRun struct
    method Init (line 18) | func (*dryRun) Init() error {
    method Commit (line 23) | func (*dryRun) Commit() error {
    method Add (line 28) | func (*dryRun) Add(decision *models.Decision) error {
    method CollectMetrics (line 33) | func (*dryRun) CollectMetrics() {
    method Delete (line 37) | func (*dryRun) Delete(decision *models.Decision) error {
    method ShutDown (line 42) | func (*dryRun) ShutDown() error {
  function NewDryRun (line 14) | func NewDryRun(_ *cfg.BouncerConfig) (types.Backend, error) {

FILE: pkg/ipsetcmd/ipset.go
  type IPSet (line 13) | type IPSet struct
    method Create (line 41) | func (i *IPSet) Create(opts CreateOptions) error {
    method Add (line 72) | func (i *IPSet) Add(entry string) error {
    method DeleteEntry (line 85) | func (i *IPSet) DeleteEntry(entry string) error {
    method List (line 98) | func (i *IPSet) List() ([]string, error) {
    method Flush (line 111) | func (i *IPSet) Flush() error {
    method Destroy (line 124) | func (i *IPSet) Destroy() error {
    method Rename (line 137) | func (i *IPSet) Rename(toSetName string) error {
    method Test (line 152) | func (i *IPSet) Test(entry string) error {
    method Save (line 165) | func (i *IPSet) Save() ([]string, error) {
    method Restore (line 178) | func (i *IPSet) Restore(filename string) error {
    method Swap (line 191) | func (i *IPSet) Swap(toSetName string) error {
    method Name (line 206) | func (i *IPSet) Name() string {
    method Exists (line 210) | func (i *IPSet) Exists() bool {
    method Len (line 218) | func (i *IPSet) Len() int {
  type CreateOptions (line 18) | type CreateOptions struct
  constant ipsetBinary (line 26) | ipsetBinary = "ipset"
  function NewIPSet (line 28) | func NewIPSet(setName string) (*IPSet, error) {
  function GetSetsStartingWith (line 250) | func GetSetsStartingWith(name string) (map[string]*IPSet, error) {

FILE: pkg/iptables/iptables.go
  constant IPTablesDroppedPacketIdx (line 22) | IPTablesDroppedPacketIdx = 0
  constant IPTablesDroppedByteIdx (line 23) | IPTablesDroppedByteIdx   = 1
  type iptables (line 26) | type iptables struct
    method Init (line 150) | func (ipt *iptables) Init() error {
    method Commit (line 179) | func (ipt *iptables) Commit() error {
    method Add (line 197) | func (ipt *iptables) Add(decision *models.Decision) error {
    method ShutDown (line 221) | func (ipt *iptables) ShutDown() error {
    method Delete (line 237) | func (ipt *iptables) Delete(decision *models.Decision) error {
  function NewIPTables (line 31) | func NewIPTables(config *cfg.BouncerConfig) (types.Backend, error) {

FILE: pkg/iptables/iptables_context.go
  constant chainName (line 22) | chainName           = "CROWDSEC_CHAIN"
  constant loggingChainName (line 23) | loggingChainName    = "CROWDSEC_LOG"
  constant dockerUserChainName (line 24) | dockerUserChainName = "DOCKER-USER"
  constant maxBanSeconds (line 25) | maxBanSeconds       = 2147483
  constant defaultTimeout (line 26) | defaultTimeout      = "300"
  type ipTablesContext (line 29) | type ipTablesContext struct
    method chainExist (line 59) | func (ctx *ipTablesContext) chainExist(chainName string) bool {
    method setupChain (line 70) | func (ctx *ipTablesContext) setupChain() {
    method deleteChain (line 138) | func (ctx *ipTablesContext) deleteChain() {
    method createRule (line 194) | func (ctx *ipTablesContext) createRule(setName string, origin string) {
    method commit (line 216) | func (ctx *ipTablesContext) commit() error {
    method add (line 362) | func (ctx *ipTablesContext) add(decision *models.Decision) {
    method shutDown (line 366) | func (ctx *ipTablesContext) shutDown() error {
    method delete (line 397) | func (ctx *ipTablesContext) delete(decision *models.Decision) error {

FILE: pkg/iptables/iptables_stub.go
  function NewIPTables (line 10) | func NewIPTables(config *cfg.BouncerConfig) (types.Backend, error) {

FILE: pkg/iptables/metrics.go
  method collectMetricsIptables (line 42) | func (ctx *ipTablesContext) collectMetricsIptables(scanner *bufio.Scanne...
  type chainCounters (line 136) | type chainCounters struct
  method collectMetricsIpset (line 143) | func (ctx *ipTablesContext) collectMetricsIpset(scanner *bufio.Scanner) ...
  method collectMetrics (line 240) | func (ctx *ipTablesContext) collectMetrics() (map[string]uint64, map[str...
  method CollectMetrics (line 273) | func (ipt *iptables) CollectMetrics() {

FILE: pkg/metrics/metrics.go
  constant CollectionInterval (line 16) | CollectionInterval = time.Second * 10
  type metricName (line 18) | type metricName
  constant DroppedPackets (line 21) | DroppedPackets   metricName = "fw_bouncer_dropped_packets"
  constant DroppedBytes (line 22) | DroppedBytes     metricName = "fw_bouncer_dropped_bytes"
  constant ProcessedPackets (line 23) | ProcessedPackets metricName = "fw_bouncer_processed_packets"
  constant ProcessedBytes (line 24) | ProcessedBytes   metricName = "fw_bouncer_processed_bytes"
  constant ActiveBannedIPs (line 25) | ActiveBannedIPs  metricName = "fw_bouncer_banned_ips"
  type backendCollector (line 28) | type backendCollector interface
  type Handler (line 32) | type Handler struct
    method MetricsUpdater (line 130) | func (m Handler) MetricsUpdater(met *models.RemediationComponentsMetri...
    method ComputeMetricsHandler (line 201) | func (m Handler) ComputeMetricsHandler(next http.Handler) http.Handler {
  type metricConfig (line 36) | type metricConfig struct
  type metricMap (line 45) | type metricMap
    method MustRegisterAll (line 47) | func (m metricMap) MustRegisterAll() {
  function getLabelValue (line 119) | func getLabelValue(labels []*io_prometheus_client.LabelPair, key string)...

FILE: pkg/nftables/metrics.go
  method collectDroppedPackets (line 18) | func (c *nftContext) collectDroppedPackets() (map[string]uint64, map[str...
  method collectActiveBannedIPs (line 63) | func (c *nftContext) collectActiveBannedIPs() (map[string]int, error) {
  method collectDropped (line 85) | func (c *nftContext) collectDropped() (map[string]uint64, map[string]uin...
  function getOriginForList (line 103) | func getOriginForList(origin string) string {
  method CollectMetrics (line 111) | func (n *nft) CollectMetrics() {

FILE: pkg/nftables/nftables.go
  constant chunkSize (line 21) | chunkSize      = 200
  constant defaultTimeout (line 22) | defaultTimeout = "4h"
  type nft (line 25) | type nft struct
    method Init (line 49) | func (n *nft) Init() error {
    method Add (line 65) | func (n *nft) Add(decision *models.Decision) error {
    method getBannedState (line 70) | func (n *nft) getBannedState() (map[string]struct{}, error) {
    method reset (line 83) | func (n *nft) reset() {
    method commitDeletedDecisions (line 88) | func (n *nft) commitDeletedDecisions() error {
    method createSetAndRuleForOrigin (line 142) | func (n *nft) createSetAndRuleForOrigin(ctx *nftContext, origin string...
    method commitAddedDecisions (line 173) | func (n *nft) commitAddedDecisions() error {
    method Commit (line 253) | func (n *nft) Commit() error {
    method Delete (line 307) | func (n *nft) Delete(decision *models.Decision) error {
    method ShutDown (line 312) | func (n *nft) ShutDown() error {
  function NewNFTables (line 36) | func NewNFTables(config *cfg.BouncerConfig) (*nft, error) {
  type tmpDecisions (line 263) | type tmpDecisions struct
  function normalizedDecisions (line 270) | func normalizedDecisions(decisions []*models.Decision) []*models.Decision {

FILE: pkg/nftables/nftables_context.go
  type nftContext (line 30) | type nftContext struct
    method setBanned (line 113) | func (c *nftContext) setBanned(banned map[string]struct{}) error {
    method initSetOnly (line 132) | func (c *nftContext) initSetOnly() error {
    method initOwnTable (line 170) | func (c *nftContext) initOwnTable(hooks []string) error {
    method init (line 232) | func (c *nftContext) init(hooks []string) error {
    method lookupTable (line 260) | func (c *nftContext) lookupTable() (*nftables.Table, error) {
    method createRule (line 275) | func (c *nftContext) createRule(chain *nftables.Chain, set *nftables.Set,
    method deleteElementChunk (line 348) | func (c *nftContext) deleteElementChunk(els []nftables.SetElement) err...
    method deleteElements (line 377) | func (c *nftContext) deleteElements(els []nftables.SetElement) error {
    method addElements (line 393) | func (c *nftContext) addElements(els map[string][]nftables.SetElement)...
    method shutDown (line 421) | func (c *nftContext) shutDown() error {
  function reprIP (line 48) | func reprIP(ip []byte) string {
  function NewNFTV4Context (line 52) | func NewNFTV4Context(config *cfg.BouncerConfig) *nftContext {
  function NewNFTV6Context (line 82) | func NewNFTV6Context(config *cfg.BouncerConfig) *nftContext {

FILE: pkg/nftables/nftables_stub.go
  function NewNFTables (line 10) | func NewNFTables(config *cfg.BouncerConfig) (types.Backend, error) {

FILE: pkg/pf/metrics.go
  type counter (line 16) | type counter struct
  function parseMetrics (line 27) | func parseMetrics(reader *strings.Reader, tables []string) map[string]co...
  function countIPs (line 83) | func countIPs(table string) int {
  method CollectMetrics (line 99) | func (pf *pf) CollectMetrics() {

FILE: pkg/pf/metrics_test.go
  function TestParseMetrics (line 11) | func TestParseMetrics(t *testing.T) {

FILE: pkg/pf/pf.go
  type pf (line 17) | type pf struct
    method Init (line 69) | func (pf *pf) Init() error {
    method Commit (line 93) | func (pf *pf) Commit() error {
    method Add (line 103) | func (pf *pf) Add(decision *models.Decision) error {
    method reset (line 108) | func (pf *pf) reset() {
    method commitDeletedDecisions (line 113) | func (pf *pf) commitDeletedDecisions() error {
    method commitAddedDecisions (line 144) | func (pf *pf) commitAddedDecisions() error {
    method Delete (line 175) | func (pf *pf) Delete(decision *models.Decision) error {
    method ShutDown (line 180) | func (pf *pf) ShutDown() error {
  constant pfctlCmd (line 25) | pfctlCmd = "/sbin/pfctl"
  constant pfDevice (line 26) | pfDevice = "/dev/pf"
  function NewPF (line 29) | func NewPF(config *cfg.BouncerConfig) (types.Backend, error) {
  function execPfctl (line 59) | func execPfctl(anchor string, arg ...string) *exec.Cmd {

FILE: pkg/pf/pf_context.go
  type pfContext (line 17) | type pfContext struct
    method checkTable (line 76) | func (ctx *pfContext) checkTable() error {
    method shutDown (line 97) | func (ctx *pfContext) shutDown() error {
    method add (line 150) | func (ctx *pfContext) add(decisions []*models.Decision) error {
    method delete (line 208) | func (ctx *pfContext) delete(decisions []*models.Decision) error {
    method init (line 227) | func (ctx *pfContext) init() error {
  constant backendName (line 24) | backendName = "pf"
  function decisionsToIPs (line 26) | func decisionsToIPs(decisions []*models.Decision) []string {
  function writeIPsToFile (line 40) | func writeIPsToFile(ips []string) (string, error) {
  function getStateIPs (line 109) | func getStateIPs() (map[string]bool, error) {

FILE: pkg/types/types.go
  type Backend (line 7) | type Backend interface

FILE: test/tests/backends/iptables/test_iptables.py
  class TestIPTables (line 26) | class TestIPTables(unittest.TestCase):
    method setUp (line 27) | def setUp(self):
    method tearDown (line 33) | def tearDown(self):
    method test_table_rule_set_are_created (line 38) | def test_table_rule_set_are_created(self):
    method test_duplicate_decisions_across_decision_stream (line 78) | def test_duplicate_decisions_across_decision_stream(self):
    method test_decision_insertion_deletion_ipv4 (line 101) | def test_decision_insertion_deletion_ipv4(self):
    method test_decision_insertion_deletion_ipv6 (line 120) | def test_decision_insertion_deletion_ipv6(self):
    method test_longest_decision_insertion (line 144) | def test_longest_decision_insertion(self):
  function get_set_elements (line 165) | def get_set_elements(set_name, with_timeout=False):
  class TestIPTablesLogging (line 178) | class TestIPTablesLogging(unittest.TestCase):
    method setUp (line 179) | def setUp(self):
    method tearDown (line 185) | def tearDown(self):
    method testLogging (line 190) | def testLogging(self):

FILE: test/tests/backends/mock_lapi.py
  class DataStore (line 14) | class DataStore:
    method __init__ (line 15) | def __init__(self) -> None:
    method insert_decisions (line 20) | def insert_decisions(self, decisions):
    method delete_decisions_by_ip (line 30) | def delete_decisions_by_ip(self, ip):
    method delete_decision_by_id (line 35) | def delete_decision_by_id(self, id):
    method update_bouncer_pull (line 41) | def update_bouncer_pull(self, api_key):
    method get_active_and_expired_decisions_since (line 44) | def get_active_and_expired_decisions_since(self, since):
    method get_decisions_for_bouncer (line 57) | def get_decisions_for_bouncer(self, api_key, startup=False):
    method get_decision_expiry_time (line 68) | def get_decision_expiry_time(decision):
  class MockLAPI (line 72) | class MockLAPI:
    method __init__ (line 73) | def __init__(self) -> None:
    method decisions (line 82) | def decisions(self):
    method start (line 93) | def start(self, port=8081):
    method stop (line 97) | def stop(self):
  function formatted_decisions (line 101) | def formatted_decisions(decisions):
  class ServerThread (line 123) | class ServerThread(Thread):
    method __init__ (line 124) | def __init__(self, app, port=8081):
    method run (line 130) | def run(self):
    method shutdown (line 133) | def shutdown(self):

FILE: test/tests/backends/nftables/test_nftables.py
  class TestNFTables (line 18) | class TestNFTables(unittest.TestCase):
    method setUp (line 19) | def setUp(self):
    method tearDown (line 25) | def tearDown(self):
    method test_table_rule_set_are_created (line 32) | def test_table_rule_set_are_created(self):
    method test_duplicate_decisions_across_decision_stream (line 67) | def test_duplicate_decisions_across_decision_stream(self):
    method test_decision_insertion_deletion_ipv4 (line 95) | def test_decision_insertion_deletion_ipv4(self):
    method test_decision_insertion_deletion_ipv6 (line 114) | def test_decision_insertion_deletion_ipv6(self):
    method test_longest_decision_insertion (line 137) | def test_longest_decision_insertion(self):
  function get_set_elements (line 158) | def get_set_elements(family, table_name, set_name, with_timeout=False):

FILE: test/tests/backends/utils.py
  function run_cmd (line 5) | def run_cmd(*cmd, ignore_error=False, shell=False):
  function generate_n_decisions (line 13) | def generate_n_decisions(n: int, action="ban", dup_count=0, ipv4=True, d...
  function new_decision (line 39) | def new_decision(ip: str):

FILE: test/tests/bouncer/test_firewall_bouncer.py
  function test_backend_mode (line 4) | def test_backend_mode(bouncer, fw_cfg_factory):
  function test_api_url (line 44) | def test_api_url(crowdsec, bouncer, fw_cfg_factory):
  function test_api_key (line 68) | def test_api_key(crowdsec, bouncer, fw_cfg_factory, api_key_factory, bou...

FILE: test/tests/bouncer/test_iptables_deny_action.py
  function test_iptables_deny_action (line 1) | def test_iptables_deny_action(bouncer, fw_cfg_factory):

FILE: test/tests/bouncer/test_tls.py
  function test_tls_server (line 4) | def test_tls_server(crowdsec, certs_dir, api_key_factory, bouncer, fw_cf...
  function test_tls_mutual (line 56) | def test_tls_mutual(crowdsec, certs_dir, api_key_factory, bouncer, fw_cf...
  function test_api_key_and_cert (line 120) | def test_api_key_and_cert(crowdsec, certs_dir, api_key_factory, bouncer,...

FILE: test/tests/bouncer/test_yaml_local.py
  function test_yaml_local (line 4) | def test_yaml_local(bouncer, fw_cfg_factory):

FILE: test/tests/conftest.py
  function bouncer_under_test (line 11) | def bouncer_under_test():
  function bouncer_with_lapi (line 18) | def bouncer_with_lapi(bouncer, crowdsec, fw_cfg_factory, api_key_factory...
  function fw_cfg_factory (line 54) | def fw_cfg_factory():

FILE: test/tests/install/no_crowdsec/test_no_crowdsec_deb.py
  function test_deb_install_purge (line 9) | def test_deb_install_purge(deb_package_path, bouncer_under_test, must_be...

FILE: test/tests/install/no_crowdsec/test_no_crowdsec_scripts.py
  function test_install_no_crowdsec (line 13) | def test_install_no_crowdsec(project_repo, bouncer_binary, must_be_root):
  function test_upgrade_no_crowdsec (line 44) | def test_upgrade_no_crowdsec(project_repo, must_be_root):
  function test_uninstall_no_crowdsec (line 59) | def test_uninstall_no_crowdsec(project_repo, must_be_root):

FILE: test/tests/install/with_crowdsec/test_crowdsec_deb.py
  function test_deb_install_purge (line 15) | def test_deb_install_purge(deb_package_path, bouncer_under_test, must_be...
  function test_deb_install_purge_yaml_local (line 75) | def test_deb_install_purge_yaml_local(deb_package_path, bouncer_under_te...

FILE: test/tests/install/with_crowdsec/test_crowdsec_scripts.py
  function test_install_crowdsec (line 15) | def test_install_crowdsec(project_repo, bouncer_binary, must_be_root):
  function test_upgrade_crowdsec (line 56) | def test_upgrade_crowdsec(project_repo, must_be_root):
  function test_uninstall_crowdsec (line 71) | def test_uninstall_crowdsec(project_repo, must_be_root):

FILE: test/tests/pkg/test_build_deb.py
  function test_deb_build (line 8) | def test_deb_build(deb_package, skip_unless_deb):

FILE: test/tests/pkg/test_build_rpm.py
  function test_rpm_build (line 6) | def test_rpm_build(rpm_package, skip_unless_rpm):

FILE: test/tests/pkg/test_scripts_nonroot.py
  function test_scripts_nonroot (line 5) | def test_scripts_nonroot(project_repo, bouncer_binary, must_be_nonroot):
Condensed preview — 96 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (236K chars).
[
  {
    "path": ".envrc",
    "chars": 10,
    "preview": "use flake\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "chars": 1883,
    "preview": "name: Bug report\ndescription: Report a bug encountered while operating crowdsec\nlabels: kind/bug\nbody:\n  - type: textare"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 140,
    "preview": "contact_links:\n  - name: Support Request\n    url: https://discourse.crowdsec.net\n    about: Support request or question "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "chars": 894,
    "preview": "name: Feature request\ndescription: Suggest an improvement or a new feature\nbody:\n  - type: textarea\n    id: feature\n    "
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 468,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n   "
  },
  {
    "path": ".github/governance.yml",
    "chars": 1155,
    "preview": "version: v1\n\nissue:\n  captures:\n    - regex: 'version: v(.+)-'\n      github_release: true\n      ignore_case: true\n      "
  },
  {
    "path": ".github/release-drafter.yml",
    "chars": 44,
    "preview": "template: |\n  ## What’s Changed\n\n  $CHANGES\n"
  },
  {
    "path": ".github/release.py",
    "chars": 3838,
    "preview": "#!/usr/bin/env python3\n\nimport argparse\nimport json\nimport os\nimport shutil\nimport subprocess\nimport sys\n\n\ndef _goos():\n"
  },
  {
    "path": ".github/workflows/build-binary-package.yml",
    "chars": 1008,
    "preview": "name: build-binary-package\n\non:\n  release:\n    types:\n    - prereleased\n\npermissions:\n  # Use write for: hub release edi"
  },
  {
    "path": ".github/workflows/governance-bot.yaml",
    "chars": 1080,
    "preview": "# .github/workflow/governance.yml\n\non:\n  pull_request_target:\n    types: [ synchronize, opened, labeled, unlabeled ]\n  i"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 1172,
    "preview": "name: Static Analysis\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\n\njobs:\n  build:\n    na"
  },
  {
    "path": ".github/workflows/release-drafter.yml",
    "chars": 940,
    "preview": "name: Release Drafter\n\non:\n  push:\n    # branches to consider in the event; optional, defaults to all\n    branches:\n    "
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 2659,
    "preview": "name: Build + tests\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\npermissions:\n  contents:"
  },
  {
    "path": ".github/workflows/tests_deb.yml",
    "chars": 1675,
    "preview": "name: Test .deb packaging\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\npermissions:\n  con"
  },
  {
    "path": ".gitignore",
    "chars": 758,
    "preview": "# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.o"
  },
  {
    "path": ".golangci.yml",
    "chars": 3574,
    "preview": "version: \"2\"\n\nlinters:\n  default: all\n  disable:\n    - cyclop    # revive\n    - funlen    # revive\n    - gocognit  # rev"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2020-2021 crowdsecurity\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "Makefile",
    "chars": 3315,
    "preview": "GO = go\nGOBUILD = $(GO) build\nGOTEST = $(GO) test\n\nBINARY_NAME=crowdsec-firewall-bouncer\nTARBALL_NAME=$(BINARY_NAME).tgz"
  },
  {
    "path": "README.md",
    "chars": 1132,
    "preview": "<p align=\"center\">\n<img src=\"https://github.com/crowdsecurity/cs-firewall-bouncer/raw/main/docs/assets/crowdsec_linux_lo"
  },
  {
    "path": "cmd/root.go",
    "chars": 7240,
    "preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"slices\"\n\t\"strings\"\n\t\"s"
  },
  {
    "path": "config/crowdsec-firewall-bouncer.service",
    "chars": 561,
    "preview": "[Unit]\nDescription=The firewall bouncer for CrowdSec\nAfter=syslog.target network.target remote-fs.target nss-lookup.targ"
  },
  {
    "path": "config/crowdsec-firewall-bouncer.yaml",
    "chars": 1245,
    "preview": "mode: ${BACKEND}\nupdate_frequency: 10s\nlog_mode: file\nlog_dir: /var/log/\nlog_level: info\nlog_compression: true\nlog_max_s"
  },
  {
    "path": "debian/changelog",
    "chars": 171,
    "preview": "crowdsec-firewall-bouncer (1.0.12) UNRELEASED; urgency=medium\n\n  * debian package\n  * pf support\n\n -- Manuel Sabban <man"
  },
  {
    "path": "debian/compat",
    "chars": 3,
    "preview": "11\n"
  },
  {
    "path": "debian/control",
    "chars": 616,
    "preview": "Source: crowdsec-firewall-bouncer\nMaintainer: Crowdsec Team <debian@crowdsec.net>\nBuild-Depends: debhelper\nSection: admi"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-iptables.postinst",
    "chars": 607,
    "preview": "#!/bin/sh\n\nsystemctl daemon-reload\n\n#shellcheck source=./scripts/_bouncer.sh\n. \"/usr/lib/$DPKG_MAINTSCRIPT_PACKAGE/_boun"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-iptables.postrm",
    "chars": 310,
    "preview": "#!/bin/sh\n\nset -eu\n\nBOUNCER=\"crowdsec-firewall-bouncer\"\nCONFIG=\"/etc/crowdsec/bouncers/$BOUNCER.yaml\"\n\nif [ \"$1\" = \"purg"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-iptables.preinst",
    "chars": 77,
    "preview": "#!/bin/sh\n\nset -e\n\n# Source debconf library.\n. /usr/share/debconf/confmodule\n"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-iptables.prerm",
    "chars": 175,
    "preview": "#!/bin/sh\n\nset -eu\n\nBOUNCER=\"crowdsec-firewall-bouncer\"\n\nsystemctl stop \"$BOUNCER\" || echo \"cannot stop service\"\nsystemc"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-nftables.postinst",
    "chars": 607,
    "preview": "#!/bin/sh\n\nsystemctl daemon-reload\n\n#shellcheck source=./scripts/_bouncer.sh\n. \"/usr/lib/$DPKG_MAINTSCRIPT_PACKAGE/_boun"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-nftables.postrm",
    "chars": 310,
    "preview": "#!/bin/sh\n\nset -eu\n\nBOUNCER=\"crowdsec-firewall-bouncer\"\nCONFIG=\"/etc/crowdsec/bouncers/$BOUNCER.yaml\"\n\nif [ \"$1\" = \"purg"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-nftables.preinst",
    "chars": 106,
    "preview": "#!/bin/sh\n\nset -e\n\n# Source debconf library.\n. /usr/share/debconf/confmodule\n\necho \"pre-inst (nftables)\" \n"
  },
  {
    "path": "debian/crowdsec-firewall-bouncer-nftables.prerm",
    "chars": 175,
    "preview": "#!/bin/sh\n\nset -eu\n\nBOUNCER=\"crowdsec-firewall-bouncer\"\n\nsystemctl stop \"$BOUNCER\" || echo \"cannot stop service\"\nsystemc"
  },
  {
    "path": "debian/rules",
    "chars": 1481,
    "preview": "#!/usr/bin/make -f\n\nexport DEB_VERSION=$(shell dpkg-parsechangelog | grep -E '^Version:' | cut -f 2 -d ' ')\nexport BUILD"
  },
  {
    "path": "flake.nix",
    "chars": 1095,
    "preview": "{\n  description = \"A Nix-flake-based Go 1.22 development environment\";\n\n  inputs.nixpkgs.url = \"https://flakehub.com/f/N"
  },
  {
    "path": "go.mod",
    "chars": 2827,
    "preview": "module github.com/crowdsecurity/cs-firewall-bouncer\n\ngo 1.25.2\n\nrequire (\n\tgithub.com/crowdsecurity/crowdsec v1.7.7\n\tgit"
  },
  {
    "path": "go.sum",
    "chars": 12605,
    "preview": "github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngit"
  },
  {
    "path": "main.go",
    "chars": 188,
    "preview": "package main\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/crowdsecurity/cs-firewall-bouncer/cmd\"\n)\n\nfunc ma"
  },
  {
    "path": "pkg/backend/backend.go",
    "chars": 2439,
    "preview": "package backend\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/crowdsecurity/cro"
  },
  {
    "path": "pkg/cfg/config.go",
    "chars": 5757,
    "preview": "package cfg\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v2\"\n\n\t\"github.com/crowds"
  },
  {
    "path": "pkg/cfg/logging.go",
    "chars": 2276,
    "preview": "package cfg\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/sir"
  },
  {
    "path": "pkg/dryrun/dryrun.go",
    "chars": 937,
    "preview": "package dryrun\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/crowdsecurity/crowdsec/pkg/models\"\n\n\t\"github.co"
  },
  {
    "path": "pkg/ipsetcmd/ipset.go",
    "chars": 5592,
    "preview": "package ipsetcmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype "
  },
  {
    "path": "pkg/iptables/iptables.go",
    "chars": 6319,
    "preview": "//go:build linux\n\npackage iptables\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen"
  },
  {
    "path": "pkg/iptables/iptables_context.go",
    "chars": 10485,
    "preview": "//go:build linux\n\npackage iptables\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"gith"
  },
  {
    "path": "pkg/iptables/iptables_stub.go",
    "chars": 251,
    "preview": "//go:build !linux\n\npackage iptables\n\nimport (\n\t\"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg\"\n\t\"github.com/crowd"
  },
  {
    "path": "pkg/iptables/metrics.go",
    "chars": 10030,
    "preview": "//go:build linux\n\npackage iptables\n\nimport (\n\t\"bufio\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/prometheu"
  },
  {
    "path": "pkg/metrics/metrics.go",
    "chars": 6264,
    "preview": "package metrics\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tio_prometheus_client \""
  },
  {
    "path": "pkg/nftables/metrics.go",
    "chars": 5074,
    "preview": "//go:build linux\n\npackage nftables\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/nftables\"\n\t\"github.com/googl"
  },
  {
    "path": "pkg/nftables/nftables.go",
    "chars": 6930,
    "preview": "//go:build linux\n\npackage nftables\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/nftables\"\n\t\"github.co"
  },
  {
    "path": "pkg/nftables/nftables_context.go",
    "chars": 10950,
    "preview": "//go:build linux\n\npackage nftables\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/google/nftables\"\n\t\"github.com/google"
  },
  {
    "path": "pkg/nftables/nftables_stub.go",
    "chars": 251,
    "preview": "//go:build !linux\n\npackage nftables\n\nimport (\n\t\"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg\"\n\t\"github.com/crowd"
  },
  {
    "path": "pkg/pf/metrics.go",
    "chars": 3675,
    "preview": "package pf\n\nimport (\n\t\"bufio\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/prometheus/client_golang/prometheu"
  },
  {
    "path": "pkg/pf/metrics_test.go",
    "chars": 1178,
    "preview": "package pf\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\""
  },
  {
    "path": "pkg/pf/pf.go",
    "chars": 4354,
    "preview": "package pf\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/crowdsecurity/c"
  },
  {
    "path": "pkg/pf/pf_context.go",
    "chars": 4967,
    "preview": "package pf\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"maps\"\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t"
  },
  {
    "path": "pkg/types/types.go",
    "chars": 247,
    "preview": "package types\n\nimport (\n\t\"github.com/crowdsecurity/crowdsec/pkg/models\"\n)\n\ntype Backend interface {\n\tInit() error\n\tShutD"
  },
  {
    "path": "rpm/SOURCES/80-crowdsec-firewall-bouncer.preset",
    "chars": 91,
    "preview": "# This file is part of crowdsec-firewall-bouncer\n\nenable crowdsec-firewall-bouncer.service\n"
  },
  {
    "path": "rpm/SPECS/crowdsec-firewall-bouncer.spec",
    "chars": 5187,
    "preview": "Name:      crowdsec-firewall-bouncer-iptables\nVersion:   %(echo $VERSION)\nRelease:   %(echo $PACKAGE_NUMBER)%{?dist}\nSum"
  },
  {
    "path": "scripts/_bouncer.sh",
    "chars": 5533,
    "preview": "#!/bin/sh\n#shellcheck disable=SC3043\n\nset -eu\n\nBOUNCER=\"crowdsec-firewall-bouncer\"\nBOUNCER_PREFIX=$(echo \"$BOUNCER\" | se"
  },
  {
    "path": "scripts/install.sh",
    "chars": 4039,
    "preview": "#!/bin/sh\n\nset -eu\n\n. ./scripts/_bouncer.sh\n\nassert_root\n\n# --------------------------------- #\n\nAPI_KEY=\"<API_KEY>\"\n\nin"
  },
  {
    "path": "scripts/uninstall.sh",
    "chars": 359,
    "preview": "#!/bin/sh\n\nset -eu\n\n. ./scripts/_bouncer.sh\n\nassert_root\n\n# --------------------------------- #\n\nuninstall() {\n    syste"
  },
  {
    "path": "scripts/upgrade.sh",
    "chars": 351,
    "preview": "#!/bin/sh\n\nset -eu\n\n. ./scripts/_bouncer.sh\n\nassert_root\n\n# --------------------------------- #\n\nsystemctl stop \"$SERVIC"
  },
  {
    "path": "test/.python-version",
    "chars": 5,
    "preview": "3.12\n"
  },
  {
    "path": "test/README.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/default.env",
    "chars": 90,
    "preview": "CROWDSEC_TEST_VERSION=\"dev\"\nCROWDSEC_TEST_FLAVORS=\"full\"\nCROWDSEC_TEST_NETWORK=\"net-test\"\n"
  },
  {
    "path": "test/pyproject.toml",
    "chars": 4519,
    "preview": "[project]\nname = \"cs-firewall-bouncer-tests\"\nversion = \"0.1.0\"\ndescription = \"Tests for cs-firewall-bouncer\"\nreadme = \"R"
  },
  {
    "path": "test/pytest.ini",
    "chars": 341,
    "preview": "[pytest]\naddopts =\n    --pdbcls=IPython.terminal.debugger:Pdb\n    --ignore=tests/install\n    --ignore=tests/backends\n   "
  },
  {
    "path": "test/tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/backends/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/backends/iptables/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/backends/iptables/crowdsec-firewall-bouncer-logging.yaml",
    "chars": 311,
    "preview": "mode: iptables\nupdate_frequency: 0.1s\nlog_mode: stdout\nlog_dir: ./\nlog_level: info\napi_url: http://127.0.0.1:8081/\napi_k"
  },
  {
    "path": "test/tests/backends/iptables/crowdsec-firewall-bouncer.yaml",
    "chars": 273,
    "preview": "mode: iptables\nupdate_frequency: 0.1s\nlog_mode: stdout\nlog_dir: ./\nlog_level: info\napi_url: http://127.0.0.1:8081/\napi_k"
  },
  {
    "path": "test/tests/backends/iptables/test_iptables.py",
    "chars": 8356,
    "preview": "import os\nimport subprocess\nimport unittest\nimport xml.etree.ElementTree as ET\nfrom ipaddress import ip_address\nfrom pat"
  },
  {
    "path": "test/tests/backends/mock_lapi.py",
    "chars": 4827,
    "preview": "import datetime\nimport logging\nfrom datetime import timedelta\nfrom ipaddress import ip_address\nfrom threading import Thr"
  },
  {
    "path": "test/tests/backends/nftables/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/backends/nftables/crowdsec-firewall-bouncer.yaml",
    "chars": 510,
    "preview": "mode: nftables\nupdate_frequency: 0.01s\nlog_mode: stdout\nlog_dir: ./\nlog_level: info\napi_url: http://127.0.0.1:8081/\napi_"
  },
  {
    "path": "test/tests/backends/nftables/test_nftables.py",
    "chars": 6976,
    "preview": "import json\nimport os\nimport subprocess\nimport unittest\nfrom ipaddress import ip_address\nfrom pathlib import Path\nfrom t"
  },
  {
    "path": "test/tests/backends/utils.py",
    "chars": 1376,
    "preview": "import subprocess\nfrom ipaddress import ip_address\n\n\ndef run_cmd(*cmd, ignore_error=False, shell=False):\n    p = subproc"
  },
  {
    "path": "test/tests/bouncer/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/bouncer/test_firewall_bouncer.py",
    "chars": 3427,
    "preview": "import json\n\n\ndef test_backend_mode(bouncer, fw_cfg_factory):\n    cfg = fw_cfg_factory()\n\n    del cfg[\"mode\"]\n\n    with "
  },
  {
    "path": "test/tests/bouncer/test_iptables_deny_action.py",
    "chars": 1458,
    "preview": "def test_iptables_deny_action(bouncer, fw_cfg_factory):\n    cfg = fw_cfg_factory()\n\n    cfg[\"log_level\"] = \"trace\"\n    c"
  },
  {
    "path": "test/tests/bouncer/test_tls.py",
    "chars": 5638,
    "preview": "import json\n\n\ndef test_tls_server(crowdsec, certs_dir, api_key_factory, bouncer, fw_cfg_factory):\n    \"\"\"TLS with server"
  },
  {
    "path": "test/tests/bouncer/test_yaml_local.py",
    "chars": 1045,
    "preview": "import os\n\n\ndef test_yaml_local(bouncer, fw_cfg_factory):\n    cfg = fw_cfg_factory()\n\n    cfg.pop(\"mode\")\n\n    with boun"
  },
  {
    "path": "test/tests/conftest.py",
    "chars": 1726,
    "preview": "import contextlib\n\nimport pytest\nfrom pytest_cs import plugin\n\n# pytest_exception_interact = plugin.pytest_exception_int"
  },
  {
    "path": "test/tests/install/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/install/no_crowdsec/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/install/no_crowdsec/test_no_crowdsec_deb.py",
    "chars": 1539,
    "preview": "import os\nimport subprocess\n\nimport pytest\n\npytestmark = pytest.mark.deb\n\n\ndef test_deb_install_purge(deb_package_path, "
  },
  {
    "path": "test/tests/install/no_crowdsec/test_no_crowdsec_scripts.py",
    "chars": 2318,
    "preview": "import os\nimport re\n\nimport pexpect\nimport pytest\nimport yaml\n\nBOUNCER = \"crowdsec-firewall-bouncer\"\nCONFIG = f\"/etc/cro"
  },
  {
    "path": "test/tests/install/with_crowdsec/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/install/with_crowdsec/test_crowdsec_deb.py",
    "chars": 4242,
    "preview": "import os\nimport subprocess\nfrom pathlib import Path\n\nimport pytest\nimport yaml\nfrom zxcvbn import zxcvbn\n\npytestmark = "
  },
  {
    "path": "test/tests/install/with_crowdsec/test_crowdsec_scripts.py",
    "chars": 2883,
    "preview": "import os\nimport re\n\nimport pexpect\nimport pytest\nimport yaml\nfrom pytest_cs.lib import cscli, text\n\nBOUNCER = \"crowdsec"
  },
  {
    "path": "test/tests/pkg/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/tests/pkg/test_build_deb.py",
    "chars": 318,
    "preview": "import pytest\n\npytestmark = pytest.mark.deb\n\n\n# This test has the side effect of building the package and leaving it in "
  },
  {
    "path": "test/tests/pkg/test_build_rpm.py",
    "chars": 210,
    "preview": "import pytest\n\npytestmark = pytest.mark.rpm\n\n\ndef test_rpm_build(rpm_package, skip_unless_rpm):\n    \"\"\"Test that the pac"
  },
  {
    "path": "test/tests/pkg/test_scripts_nonroot.py",
    "chars": 579,
    "preview": "import os\nimport subprocess\n\n\ndef test_scripts_nonroot(project_repo, bouncer_binary, must_be_nonroot):\n    assert os.get"
  }
]

About this extraction

This page contains the full source code of the crowdsecurity/cs-firewall-bouncer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 96 files (208.7 KB), approximately 66.3k tokens, and a symbol index with 245 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!